From 0beaf2d6431e519e912422b43e881ef5486caf27 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 22 Jul 2022 12:09:48 +0100 Subject: [PATCH 001/418] Fixes #177 (Revise class and method visibilities related to styles to use them programmatically). --- .../src/com/structurizr/view/ThemeUtils.java | 2 +- .../src/com/structurizr/view/Styles.java | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/structurizr-client/src/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/com/structurizr/view/ThemeUtils.java index 045e71579..ce1eef78b 100644 --- a/structurizr-client/src/com/structurizr/view/ThemeUtils.java +++ b/structurizr-client/src/com/structurizr/view/ThemeUtils.java @@ -80,7 +80,7 @@ public static void loadThemes(Workspace workspace) throws Exception { Theme theme = objectMapper.readValue(json, Theme.class); - workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(url, theme.getElements(), theme.getRelationships()); + workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(theme); } httpClient.close(); diff --git a/structurizr-core/src/com/structurizr/view/Styles.java b/structurizr-core/src/com/structurizr/view/Styles.java index e5e33813b..44a290ddc 100644 --- a/structurizr-core/src/com/structurizr/view/Styles.java +++ b/structurizr-core/src/com/structurizr/view/Styles.java @@ -17,7 +17,7 @@ public final class Styles { private Collection elements = new LinkedList<>(); private Collection relationships = new LinkedList<>(); - private Map themes = new LinkedHashMap<>(); + private List themes = new ArrayList<>(); public Collection getElements() { return elements; @@ -100,7 +100,7 @@ public ElementStyle findElementStyle(String tag) { ElementStyle style = new ElementStyle(tag); Collection elementStyles = new ArrayList<>(); - for (Theme theme : themes.values()) { + for (Theme theme : themes) { elementStyles.addAll(theme.getElements()); } elementStyles.addAll(elements); @@ -131,7 +131,7 @@ public RelationshipStyle findRelationshipStyle(String tag) { RelationshipStyle style = new RelationshipStyle(tag); Collection relationshipStyles= new ArrayList<>(); - for (Theme theme : themes.values()) { + for (Theme theme : themes) { relationshipStyles.addAll(theme.getRelationships()); } relationshipStyles.addAll(relationships); @@ -228,8 +228,15 @@ public RelationshipStyle findRelationshipStyle(Relationship relationship) { return style; } - void addStylesFromTheme(String url, Collection elements, Collection relationships) { - themes.put(url, new Theme(elements, relationships)); + /** + * Adds the element/relationship styles from the given theme. + * + * @param theme a Theme object + */ + public void addStylesFromTheme(Theme theme) { + if (theme != null) { + themes.add(theme); + } } -} +} \ No newline at end of file From 1e0f098494d6e1e4b060aa33cba3f621e41b1c2e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 22 Jul 2022 12:13:46 +0100 Subject: [PATCH 002/418] Fixes tests. --- .../test/unit/com/structurizr/view/ThemeUtilsTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java index 119d10390..e1621d658 100644 --- a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java @@ -77,13 +77,13 @@ public void test_findElementStyle_WithThemes() { Collection elementStyles = new ArrayList<>(); Collection relationshipStyles = new ArrayList<>(); elementStyles.add(new ElementStyle("Element").shape(Shape.Box).background("#000000").color("#ffffff")); - workspace.getViews().getConfiguration().getStyles().addStylesFromTheme("url1", elementStyles, relationshipStyles); + workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(new Theme(elementStyles, relationshipStyles)); // theme 2 elementStyles = new ArrayList<>(); relationshipStyles = new ArrayList<>(); elementStyles.add(new ElementStyle("Element").background("#ff0000")); - workspace.getViews().getConfiguration().getStyles().addStylesFromTheme("url2", elementStyles, relationshipStyles); + workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(new Theme(elementStyles, relationshipStyles)); ElementStyle style = workspace.getViews().getConfiguration().getStyles().findElementStyle(softwareSystem); assertEquals(Integer.valueOf(450), style.getWidth()); @@ -111,13 +111,13 @@ public void test_findRelationshipStyle_WithThemes() { Collection elementStyles = new ArrayList<>(); Collection relationshipStyles = new ArrayList<>(); relationshipStyles.add(new RelationshipStyle("Relationship").color("#ff0000").thickness(4)); - workspace.getViews().getConfiguration().getStyles().addStylesFromTheme("url1", elementStyles, relationshipStyles); + workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(new Theme(elementStyles, relationshipStyles)); // theme 2 elementStyles = new ArrayList<>(); relationshipStyles = new ArrayList<>(); relationshipStyles.add(new RelationshipStyle("Relationship").color("#0000ff")); - workspace.getViews().getConfiguration().getStyles().addStylesFromTheme("url2", elementStyles, relationshipStyles); + workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(new Theme(elementStyles, relationshipStyles)); RelationshipStyle style = workspace.getViews().getConfiguration().getStyles().findRelationshipStyle(relationship); assertEquals(Integer.valueOf(4), style.getThickness()); // from theme 1 From 11416d1db4e0e79b46da79ec84aeb3a1bb550e3e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Jul 2022 17:28:00 +0100 Subject: [PATCH 003/418] Adds the PropertyHolder interface, to make it easier to set properties via the DSL. --- structurizr-core/src/com/structurizr/AbstractWorkspace.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/com/structurizr/AbstractWorkspace.java index 0375ba917..c78f9c0fd 100644 --- a/structurizr-core/src/com/structurizr/AbstractWorkspace.java +++ b/structurizr-core/src/com/structurizr/AbstractWorkspace.java @@ -10,7 +10,7 @@ /** * The superclass for regular and encrypted workspaces. */ -public abstract class AbstractWorkspace { +public abstract class AbstractWorkspace implements PropertyHolder { private long id; private String name; From cd3fa5dda81e1a0ffd69237cc114dc3c9a6ac2c1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 30 Jul 2022 13:34:15 +0100 Subject: [PATCH 004/418] Provides a way to add specific relationships to dynamic views. --- build.gradle | 2 +- docs/changelog.md | 3 +- .../src/com/structurizr/view/DynamicView.java | 31 +++++++++++ .../structurizr/view/DynamicViewTests.java | 54 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index eb6c17aec..5200d34b9 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.13.1' + version = '1.14.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index d1566a0f2..10c2116f9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,9 @@ # Changelog -## 1.13.1 (unreleased to Maven Central) +## 1.14.0 (unreleased to Maven Central) - Adds a helper method (`AbstractImpliedRelationshipsStrategy.createImpliedRelationship`) to create implied relationships, which can then be used by custom implementations. +- Provides a way to add specific relationships to dynamic views. ## 1.13.0 (25th June 2022) diff --git a/structurizr-core/src/com/structurizr/view/DynamicView.java b/structurizr-core/src/com/structurizr/view/DynamicView.java index 9a29dd37b..f2b566454 100644 --- a/structurizr-core/src/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/com/structurizr/view/DynamicView.java @@ -165,6 +165,37 @@ public RelationshipView add(@Nonnull StaticStructureElement source, String descr } } + /** + * Adds a specific relationship to this dynamic view, with the original description. + * + * @param relationship the Relationship to add + * @return a RelationshipView + */ + public RelationshipView add(Relationship relationship) { + return add(relationship, ""); + } + + /** + * Adds a specific relationship to this dynamic view, with an overidden description. + * + * @param relationship the Relationship to add + * @param description the overidden description + * @return a RelationshipView + */ + public RelationshipView add(Relationship relationship, String description) { + if (relationship == null) { + throw new IllegalArgumentException("A relationship must be specified."); + } + + checkElementCanBeAdded(relationship.getSource()); + checkElementCanBeAdded(relationship.getDestination()); + + addElement(relationship.getSource(), false); + addElement(relationship.getDestination(), false); + + return addRelationship(relationship, description, sequenceNumber.getNext(), false); + } + protected RelationshipView addRelationship(Relationship relationship, String description, String order, boolean response) { RelationshipView relationshipView = addRelationship(relationship); if (relationshipView != null) { diff --git a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java index f26997d7d..065c0befc 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java @@ -208,6 +208,40 @@ public void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDesti } } + @Test + public void test_addRelationshipWithOriginalDescription() { + workspace = new Workspace("Name", "Description"); + model = workspace.getModel(); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(relationship); + + assertEquals(2, view.getElements().size()); + assertSame(relationship, view.getRelationships().iterator().next().getRelationship()); + assertEquals("", view.getRelationships().iterator().next().getDescription()); + } + + @Test + public void test_addRelationshipWithOveriddenDescription() { + workspace = new Workspace("Name", "Description"); + model = workspace.getModel(); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(relationship, "New description"); + + assertEquals(2, view.getElements().size()); + assertSame(relationship, view.getRelationships().iterator().next().getRelationship()); + assertEquals("New description", view.getRelationships().iterator().next().getDescription()); + } + @Test public void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExists() { final DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); @@ -245,6 +279,26 @@ public void test_normalSequence() { assertSame(container3, view.getRelationships().stream().filter(r -> r.getOrder().equals("2")).findFirst().get().getRelationship().getDestination()); } + @Test + public void test_normalSequence_WhenThereAreMultipleDescriptions() { + workspace = new Workspace("Name", "Description"); + model = workspace.getModel(); + + SoftwareSystem ss1 = workspace.getModel().addSoftwareSystem("Software System 1", ""); + SoftwareSystem ss2 = workspace.getModel().addSoftwareSystem("Software System 2", ""); + + Relationship r1 = ss1.uses(ss2, "Uses 1"); + Relationship r2 = ss1.uses(ss2, "Uses 2"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + + RelationshipView rv1 = view.add(ss1, "Uses 1", ss2); + RelationshipView rv2 = view.add(ss1, "Uses 2", ss2); + + assertSame(r1, rv1.getRelationship()); + assertSame(r2, rv2.getRelationship()); + } + @Test public void test_normalSequence_WhenThereAreMultipleTechnologies() { workspace = new Workspace("Name", "Description"); From 3f2ed13b7fda1895ec57bea0a3fe191c9ee85a3e Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 1 Aug 2022 20:25:12 +0200 Subject: [PATCH 005/418] JUnit 5 --- structurizr-client/build.gradle | 6 +- .../api/BackwardsCompatibilityTests.java | 6 +- .../StructurizrClientIntegrationTests.java | 24 +-- .../api/WorkspaceRulesValidationTests.java | 40 ++--- .../com/structurizr/api/ApiResponseTests.java | 6 +- ...shBasedMessageAuthenticationCodeTests.java | 6 +- .../api/HmacAuthorizationHeaderTests.java | 8 +- .../com/structurizr/api/HmacContentTests.java | 8 +- .../com/structurizr/api/Md5DigestTests.java | 8 +- .../api/StructurizrClientTests.java | 38 ++--- .../AesEncryptionStrategyTests.java | 69 ++++---- .../encryption/EncryptedWorkspaceTests.java | 18 +- .../io/json/EncryptedJsonTests.java | 6 +- .../io/json/EncryptedJsonWriterTests.java | 10 +- .../com/structurizr/io/json/JsonTests.java | 12 +- .../structurizr/io/json/JsonWriterTests.java | 10 +- .../structurizr/util/WorkspaceUtilsTests.java | 27 ++- .../com/structurizr/view/ThemeUtilsTests.java | 17 +- structurizr-core/build.gradle | 6 +- .../unit/com/structurizr/WorkspaceTests.java | 16 +- .../WorkspaceConfigurationTests.java | 14 +- .../documentation/DecisionTests.java | 8 +- .../documentation/DocumentationTests.java | 30 ++-- .../documentation/SectionTests.java | 6 +- .../structurizr/model/CodeElementTests.java | 69 ++++---- .../com/structurizr/model/ComponentTests.java | 37 ++--- .../model/ContainerInstanceTests.java | 42 ++--- .../com/structurizr/model/ContainerTests.java | 54 +++--- ...essAnyRelationshipExistsStrategyTests.java | 10 +- ...ssSameRelationshipExistsStrategyTests.java | 8 +- .../structurizr/model/CustomElementTests.java | 26 +-- ...aultImpliedRelationshipsStrategyTests.java | 8 +- .../model/DeploymentNodeTests.java | 48 +++--- .../com/structurizr/model/ElementTests.java | 90 +++++----- .../model/GroupableElementTests.java | 14 +- .../model/HttpHealthCheckTests.java | 20 +-- .../model/InfrastructureNodeTests.java | 12 +- .../com/structurizr/model/ModelItemTests.java | 79 +++++---- .../com/structurizr/model/ModelTests.java | 144 ++++++++-------- .../com/structurizr/model/PersonTests.java | 20 +-- .../structurizr/model/RelationshipTests.java | 44 ++--- .../model/SoftwareSystemInstanceTests.java | 42 ++--- .../model/SoftwareSystemTests.java | 62 +++---- .../com/structurizr/util/ImageUtilsTests.java | 38 ++--- .../structurizr/util/StringUtilsTests.java | 12 +- .../unit/com/structurizr/util/UrlTests.java | 14 +- .../view/AutomaticLayoutTests.java | 14 +- .../com/structurizr/view/BrandingTests.java | 27 +-- .../com/structurizr/view/ColorPairTests.java | 24 +-- .../unit/com/structurizr/view/ColorTests.java | 14 +- .../structurizr/view/ComponentViewTests.java | 110 ++++++------ .../structurizr/view/ConfigurationTests.java | 31 ++-- .../structurizr/view/ContainerViewTests.java | 50 +++--- .../view/DefaultLayoutMergeStrategyTests.java | 18 +- .../structurizr/view/DeploymentViewTests.java | 82 ++++----- .../com/structurizr/view/DimensionsTests.java | 11 +- .../structurizr/view/DynamicViewTests.java | 58 +++---- .../structurizr/view/ElementStyleTests.java | 116 +++++++------ .../structurizr/view/ElementViewTests.java | 8 +- .../structurizr/view/FilteredViewTests.java | 6 +- .../unit/com/structurizr/view/FontTests.java | 27 +-- .../com/structurizr/view/PaperSizeTests.java | 10 +- .../view/RelationshipStyleTests.java | 56 ++++--- .../view/SequenceCounterTests.java | 6 +- .../structurizr/view/SequenceNumberTests.java | 8 +- .../com/structurizr/view/StaticViewTests.java | 12 +- .../com/structurizr/view/StylesTests.java | 46 +++--- .../view/SystemContextViewTests.java | 56 +++---- .../view/SystemLandscapeViewTests.java | 54 +++--- .../structurizr/view/TerminologyTests.java | 6 +- .../com/structurizr/view/VertexTests.java | 20 +-- .../com/structurizr/view/ViewSetTests.java | 156 +++++++++--------- .../unit/com/structurizr/view/ViewTests.java | 86 +++++----- 73 files changed, 1234 insertions(+), 1170 deletions(-) diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 6c5bd755d..2c4936daf 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -10,5 +10,9 @@ dependencies { implementation 'commons-logging:commons-logging:1.2' - testImplementation 'junit:junit:4.12' + testImplementation(platform('org.junit:junit-bom:5.9.0')) + testImplementation('org.junit.jupiter:junit-jupiter') +} +test { + useJUnitPlatform() } \ No newline at end of file diff --git a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java index b5c9959da..e4177ab26 100644 --- a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java +++ b/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java @@ -1,16 +1,16 @@ package com.structurizr.api; import com.structurizr.util.WorkspaceUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.File; -public class BackwardsCompatibilityTests { +class BackwardsCompatibilityTests { private static final File PATH_TO_WORKSPACE_FILES = new File("test/integration/backwardsCompatibility"); @Test - public void test() throws Exception { + void test() throws Exception { for (File file : PATH_TO_WORKSPACE_FILES.listFiles(f -> f.getName().endsWith(".json"))) { WorkspaceUtils.loadWorkspaceFromJson(file); } diff --git a/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java b/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java index d0c16ef78..3c0fd2ba7 100644 --- a/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java +++ b/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java @@ -8,22 +8,22 @@ import com.structurizr.model.Person; import com.structurizr.model.SoftwareSystem; import com.structurizr.view.SystemContextView; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.File; import java.io.FileReader; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class StructurizrClientIntegrationTests { private StructurizrClient structurizrClient; private File workspaceArchiveLocation = new File(System.getProperty("java.io.tmpdir"), "structurizr"); - @Before - public void setUp() { + @BeforeEach + void setUp() { structurizrClient = new StructurizrClient("81ace434-94a1-486f-a786-37bbeaa44e08", "a8673e21-7b6f-4f52-be65-adb7248be86b"); structurizrClient.setWorkspaceArchiveLocation(workspaceArchiveLocation); workspaceArchiveLocation.mkdirs(); @@ -32,8 +32,8 @@ public void setUp() { structurizrClient.setMergeFromRemote(false); } - @After - public void tearDown() { + @AfterEach + void tearDown() { clearWorkspaceArchive(); workspaceArchiveLocation.delete(); } @@ -51,7 +51,7 @@ private File getArchivedWorkspace() { } @Test - public void test_putAndGetWorkspace_WithoutEncryption() throws Exception { + void test_putAndGetWorkspace_WithoutEncryption() throws Exception { Workspace workspace = new Workspace("Structurizr client library tests - without encryption", "A test workspace for the Structurizr client library"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); Person person = workspace.getModel().addPerson("Person", "Description"); @@ -77,7 +77,7 @@ public void test_putAndGetWorkspace_WithoutEncryption() throws Exception { } @Test - public void test_putAndGetWorkspace_WithEncryption() throws Exception { + void test_putAndGetWorkspace_WithEncryption() throws Exception { structurizrClient.setEncryptionStrategy(new AesEncryptionStrategy("password")); Workspace workspace = new Workspace("Structurizr client library tests - with encryption", "A test workspace for the Structurizr client library"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -104,14 +104,14 @@ public void test_putAndGetWorkspace_WithEncryption() throws Exception { } @Test - public void test_lockWorkspace() throws Exception { + void test_lockWorkspace() throws Exception { structurizrClient.unlockWorkspace(20081); assertTrue(structurizrClient.lockWorkspace(20081)); } @Test - public void test_unlockWorkspace() throws Exception { + void test_unlockWorkspace() throws Exception { structurizrClient.lockWorkspace(20081); assertTrue(structurizrClient.unlockWorkspace(20081)); } diff --git a/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java b/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java index 88f8b9c08..4319f7128 100644 --- a/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java +++ b/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java @@ -2,18 +2,18 @@ import com.structurizr.WorkspaceValidationException; import com.structurizr.util.WorkspaceUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.File; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class WorkspaceRulesValidationTests { private static final File PATH_TO_WORKSPACE_FILES = new File("test/integration/workspaceValidation"); @Test - public void test_exceptionThrown_WhenElementIdsAreNotUnique() throws Exception { + void test_exceptionThrown_WhenElementIdsAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementIdsAreNotUnique.json")); fail(); @@ -24,7 +24,7 @@ public void test_exceptionThrown_WhenElementIdsAreNotUnique() throws Exception { } @Test - public void test_exceptionThrown_WhenRelationshipIdsAreNotUnique() throws Exception { + void test_exceptionThrown_WhenRelationshipIdsAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipIdsAreNotUnique.json")); fail(); @@ -35,7 +35,7 @@ public void test_exceptionThrown_WhenRelationshipIdsAreNotUnique() throws Except } @Test - public void test_exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { + void test_exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ViewKeysAreNotUnique.json")); fail(); @@ -45,7 +45,7 @@ public void test_exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { } @Test - public void test_exceptionThrown_WhenPeopleAndSoftwareSystemNamesAreNotUnique() throws Exception { + void test_exceptionThrown_WhenPeopleAndSoftwareSystemNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "PeopleAndSoftwareSystemNamesAreNotUnique.json")); fail(); @@ -55,7 +55,7 @@ public void test_exceptionThrown_WhenPeopleAndSoftwareSystemNamesAreNotUnique() } @Test - public void test_exceptionThrown_WhenContainerNamesAreNotUnique() throws Exception { + void test_exceptionThrown_WhenContainerNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ContainerNamesAreNotUnique.json")); fail(); @@ -65,7 +65,7 @@ public void test_exceptionThrown_WhenContainerNamesAreNotUnique() throws Excepti } @Test - public void test_exceptionThrown_WhenComponentNamesAreNotUnique() throws Exception { + void test_exceptionThrown_WhenComponentNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ComponentNamesAreNotUnique.json")); fail(); @@ -75,7 +75,7 @@ public void test_exceptionThrown_WhenComponentNamesAreNotUnique() throws Excepti } @Test - public void test_exceptionThrown_WhenTopLevelDeploymentNodeNamesAreNotUnique() throws Exception { + void test_exceptionThrown_WhenTopLevelDeploymentNodeNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "TopLevelDeploymentNodeNamesAreNotUnique.json")); fail(); @@ -85,12 +85,12 @@ public void test_exceptionThrown_WhenTopLevelDeploymentNodeNamesAreNotUnique() t } @Test - public void test_exceptionNotThrown_WhenTopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments() throws Exception { + void test_exceptionNotThrown_WhenTopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments() throws Exception { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json")); } @Test - public void test_exceptionThrown_WhenChildDeploymentNodeNamesAreNotUnique() throws Exception { + void test_exceptionThrown_WhenChildDeploymentNodeNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ChildDeploymentNodeNamesAreNotUnique.json")); fail(); @@ -100,7 +100,7 @@ public void test_exceptionThrown_WhenChildDeploymentNodeNamesAreNotUnique() thro } @Test - public void test_exceptionThrown_WhenRelationshipDescriptionsAreNotUnique() throws Exception { + void test_exceptionThrown_WhenRelationshipDescriptionsAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipDescriptionsAreNotUnique.json")); fail(); @@ -110,7 +110,7 @@ public void test_exceptionThrown_WhenRelationshipDescriptionsAreNotUnique() thro } @Test - public void test_exceptionThrown_WhenSoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel() throws Exception { + void test_exceptionThrown_WhenSoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json")); fail(); @@ -120,7 +120,7 @@ public void test_exceptionThrown_WhenSoftwareSystemAssociatedWithSystemContextVi } @Test - public void test_exceptionThrown_WhenSoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel() throws Exception { + void test_exceptionThrown_WhenSoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json")); fail(); @@ -130,7 +130,7 @@ public void test_exceptionThrown_WhenSoftwareSystemAssociatedWithContainerViewIs } @Test - public void test_exceptionThrown_WhenContainerAssociatedWithComponentViewIsMissingFromTheModel() throws Exception { + void test_exceptionThrown_WhenContainerAssociatedWithComponentViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ContainerAssociatedWithComponentViewIsMissingFromTheModel.json")); fail(); @@ -140,7 +140,7 @@ public void test_exceptionThrown_WhenContainerAssociatedWithComponentViewIsMissi } @Test - public void test_exceptionThrown_WhenElementAssociatedWithDynamicViewIsMissingFromTheModel() throws Exception { + void test_exceptionThrown_WhenElementAssociatedWithDynamicViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementAssociatedWithDynamicViewIsMissingFromTheModel.json")); fail(); @@ -150,7 +150,7 @@ public void test_exceptionThrown_WhenElementAssociatedWithDynamicViewIsMissingFr } @Test - public void test_exceptionThrown_WhenSoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel() throws Exception { + void test_exceptionThrown_WhenSoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json")); fail(); @@ -160,7 +160,7 @@ public void test_exceptionThrown_WhenSoftwareSystemAssociatedWithDeploymentViewI } @Test - public void test_exceptionThrown_WhenViewAssociatedWithFilteredViewIsMissingFromTheWorkspace() throws Exception { + void test_exceptionThrown_WhenViewAssociatedWithFilteredViewIsMissingFromTheWorkspace() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json")); fail(); @@ -170,7 +170,7 @@ public void test_exceptionThrown_WhenViewAssociatedWithFilteredViewIsMissingFrom } @Test - public void test_exceptionThrown_WhenElementReferencedByViewIsMissingFromTheModel() throws Exception { + void test_exceptionThrown_WhenElementReferencedByViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementReferencedByViewIsMissingFromTheModel.json")); fail(); @@ -180,7 +180,7 @@ public void test_exceptionThrown_WhenElementReferencedByViewIsMissingFromTheMode } @Test - public void test_exceptionThrown_WhenRelationshipReferencedByViewIsMissingFromTheModel() throws Exception { + void test_exceptionThrown_WhenRelationshipReferencedByViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipReferencedByViewIsMissingFromTheModel.json")); fail(); diff --git a/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java b/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java index b0f088049..0c416cc28 100644 --- a/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java @@ -1,13 +1,13 @@ package com.structurizr.api; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ApiResponseTests { @Test - public void test_parse_createsAnApiErrorObjectWithTheSpecifiedErrorMessage() throws Exception { + void test_parse_createsAnApiErrorObjectWithTheSpecifiedErrorMessage() throws Exception { ApiResponse apiResponse = ApiResponse.parse("{\"message\": \"Hello\"}"); assertEquals("Hello", apiResponse.getMessage()); } diff --git a/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java b/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java index 4ac054a91..08a930ccd 100644 --- a/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java @@ -1,15 +1,15 @@ package com.structurizr.api; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class HashBasedMessageAuthenticationCodeTests { private HashBasedMessageAuthenticationCode code; @Test - public void test_generate() throws Exception { + void test_generate() throws Exception { // this example is taken from http://en.wikipedia.org/wiki/Hash-based_message_authentication_code code = new HashBasedMessageAuthenticationCode("key"); assertEquals("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", code.generate("The quick brown fox jumps over the lazy dog")); diff --git a/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java b/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java index abf23d95c..df5ff9166 100644 --- a/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java @@ -1,21 +1,21 @@ package com.structurizr.api; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class HmacAuthorizationHeaderTests { private HmacAuthorizationHeader header; @Test - public void test_format() { + void test_format() { header = new HmacAuthorizationHeader("apiKey", "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"); assertEquals("apiKey:ZjdiYzgzZjQzMDUzODQyNGIxMzI5OGU2YWE2ZmIxNDNlZjRkNTlhMTQ5NDYxNzU5OTc0NzlkYmMyZDFhM2NkOA==", header.format()); } @Test - public void test_parse() { + void test_parse() { header = HmacAuthorizationHeader.parse("apiKey:ZjdiYzgzZjQzMDUzODQyNGIxMzI5OGU2YWE2ZmIxNDNlZjRkNTlhMTQ5NDYxNzU5OTc0NzlkYmMyZDFhM2NkOA=="); assertEquals("apiKey", header.getApiKey()); assertEquals("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", header.getHmac()); diff --git a/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java b/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java index 7397d0ed0..3ee024bdd 100644 --- a/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java @@ -1,19 +1,19 @@ package com.structurizr.api; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class HmacContentTests { @Test - public void test_toString_WhenThereAreNoStrings() { + void test_toString_WhenThereAreNoStrings() { assertEquals("", new HmacContent().toString()); } @Test - public void test_toString_WhenThereAreSomeStrings() { + void test_toString_WhenThereAreSomeStrings() { assertEquals("String1\nString2\nString3\n", new HmacContent("String1", "String2", "String3").toString()); } diff --git a/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java b/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java index 6cf3d737f..c880e38b3 100644 --- a/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java @@ -1,20 +1,20 @@ package com.structurizr.api; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class Md5DigestTests { private Md5Digest md5 = new Md5Digest(); @Test - public void test_generate_TreatsNullAsEmptyContent() throws Exception { + void test_generate_TreatsNullAsEmptyContent() throws Exception { assertEquals(md5.generate(null), md5.generate("")); } @Test - public void test_generate() throws Exception { + void test_generate() throws Exception { assertEquals("ed076287532e86365e841e92bfc50d8c", md5.generate("Hello World!")); assertEquals("d41d8cd98f00b204e9800998ecf8427e", md5.generate("")); } diff --git a/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java b/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java index d0a6caab3..c7d4ecd2a 100644 --- a/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java @@ -1,16 +1,16 @@ package com.structurizr.api; import com.structurizr.Workspace; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class StructurizrClientTests { private StructurizrClient structurizrClient; @Test - public void test_construction_WithTwoParameters() { + void test_construction_WithTwoParameters() { structurizrClient = new StructurizrClient("key", "secret"); assertEquals("https://api.structurizr.com", structurizrClient.getUrl()); assertEquals("key", structurizrClient.getApiKey()); @@ -18,7 +18,7 @@ public void test_construction_WithTwoParameters() { } @Test - public void test_construction_WithThreeParameters() { + void test_construction_WithThreeParameters() { structurizrClient = new StructurizrClient("https://localhost", "key", "secret"); assertEquals("https://localhost", structurizrClient.getUrl()); assertEquals("key", structurizrClient.getApiKey()); @@ -26,7 +26,7 @@ public void test_construction_WithThreeParameters() { } @Test - public void test_construction_WithThreeParameters_TruncatesTheApiUrl_WhenTheApiUrlHasATrailingSlashCharacter() { + void test_construction_WithThreeParameters_TruncatesTheApiUrl_WhenTheApiUrlHasATrailingSlashCharacter() { structurizrClient = new StructurizrClient("https://localhost/", "key", "secret"); assertEquals("https://localhost", structurizrClient.getUrl()); assertEquals("key", structurizrClient.getApiKey()); @@ -34,7 +34,7 @@ public void test_construction_WithThreeParameters_TruncatesTheApiUrl_WhenTheApiU } @Test - public void test_construction_ThrowsAnException_WhenANullApiKeyIsUsed() { + void test_construction_ThrowsAnException_WhenANullApiKeyIsUsed() { try { structurizrClient = new StructurizrClient(null, "secret"); fail(); @@ -44,7 +44,7 @@ public void test_construction_ThrowsAnException_WhenANullApiKeyIsUsed() { } @Test - public void test_construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { + void test_construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { try { structurizrClient = new StructurizrClient(" ", "secret"); fail(); @@ -54,7 +54,7 @@ public void test_construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { } @Test - public void test_construction_ThrowsAnException_WhenANullApiSecretIsUsed() { + void test_construction_ThrowsAnException_WhenANullApiSecretIsUsed() { try { structurizrClient = new StructurizrClient("key", null); fail(); @@ -64,7 +64,7 @@ public void test_construction_ThrowsAnException_WhenANullApiSecretIsUsed() { } @Test - public void test_construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { + void test_construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { try { structurizrClient = new StructurizrClient("key", " "); fail(); @@ -74,7 +74,7 @@ public void test_construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { } @Test - public void test_construction_ThrowsAnException_WhenANullApiUrlIsUsed() { + void test_construction_ThrowsAnException_WhenANullApiUrlIsUsed() { try { structurizrClient = new StructurizrClient(null, "key", "secret"); fail(); @@ -84,7 +84,7 @@ public void test_construction_ThrowsAnException_WhenANullApiUrlIsUsed() { } @Test - public void test_construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { + void test_construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { try { structurizrClient = new StructurizrClient(" ", "key", "secret"); fail(); @@ -94,7 +94,7 @@ public void test_construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { } @Test - public void test_getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { + void test_getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { try { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.getWorkspace(0); @@ -105,7 +105,7 @@ public void test_getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() t } @Test - public void test_putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { + void test_putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { try { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.putWorkspace(0, new Workspace("Name", "Description")); @@ -116,7 +116,7 @@ public void test_putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() t } @Test - public void test_putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws Exception { + void test_putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws Exception { try { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.putWorkspace(1234, null); @@ -127,7 +127,7 @@ public void test_putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() } @Test - public void test_constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropertiesAreFound() { + void test_constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropertiesAreFound() { try { structurizrClient = new StructurizrClient(); fail(); @@ -137,20 +137,20 @@ public void test_constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropert } @Test - public void test_getAgent() { + void test_getAgent() { structurizrClient = new StructurizrClient("key", "secret"); assertTrue(structurizrClient.getAgent().startsWith("structurizr-java/")); } @Test - public void test_setAgent() { + void test_setAgent() { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.setAgent("new_agent"); assertEquals("new_agent", structurizrClient.getAgent()); } @Test - public void test_setAgent_ThrowsAnException_WhenPassedNull() { + void test_setAgent_ThrowsAnException_WhenPassedNull() { structurizrClient = new StructurizrClient("key", "secret"); try { @@ -162,7 +162,7 @@ public void test_setAgent_ThrowsAnException_WhenPassedNull() { } @Test - public void test_setAgent_ThrowsAnException_WhenPassedAnEmptyString() { + void test_setAgent_ThrowsAnException_WhenPassedAnEmptyString() { structurizrClient = new StructurizrClient("key", "secret"); try { diff --git a/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java b/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java index 8c8f4a8ec..8b390f8f6 100644 --- a/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java +++ b/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java @@ -1,17 +1,16 @@ package com.structurizr.encryption; -import org.junit.Test; +import org.junit.jupiter.api.Test; import javax.crypto.BadPaddingException; import java.security.InvalidKeyException; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; public class AesEncryptionStrategyTests { @Test - public void test_encrypt_EncryptsPlaintext() throws Exception { + void test_encrypt_EncryptsPlaintext() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "06DC30A48ADEEE72D98E33C2CEAEAD3E", "ED124530AF64A5CAD8EF463CF5628434", "password"); String ciphertext = strategy.encrypt("Hello world"); @@ -19,7 +18,7 @@ public void test_encrypt_EncryptsPlaintext() throws Exception { } @Test - public void test_decrypt_decryptsTheCiphertext_WhenTheSameStrategyInstanceIsUsed() throws Exception { + void test_decrypt_decryptsTheCiphertext_WhenTheSameStrategyInstanceIsUsed() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); String ciphertext = strategy.encrypt("Hello world"); @@ -27,7 +26,7 @@ public void test_decrypt_decryptsTheCiphertext_WhenTheSameStrategyInstanceIsUsed } @Test - public void test_decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() throws Exception { + void test_decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); String ciphertext = strategy.encrypt("Hello world"); @@ -37,7 +36,7 @@ public void test_decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() } @Test - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectKeySizeIsUsed() throws Exception { + void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectKeySizeIsUsed() throws Exception { try { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); @@ -53,44 +52,52 @@ public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectKeySizeIsUs } } - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUsed() throws Exception { - AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); + @Test + void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUsed() throws Exception { + assertThrows(BadPaddingException.class, () -> { + AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); + String ciphertext = strategy.encrypt("Hello world"); - strategy = new AesEncryptionStrategy(strategy.getKeySize(), 2000, strategy.getSalt(), strategy.getIv(), "password"); - strategy.decrypt(ciphertext); + strategy = new AesEncryptionStrategy(strategy.getKeySize(), 2000, strategy.getSalt(), strategy.getIv(), "password"); + strategy.decrypt(ciphertext); + }); } - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throws Exception { - AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); + @Test + void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throws Exception { + assertThrows(BadPaddingException.class, () -> { + AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); + String ciphertext = strategy.encrypt("Hello world"); - strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), "133D30C2A658B3081279A97FD3B1F7CDE10C4FB61D39EEA8", strategy.getIv(), "password"); - strategy.decrypt(ciphertext); + strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), "133D30C2A658B3081279A97FD3B1F7CDE10C4FB61D39EEA8", strategy.getIv(), "password"); + strategy.decrypt(ciphertext); + }); } - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws Exception { - AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); + @Test + void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws Exception { + assertThrows(BadPaddingException.class, () -> { + AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); + String ciphertext = strategy.encrypt("Hello world"); - strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), strategy.getSalt(), "1DED89E4FB15F61DC6433E3BADA4A891", "password"); - strategy.decrypt(ciphertext); + strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), strategy.getSalt(), "1DED89E4FB15F61DC6433E3BADA4A891", "password"); + strategy.decrypt(ciphertext); + }); } - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectPassphraseIsUsed() throws Exception { - AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); + @Test + void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectPassphraseIsUsed() throws Exception { + assertThrows(BadPaddingException.class, () -> { + AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); + String ciphertext = strategy.encrypt("Hello world"); - strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), strategy.getSalt(), strategy.getIv(), "The Wrong Password"); - strategy.decrypt(ciphertext); + strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), strategy.getSalt(), strategy.getIv(), "The Wrong Password"); + strategy.decrypt(ciphertext); + }); } } diff --git a/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java b/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java index 1d88a6ff1..81a87b6a0 100644 --- a/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java +++ b/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java @@ -3,14 +3,12 @@ import com.structurizr.Workspace; import com.structurizr.configuration.Role; import com.structurizr.io.json.JsonWriter; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.StringWriter; -import static junit.framework.TestCase.assertSame; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class EncryptedWorkspaceTests { @@ -18,7 +16,7 @@ public class EncryptedWorkspaceTests { private Workspace workspace; private EncryptionStrategy encryptionStrategy; - @Before + @BeforeEach public void setUp() throws Exception { workspace = new Workspace("Name", "Description"); workspace.setVersion("1.2.3"); @@ -31,7 +29,7 @@ public void setUp() throws Exception { } @Test - public void test_construction_WhenTwoParametersAreSpecified() throws Exception { + void test_construction_WhenTwoParametersAreSpecified() throws Exception { encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); assertEquals("Name", encryptedWorkspace.getName()); @@ -55,7 +53,7 @@ public void test_construction_WhenTwoParametersAreSpecified() throws Exception { } @Test - public void test_construction_WhenThreeParametersAreSpecified() throws Exception { + void test_construction_WhenThreeParametersAreSpecified() throws Exception { JsonWriter jsonWriter = new JsonWriter(false); StringWriter stringWriter = new StringWriter(); jsonWriter.write(workspace, stringWriter); @@ -77,7 +75,7 @@ public void test_construction_WhenThreeParametersAreSpecified() throws Exception } @Test - public void test_getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws Exception { + void test_getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws Exception { encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); String cipherText = encryptedWorkspace.getCiphertext(); @@ -89,7 +87,7 @@ public void test_getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws } @Test - public void test_getWorkspace_ReturnsTheWorkspace_WhenACipherextIsSpecified() throws Exception { + void test_getWorkspace_ReturnsTheWorkspace_WhenACipherextIsSpecified() throws Exception { JsonWriter jsonWriter = new JsonWriter(false); StringWriter stringWriter = new StringWriter(); jsonWriter.write(workspace, stringWriter); diff --git a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java index 4b38c5638..14ed4f11e 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java @@ -3,17 +3,17 @@ import com.structurizr.Workspace; import com.structurizr.encryption.AesEncryptionStrategy; import com.structurizr.encryption.EncryptedWorkspace; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.StringReader; import java.io.StringWriter; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class EncryptedJsonTests { @Test - public void test_write_and_read() throws Exception { + void test_write_and_read() throws Exception { final Workspace workspace1 = new Workspace("Name", "Description"); // output the model as JSON diff --git a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java index 9a9efae49..eccb79d45 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java @@ -3,17 +3,17 @@ import com.structurizr.Workspace; import com.structurizr.encryption.AesEncryptionStrategy; import com.structurizr.encryption.EncryptedWorkspace; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.StringWriter; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class EncryptedJsonWriterTests { @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorkspaceIsSpecified() throws Exception { + void test_write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorkspaceIsSpecified() throws Exception { try { EncryptedJsonWriter writer = new EncryptedJsonWriter(true); writer.write(null, new StringWriter()); @@ -24,7 +24,7 @@ public void test_write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorksp } @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { + void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { try { EncryptedJsonWriter writer = new EncryptedJsonWriter(true); Workspace workspace = new Workspace("Name", "Description"); diff --git a/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java b/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java index baab4d7fc..2c436c2a1 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java @@ -2,19 +2,19 @@ import com.structurizr.Workspace; import com.structurizr.model.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.StringReader; import java.io.StringWriter; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class JsonTests { @Test - public void test_write_and_read() throws Exception { + void test_write_and_read() throws Exception { final Workspace workspace1 = new Workspace("Name", "Description"); // output the model as JSON @@ -31,7 +31,7 @@ public void test_write_and_read() throws Exception { } @Test - public void test_backwardsCompatibilityOfRenamingEnterpriseContextViewsToSystemLandscapeViews() throws Exception { + void test_backwardsCompatibilityOfRenamingEnterpriseContextViewsToSystemLandscapeViews() throws Exception { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key", "description"); @@ -48,7 +48,7 @@ public void test_backwardsCompatibilityOfRenamingEnterpriseContextViewsToSystemL } @Test - public void test_write_and_read_withCustomIdGenerator() throws Exception { + void test_write_and_read_withCustomIdGenerator() throws Exception { Workspace workspace1 = new Workspace("Name", "Description"); workspace1.getModel().setIdGenerator(new CustomIdGenerator()); Person user = workspace1.getModel().addPerson("User"); diff --git a/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java b/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java index 48049ebbc..bb7a8301a 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java @@ -1,17 +1,17 @@ package com.structurizr.io.json; import com.structurizr.Workspace; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.StringWriter; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class JsonWriterTests { @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpecified() throws Exception { + void test_write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpecified() throws Exception { try { JsonWriter writer = new JsonWriter(true); writer.write(null, new StringWriter()); @@ -22,7 +22,7 @@ public void test_write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpec } @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { + void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { try { JsonWriter writer = new JsonWriter(true); Workspace workspace = new Workspace("Name", "Description"); diff --git a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java index 0ef0bcac0..0657d4ebf 100644 --- a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java @@ -1,19 +1,18 @@ package com.structurizr.util; import com.structurizr.Workspace; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.File; import java.io.FilenameFilter; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class WorkspaceUtilsTests { @Test - public void test_loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecified() { + void test_loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecified() { try { WorkspaceUtils.loadWorkspaceFromJson(null); fail(); @@ -23,7 +22,7 @@ public void test_loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecifie } @Test - public void test_loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist() { + void test_loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist() { try { WorkspaceUtils.loadWorkspaceFromJson(new File("test/unit/com/structurizr/util/other-workspace.json")); fail(); @@ -33,7 +32,7 @@ public void test_loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist } @Test - public void test_saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpecified() { + void test_saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpecified() { try { WorkspaceUtils.saveWorkspaceToJson(null, null); fail(); @@ -43,7 +42,7 @@ public void test_saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpeci } @Test - public void test_saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified() { + void test_saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified() { try { WorkspaceUtils.saveWorkspaceToJson(new Workspace("Name", "Description"), null); fail(); @@ -53,7 +52,7 @@ public void test_saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified( } @Test - public void test_saveWorkspaceToJson_and_loadWorkspaceFromJson() throws Exception { + void test_saveWorkspaceToJson_and_loadWorkspaceFromJson() throws Exception { File file = new File("build/workspace-utils.json"); Workspace workspace = new Workspace("Name", "Description"); WorkspaceUtils.saveWorkspaceToJson(workspace, file); @@ -63,7 +62,7 @@ public void test_saveWorkspaceToJson_and_loadWorkspaceFromJson() throws Exceptio } @Test - public void test_toJson_ThrowsAnException_WhenANullWorkspaceIsProvided() throws Exception { + void test_toJson_ThrowsAnException_WhenANullWorkspaceIsProvided() throws Exception { try { WorkspaceUtils.toJson(null, true); fail(); @@ -73,7 +72,7 @@ public void test_toJson_ThrowsAnException_WhenANullWorkspaceIsProvided() throws } @Test - public void test_toJson() throws Exception { + void test_toJson() throws Exception { Workspace workspace = new Workspace("Name", "Description"); String indentedOutput = WorkspaceUtils.toJson(workspace, true); String unindentedOutput = WorkspaceUtils.toJson(workspace, false); @@ -97,7 +96,7 @@ public void test_toJson() throws Exception { } @Test - public void test_fromJson_ThrowsAnException_WhenANullJsonStringIsProvided() throws Exception { + void test_fromJson_ThrowsAnException_WhenANullJsonStringIsProvided() throws Exception { try { WorkspaceUtils.fromJson(null); fail(); @@ -107,7 +106,7 @@ public void test_fromJson_ThrowsAnException_WhenANullJsonStringIsProvided() thro } @Test - public void test_fromJson_ThrowsAnException_WhenAnEmptyJsonStringIsProvided() throws Exception { + void test_fromJson_ThrowsAnException_WhenAnEmptyJsonStringIsProvided() throws Exception { try { WorkspaceUtils.fromJson(" "); fail(); @@ -117,7 +116,7 @@ public void test_fromJson_ThrowsAnException_WhenAnEmptyJsonStringIsProvided() th } @Test - public void test_fromJson() throws Exception { + void test_fromJson() throws Exception { Workspace workspace = WorkspaceUtils.fromJson("{\"id\":0,\"name\":\"Name\",\"description\":\"Description\",\"model\":{},\"documentation\":{},\"views\":{\"configuration\":{\"branding\":{},\"styles\":{},\"terminology\":{}}}}"); assertEquals("Name", workspace.getName()); assertEquals("Description", workspace.getDescription()); diff --git a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java index e1621d658..0714d6a7f 100644 --- a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java @@ -4,18 +4,17 @@ import com.structurizr.model.Relationship; import com.structurizr.model.SoftwareSystem; import com.structurizr.model.Tags; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Collection; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ThemeUtilsTests { @Test - public void test_loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { + void test_loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); ThemeUtils.loadThemes(workspace); @@ -24,7 +23,7 @@ public void test_loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception } @Test - public void test_loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { + void test_loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); softwareSystem.addTags("Amazon Web Services - Alexa For Business"); @@ -44,7 +43,7 @@ public void test_loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { } @Test - public void test_toJson() throws Exception { + void test_toJson() throws Exception { Workspace workspace = new Workspace("Name", "Description"); assertEquals("{\n" + " \"name\" : \"Name\",\n" + @@ -68,7 +67,7 @@ public void test_toJson() throws Exception { } @Test - public void test_findElementStyle_WithThemes() { + void test_findElementStyle_WithThemes() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); workspace.getViews().getConfiguration().getStyles().addElementStyle("Element").shape(Shape.RoundedBox); @@ -101,7 +100,7 @@ public void test_findElementStyle_WithThemes() { } @Test - public void test_findRelationshipStyle_WithThemes() { + void test_findRelationshipStyle_WithThemes() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); Relationship relationship = softwareSystem.uses(softwareSystem, "Uses"); @@ -122,7 +121,7 @@ public void test_findRelationshipStyle_WithThemes() { RelationshipStyle style = workspace.getViews().getConfiguration().getStyles().findRelationshipStyle(relationship); assertEquals(Integer.valueOf(4), style.getThickness()); // from theme 1 assertEquals("#0000ff", style.getColor()); // from theme 2 - Assert.assertFalse(style.getDashed()); // from workspace + assertFalse(style.getDashed()); // from workspace assertEquals(Routing.Direct, style.getRouting()); assertEquals(Integer.valueOf(24), style.getFontSize()); assertEquals(Integer.valueOf(200), style.getWidth()); diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 4f395e47f..0fdf5097c 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -5,6 +5,10 @@ dependencies { implementation 'commons-logging:commons-logging:1.2' - testImplementation 'junit:junit:4.12' + testImplementation(platform('org.junit:junit-bom:5.9.0')) + testImplementation('org.junit.jupiter:junit-jupiter') testImplementation 'org.assertj:assertj-core:3.9.1' +} +test { + useJUnitPlatform() } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java index e969b6495..447687bca 100644 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java @@ -5,38 +5,38 @@ import com.structurizr.model.Component; import com.structurizr.model.Container; import com.structurizr.model.SoftwareSystem; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.File; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class WorkspaceTests { private Workspace workspace = new Workspace("Name", "Description"); @Test - public void test_isEmpty_ReturnsTrue_WhenThereAreNoElementsViewsOrDocumentation() { + void test_isEmpty_ReturnsTrue_WhenThereAreNoElementsViewsOrDocumentation() { workspace = new Workspace("Name", "Description"); assertTrue(workspace.isEmpty()); } @Test - public void test_isEmpty_ReturnsFalse_WhenThereAreElements() { + void test_isEmpty_ReturnsFalse_WhenThereAreElements() { workspace = new Workspace("Name", "Description"); workspace.getModel().addPerson("Name", "Description"); assertFalse(workspace.isEmpty()); } @Test - public void test_isEmpty_ReturnsFalse_WhenThereAreViews() { + void test_isEmpty_ReturnsFalse_WhenThereAreViews() { workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key", "Description"); assertFalse(workspace.isEmpty()); } @Test - public void test_isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { + void test_isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { workspace = new Workspace("Name", "Description"); Decision d = new Decision("1"); d.setTitle("Title"); @@ -48,7 +48,7 @@ public void test_isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exceptio } @Test - public void test_countAndLogWarnings() { + void test_countAndLogWarnings() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1", null); SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2", " "); @@ -66,7 +66,7 @@ public void test_countAndLogWarnings() { } @Test - public void test_hydrate_DoesNotCrash() { + void test_hydrate_DoesNotCrash() { Workspace workspace = new Workspace("Name", "Description"); assertNotNull(workspace.getViews()); assertNotNull(workspace.getDocumentation()); diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java index 0658abd18..23c39802e 100644 --- a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -1,14 +1,14 @@ package com.structurizr.configuration; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class WorkspaceConfigurationTests { @Test - public void test_addUser_ThrowsAnException_WhenANullUsernameIsSpecified() { + void test_addUser_ThrowsAnException_WhenANullUsernameIsSpecified() { try { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser(null, Role.ReadWrite); @@ -19,7 +19,7 @@ public void test_addUser_ThrowsAnException_WhenANullUsernameIsSpecified() { } @Test - public void test_addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { + void test_addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { try { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser(" ", Role.ReadWrite); @@ -30,7 +30,7 @@ public void test_addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { } @Test - public void test_addUser_ThrowsAnException_WhenANullRoleIsSpecified() { + void test_addUser_ThrowsAnException_WhenANullRoleIsSpecified() { try { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser("user@domain.com", null); @@ -41,7 +41,7 @@ public void test_addUser_ThrowsAnException_WhenANullRoleIsSpecified() { } @Test - public void test_addUser_AddsAUser() { + void test_addUser_AddsAUser() { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser("user@domain.com", Role.ReadOnly); diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java b/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java index 395d33a6e..b107f7568 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java @@ -1,15 +1,15 @@ package com.structurizr.documentation; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; public class DecisionTests extends AbstractWorkspaceTestBase { @Test - public void test_hasLinkTo() { + void test_hasLinkTo() { Decision d1 = new Decision("1"); Decision d2 = new Decision("2"); Decision d3 = new Decision("3"); diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java index 1f1ee97fe..e290f5bdd 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java @@ -1,22 +1,22 @@ package com.structurizr.documentation; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class DocumentationTests extends AbstractWorkspaceTestBase { private Documentation documentation; - @Before + @BeforeEach public void setUp() { documentation = workspace.getDocumentation(); } @Test - public void test_addSection_ThrowsAnException_WhenTheTitleIsNotSpecified() { + void test_addSection_ThrowsAnException_WhenTheTitleIsNotSpecified() { try { Section section = new Section(); @@ -28,7 +28,7 @@ public void test_addSection_ThrowsAnException_WhenTheTitleIsNotSpecified() { } @Test - public void test_addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { + void test_addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { try { Section section = new Section(); section.setTitle("Title"); @@ -41,7 +41,7 @@ public void test_addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { } @Test - public void test_addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { + void test_addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { try { Section section = new Section(); section.setTitle("Title"); @@ -55,7 +55,7 @@ public void test_addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { } @Test - public void test_addSection_ThrowsAnException_WhenASectionExistsWithTheSameTitle() { + void test_addSection_ThrowsAnException_WhenASectionExistsWithTheSameTitle() { try { Section section = new Section(); section.setTitle("Title"); @@ -71,7 +71,7 @@ public void test_addSection_ThrowsAnException_WhenASectionExistsWithTheSameTitle } @Test - public void test_addSection() { + void test_addSection() { Section section = new Section(); section.setTitle("Title"); section.setContent("Content"); @@ -88,7 +88,7 @@ public void test_addSection() { } @Test - public void test_addSection_IncrementsTheSectionOrderNumber() { + void test_addSection_IncrementsTheSectionOrderNumber() { Section section1 = new Section("Title 1", Format.Markdown, "Content"); Section section2 = new Section("Title 2", Format.Markdown, "Content"); Section section3 = new Section("Title 3", Format.Markdown, "Content"); @@ -103,7 +103,7 @@ public void test_addSection_IncrementsTheSectionOrderNumber() { } @Test - public void test_addDecision_ThrowsAnException_WhenTheTitleIsNotSpecified() { + void test_addDecision_ThrowsAnException_WhenTheTitleIsNotSpecified() { try { Decision decision = new Decision("1"); @@ -115,7 +115,7 @@ public void test_addDecision_ThrowsAnException_WhenTheTitleIsNotSpecified() { } @Test - public void test_addDecision_ThrowsAnException_WhenTheContentIsNotSpecified() { + void test_addDecision_ThrowsAnException_WhenTheContentIsNotSpecified() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); @@ -128,7 +128,7 @@ public void test_addDecision_ThrowsAnException_WhenTheContentIsNotSpecified() { } @Test - public void test_addDecision_ThrowsAnException_WhenTheStatusIsNotSpecified() { + void test_addDecision_ThrowsAnException_WhenTheStatusIsNotSpecified() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); @@ -142,7 +142,7 @@ public void test_addDecision_ThrowsAnException_WhenTheStatusIsNotSpecified() { } @Test - public void test_addDecision_ThrowsAnException_WhenTheFormatIsNotSpecified() { + void test_addDecision_ThrowsAnException_WhenTheFormatIsNotSpecified() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); @@ -157,7 +157,7 @@ public void test_addDecision_ThrowsAnException_WhenTheFormatIsNotSpecified() { } @Test - public void test_addDecision_ThrowsAnException_WhenADecisionExistsWithTheSameId() { + void test_addDecision_ThrowsAnException_WhenADecisionExistsWithTheSameId() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); diff --git a/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java b/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java index bddc2cf47..92016904a 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java @@ -1,13 +1,13 @@ package com.structurizr.documentation; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class SectionTests { @Test - public void test_construction() { + void test_construction() { Section section = new Section("Title", Format.Markdown, "Content"); assertEquals("Title", section.getTitle()); diff --git a/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java b/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java index 581b14516..d011785a0 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java @@ -1,28 +1,27 @@ package com.structurizr.model; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static junit.framework.TestCase.assertNull; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class CodeElementTests { @Test - public void test_construction_WhenAFullyQualifiedNameIsSpecified() { + void test_construction_WhenAFullyQualifiedNameIsSpecified() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals("SomeComponent", codeElement.getName()); assertEquals("com.structurizr.component.SomeComponent", codeElement.getType()); } @Test - public void test_construction_WhenAFullyQualifiedNameIsSpecifiedInTheDefaultPackage() { + void test_construction_WhenAFullyQualifiedNameIsSpecifiedInTheDefaultPackage() { CodeElement codeElement = new CodeElement("SomeComponent"); assertEquals("SomeComponent", codeElement.getName()); assertEquals("SomeComponent", codeElement.getType()); } @Test - public void test_descriptionProperty() { + void test_descriptionProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNull(codeElement.getDescription()); @@ -31,7 +30,7 @@ public void test_descriptionProperty() { } @Test - public void test_sizeProperty() { + void test_sizeProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals(0, codeElement.getSize()); @@ -40,7 +39,7 @@ public void test_sizeProperty() { } @Test - public void test_languageProperty() { + void test_languageProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals("Java", codeElement.getLanguage()); @@ -49,7 +48,7 @@ public void test_languageProperty() { } @Test - public void test_categoryProperty() { + void test_categoryProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNull(codeElement.getCategory()); @@ -58,7 +57,7 @@ public void test_categoryProperty() { } @Test - public void test_visibilityProperty() { + void test_visibilityProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNull(codeElement.getVisibility()); @@ -67,70 +66,76 @@ public void test_visibilityProperty() { } @Test - public void test_setUrl() { + void test_setUrl() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); codeElement.setUrl("https://structurizr.com"); assertEquals("https://structurizr.com", codeElement.getUrl()); } - @Test(expected = IllegalArgumentException.class) - public void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - codeElement.setUrl("htt://blah"); + @Test + void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); + codeElement.setUrl("htt://blah"); + }); } @Test - public void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { + void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); codeElement.setUrl(null); assertNull(codeElement.getUrl()); } @Test - public void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); codeElement.setUrl(" "); assertNull(codeElement.getUrl()); } - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnIllegalArgumentException_WhenANullFullyQualifiedNameIsSpecified() { - new CodeElement(null); + @Test + void test_construction_ThrowsAnIllegalArgumentException_WhenANullFullyQualifiedNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + new CodeElement(null); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnIllegalArgumentException_WhenAnEmptyFullyQualifiedNameIsSpecified() { - new CodeElement(" "); + @Test + void test_construction_ThrowsAnIllegalArgumentException_WhenAnEmptyFullyQualifiedNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + new CodeElement(" "); + }); } @Test - public void test_equals_ReturnsFalse_WhenComparedToNull() { + void test_equals_ReturnsFalse_WhenComparedToNull() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertFalse(codeElement.equals(null)); + assertNotEquals(codeElement, null); } @Test - public void test_equals_ReturnsFalse_WhenComparedToDifferentTypeOfObject() { + void test_equals_ReturnsFalse_WhenComparedToDifferentTypeOfObject() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertFalse(codeElement.equals("hello")); + assertNotEquals(codeElement, "hello"); } @Test - public void test_equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithADifferentType() { + void test_equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithADifferentType() { CodeElement codeElement1 = new CodeElement("com.structurizr.component.SomeComponent1"); CodeElement codeElement2 = new CodeElement("com.structurizr.component.SomeComponent2"); - assertFalse(codeElement1.equals(codeElement2)); + assertNotEquals(codeElement1, codeElement2); } @Test - public void test_equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithTheSameType() { + void test_equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithTheSameType() { CodeElement codeElement1 = new CodeElement("com.structurizr.component.SomeComponent1"); CodeElement codeElement2 = new CodeElement("com.structurizr.component.SomeComponent1"); - assertTrue(codeElement1.equals(codeElement2)); + assertEquals(codeElement1, codeElement2); } @Test - public void test_getPackage_ReturnsThePackageName() { + void test_getPackage_ReturnsThePackageName() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals("com.structurizr.component", codeElement.getPackage()); } diff --git a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java index 5d1bbefc8..0036db291 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java @@ -1,12 +1,11 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static junit.framework.TestCase.assertNull; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ComponentTests extends AbstractWorkspaceTestBase { @@ -14,38 +13,38 @@ public class ComponentTests extends AbstractWorkspaceTestBase { private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); @Test - public void test_getName_ReturnsTheGivenName_WhenANameIsGiven() { + void test_getName_ReturnsTheGivenName_WhenANameIsGiven() { Component component = new Component(); component.setName("Some name"); assertEquals("Some name", component.getName()); } @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { Component component = container.addComponent("Component", "Description"); assertEquals("Component://System.Container.Component", component.getCanonicalName()); } @Test - public void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { Component component = container.addComponent("Name1/.Name2", "Description"); assertEquals("Component://System.Container.Name1Name2", component.getCanonicalName()); } @Test - public void test_getParent_ReturnsTheParentContainer() { + void test_getParent_ReturnsTheParentContainer() { Component component = container.addComponent("Component", "Description"); assertEquals(container, component.getParent()); } @Test - public void test_getContainer_ReturnsTheParentContainer() { + void test_getContainer_ReturnsTheParentContainer() { Component component = container.addComponent("Name", "Description"); assertEquals(container, component.getContainer()); } @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { + void test_removeTags_DoesNotRemoveRequiredTags() { Component component = new Component(); assertTrue(component.getTags().contains(Tags.ELEMENT)); assertTrue(component.getTags().contains(Tags.COMPONENT)); @@ -58,7 +57,7 @@ public void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - public void test_technologyProperty() { + void test_technologyProperty() { Component component = new Component(); assertNull(component.getTechnology()); @@ -67,7 +66,7 @@ public void test_technologyProperty() { } @Test - public void test_sizeProperty() { + void test_sizeProperty() { Component component = new Component(); assertEquals(0, component.getSize()); @@ -76,7 +75,7 @@ public void test_sizeProperty() { } @Test - public void test_setType_ThrowsAnExceptionWhenPassedNull() { + void test_setType_ThrowsAnExceptionWhenPassedNull() { Component component = new Component(); try { component.setType(null); @@ -87,7 +86,7 @@ public void test_setType_ThrowsAnExceptionWhenPassedNull() { } @Test - public void test_setType_AddsAPrimaryCodeElement_WhenPassedAFullyQualifiedTypeName() { + void test_setType_AddsAPrimaryCodeElement_WhenPassedAFullyQualifiedTypeName() { Component component = new Component(); component.setType("com.structurizr.web.HomePageController"); @@ -100,7 +99,7 @@ public void test_setType_AddsAPrimaryCodeElement_WhenPassedAFullyQualifiedTypeNa } @Test - public void test_setType_OverwritesThePrimaryCodeElement_WhenCalledMoreThanOnce() { + void test_setType_OverwritesThePrimaryCodeElement_WhenCalledMoreThanOnce() { Component component = new Component(); component.setType("com.structurizr.web.HomePageController"); component.setType("com.structurizr.web.SomeOtherController"); @@ -115,7 +114,7 @@ public void test_setType_OverwritesThePrimaryCodeElement_WhenCalledMoreThanOnce( } @Test - public void test_addSupportingType_ThrowsAnExceptionWhenPassedNull() { + void test_addSupportingType_ThrowsAnExceptionWhenPassedNull() { Component component = new Component(); try { component.addSupportingType(null); @@ -126,7 +125,7 @@ public void test_addSupportingType_ThrowsAnExceptionWhenPassedNull() { } @Test - public void test_addSupportingType_AddsASupportingCodeElement_WhenPassedAFullyQualifiedTypeName() { + void test_addSupportingType_AddsASupportingCodeElement_WhenPassedAFullyQualifiedTypeName() { Component component = new Component(); component.addSupportingType("com.structurizr.web.HomePageViewModel"); @@ -139,20 +138,20 @@ public void test_addSupportingType_AddsASupportingCodeElement_WhenPassedAFullyQu } @Test - public void test_getType_ReturnsNull_WhenThereAreNoCodeElements() { + void test_getType_ReturnsNull_WhenThereAreNoCodeElements() { Component component = new Component(); assertNull(component.getType()); } @Test - public void test_getType_ReturnsNull_WhenThereAreNoPrimaryCodeElements() { + void test_getType_ReturnsNull_WhenThereAreNoPrimaryCodeElements() { Component component = new Component(); component.addSupportingType("com.structurizr.SomeType"); assertNull(component.getType()); } @Test - public void test_getType_ReturnsThePrimaryCodeElement_WhenThereIsAPrimaryCodeElement() { + void test_getType_ReturnsThePrimaryCodeElement_WhenThereIsAPrimaryCodeElement() { Component component = new Component(); component.setType("com.structurizr.SomeType"); CodeElement codeElement = component.getType(); diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java index 96b306f9a..12e62a53c 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java @@ -1,9 +1,9 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ContainerInstanceTests extends AbstractWorkspaceTestBase { @@ -12,7 +12,7 @@ public class ContainerInstanceTests extends AbstractWorkspaceTestBase { private DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @Test - public void test_construction() { + void test_construction() { ContainerInstance instance = deploymentNode.add(database); assertSame(database, instance.getContainer()); @@ -21,7 +21,7 @@ public void test_construction() { } @Test - public void test_getContainerId() { + void test_getContainerId() { ContainerInstance instance = deploymentNode.add(database); assertEquals(database.getId(), instance.getContainerId()); @@ -31,7 +31,7 @@ public void test_getContainerId() { } @Test - public void test_getName() { + void test_getName() { ContainerInstance instance = deploymentNode.add(database); assertEquals("Database Schema", instance.getName()); @@ -41,28 +41,28 @@ public void test_getName() { } @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { ContainerInstance instance = deploymentNode.add(database); assertEquals("ContainerInstance://Default/Deployment Node/System.Database Schema[1]", instance.getCanonicalName()); } @Test - public void test_getParent_ReturnsTheParentDeploymentNode() { + void test_getParent_ReturnsTheParentDeploymentNode() { ContainerInstance instance = deploymentNode.add(database); assertEquals(deploymentNode, instance.getParent()); } @Test - public void test_getRequiredTags() { + void test_getRequiredTags() { ContainerInstance instance = deploymentNode.add(database); assertTrue(instance.getDefaultTags().isEmpty()); } @Test - public void test_getTags() { + void test_getTags() { database.addTags("Database"); ContainerInstance instance = deploymentNode.add(database); instance.addTags("Primary Instance"); @@ -71,7 +71,7 @@ public void test_getTags() { } @Test - public void test_removeTags_DoesNotRemoveAnyTags() { + void test_removeTags_DoesNotRemoveAnyTags() { ContainerInstance instance = deploymentNode.add(database); assertTrue(instance.getTags().contains(Tags.CONTAINER_INSTANCE)); @@ -82,7 +82,7 @@ public void test_removeTags_DoesNotRemoveAnyTags() { } @Test - public void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { + void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { ContainerInstance instance = deploymentNode.add(database); assertEquals(1, instance.getDeploymentGroups().size()); @@ -90,7 +90,7 @@ public void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { } @Test - public void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { + void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { ContainerInstance instance = deploymentNode.add(database, "Group 1"); assertEquals(1, instance.getDeploymentGroups().size()); @@ -98,7 +98,7 @@ public void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { } @Test - public void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { + void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { ContainerInstance instance = deploymentNode.add(database, "Group 1", "Group 2"); assertEquals(2, instance.getDeploymentGroups().size()); @@ -107,7 +107,7 @@ public void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { } @Test - public void test_addHealthCheck() { + void test_addHealthCheck() { ContainerInstance instance = deploymentNode.add(database); assertTrue(instance.getHealthChecks().isEmpty()); @@ -120,7 +120,7 @@ public void test_addHealthCheck() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { + void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { ContainerInstance instance = deploymentNode.add(database); try { @@ -132,7 +132,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { + void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { ContainerInstance instance = deploymentNode.add(database); try { @@ -144,7 +144,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { + void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { ContainerInstance instance = deploymentNode.add(database); try { @@ -156,7 +156,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { + void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { ContainerInstance instance = deploymentNode.add(database); try { @@ -168,7 +168,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { + void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { ContainerInstance instance = deploymentNode.add(database); try { @@ -180,7 +180,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { + void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { ContainerInstance instance = deploymentNode.add(database); try { @@ -192,7 +192,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero( } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { + void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { ContainerInstance instance = deploymentNode.add(database); try { diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java index 07813325b..80bd6af86 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java @@ -1,9 +1,9 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ContainerTests extends AbstractWorkspaceTestBase { @@ -11,7 +11,7 @@ public class ContainerTests extends AbstractWorkspaceTestBase { private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); @Test - public void test_technologyProperty() { + void test_technologyProperty() { assertEquals("Some technology", container.getTechnology()); container.setTechnology("Some other technology"); @@ -19,29 +19,29 @@ public void test_technologyProperty() { } @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { assertEquals("Container://System.Container", container.getCanonicalName()); } @Test - public void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { container = softwareSystem.addContainer("Name1/.Name2", "Description", "Some technology"); assertEquals("Container://System.Name1Name2", container.getCanonicalName()); } @Test - public void test_getParent_ReturnsTheParentSoftwareSystem() { + void test_getParent_ReturnsTheParentSoftwareSystem() { assertEquals(softwareSystem, container.getParent()); } @Test - public void test_getSoftwareSystem_ReturnsTheParentSoftwareSystem() { + void test_getSoftwareSystem_ReturnsTheParentSoftwareSystem() { assertEquals(softwareSystem, container.getSoftwareSystem()); } @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { + void test_removeTags_DoesNotRemoveRequiredTags() { assertTrue(container.getTags().contains(Tags.ELEMENT)); assertTrue(container.getTags().contains(Tags.CONTAINER)); @@ -52,18 +52,22 @@ public void test_removeTags_DoesNotRemoveRequiredTags() { assertTrue(container.getTags().contains(Tags.CONTAINER)); } - @Test(expected = IllegalArgumentException.class) - public void test_addComponent_ThrowsAnException_WhenANullNameIsSpecified() { - container.addComponent(null, ""); + @Test + void test_addComponent_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + container.addComponent(null, ""); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_addComponent_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - container.addComponent(" ", ""); + @Test + void test_addComponent_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + container.addComponent(" ", ""); + }); } @Test - public void test_addComponent_ThrowsAnException_WhenAComponentWithTheSameNameAlreadyExists() { + void test_addComponent_ThrowsAnException_WhenAComponentWithTheSameNameAlreadyExists() { container.addComponent("Component 1", ""); try { container.addComponent("Component 1", ""); @@ -74,7 +78,7 @@ public void test_addComponent_ThrowsAnException_WhenAComponentWithTheSameNameAlr } @Test - public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() { + void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() { Component component = container.addComponent("Name", "Description"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -86,7 +90,7 @@ public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() } @Test - public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnology() { + void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnology() { Component component = container.addComponent("Name", "Description", "Technology"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -98,7 +102,7 @@ public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAn } @Test - public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndStringType() { + void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndStringType() { Component component = container.addComponent("Name", "SomeType", "Description", "Technology"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -110,7 +114,7 @@ public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAn } @Test - public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndClassType() { + void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndClassType() { Component component = container.addComponent("Name", this.getClass(), "Description", "Technology"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -122,7 +126,7 @@ public void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAn } @Test - public void test_getComponentWithName_ThrowsAnException_WhenANullNameIsSpecified() { + void test_getComponentWithName_ThrowsAnException_WhenANullNameIsSpecified() { try { container.getComponentWithName(null); fail(); @@ -132,7 +136,7 @@ public void test_getComponentWithName_ThrowsAnException_WhenANullNameIsSpecified } @Test - public void test_getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void test_getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { try { container.getComponentWithName(" "); fail(); @@ -142,7 +146,7 @@ public void test_getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecifi } @Test - public void test_getComponentOfType_ThrowsAnException_WhenANullTypeIsSpecified() { + void test_getComponentOfType_ThrowsAnException_WhenANullTypeIsSpecified() { try { container.getComponentOfType(null); fail(); @@ -152,7 +156,7 @@ public void test_getComponentOfType_ThrowsAnException_WhenANullTypeIsSpecified() } @Test - public void test_getComponentOfType_ThrowsAnException_WhenAnEmptyTypeIsSpecified() { + void test_getComponentOfType_ThrowsAnException_WhenAnEmptyTypeIsSpecified() { try { container.getComponentOfType(" "); fail(); @@ -162,12 +166,12 @@ public void test_getComponentOfType_ThrowsAnException_WhenAnEmptyTypeIsSpecified } @Test - public void test_getComponentOfType_ReturnsNull_WhenNoComponentWithTheSpecifiedTypeExists() { + void test_getComponentOfType_ReturnsNull_WhenNoComponentWithTheSpecifiedTypeExists() { assertNull(container.getComponentOfType("SomeType")); } @Test - public void test_getComponentOfType_ReturnsAComponent_WhenAComponentWithTheSpecifiedTypeExists() { + void test_getComponentOfType_ReturnsAComponent_WhenAComponentWithTheSpecifiedTypeExists() { container.addComponent("Name", "SomeType", "Description", "Technology"); Component component = container.getComponentOfType("SomeType"); diff --git a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java index d61147232..d02cd43b4 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java @@ -1,16 +1,16 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests extends AbstractWorkspaceTestBase { @Test - public void test_impliedRelationshipsAreCreated() { + void test_impliedRelationshipsAreCreated() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); @@ -21,7 +21,7 @@ public void test_impliedRelationshipsAreCreated() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); - Relationship explicitRelationship = aaa.uses(bbb, "Uses 1", "Technology", InteractionStyle.Asynchronous, new String[] { "Tag 1", "Tag 2" }); + Relationship explicitRelationship = aaa.uses(bbb, "Uses 1", "Technology", InteractionStyle.Asynchronous, new String[]{"Tag 1", "Tag 2"}); assertEquals(9, model.getRelationships().size()); assertTrue(aaa.hasEfferentRelationshipWith(bbb, "Uses 1")); @@ -54,7 +54,7 @@ public void test_impliedRelationshipsAreCreated() { } @Test - public void test_impliedRelationshipsAreCreated_UnlessAnyRelationshipExists() { + void test_impliedRelationshipsAreCreated_UnlessAnyRelationshipExists() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); diff --git a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java index 0fa35ee89..38ebb887b 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java @@ -1,16 +1,16 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests extends AbstractWorkspaceTestBase { @Test - public void test_impliedRelationships_WhenNoSummaryRelationshipsExist() { + void test_impliedRelationships_WhenNoSummaryRelationshipsExist() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); @@ -21,7 +21,7 @@ public void test_impliedRelationships_WhenNoSummaryRelationshipsExist() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy()); - Relationship explicitRelationship = aaa.uses(bbb, "Uses 1", "Technology", InteractionStyle.Asynchronous, new String[] { "Tag 1", "Tag 2" }); + Relationship explicitRelationship = aaa.uses(bbb, "Uses 1", "Technology", InteractionStyle.Asynchronous, new String[]{"Tag 1", "Tag 2"}); assertEquals(9, model.getRelationships().size()); assertTrue(aaa.hasEfferentRelationshipWith(bbb, "Uses 1")); diff --git a/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java b/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java index 6637c088c..81fdbaf82 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java @@ -1,14 +1,14 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class CustomElementTests extends AbstractWorkspaceTestBase { @Test - public void test_basicProperties() { + void test_basicProperties() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertEquals("Name", element.getName()); assertEquals("Type", element.getMetadata()); @@ -16,26 +16,26 @@ public void test_basicProperties() { } @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertEquals("Custom://Name", element.getCanonicalName()); } @Test - public void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); element.setName("Name1/.Name2"); assertEquals("Custom://Name1Name2", element.getCanonicalName()); } @Test - public void test_getParent_ReturnsNull() { + void test_getParent_ReturnsNull() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertNull(element.getParent()); } @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { + void test_removeTags_DoesNotRemoveRequiredTags() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertTrue(element.getTags().contains(Tags.ELEMENT)); @@ -45,7 +45,7 @@ public void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - public void test_uses_AddsARelationshipWhenTheDescriptionIsSpecified() { + void test_uses_AddsARelationshipWhenTheDescriptionIsSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); @@ -61,7 +61,7 @@ public void test_uses_AddsARelationshipWhenTheDescriptionIsSpecified() { } @Test - public void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { + void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); @@ -73,11 +73,11 @@ public void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecifi assertSame(element2, relationship.getDestination()); assertEquals("Uses", relationship.getDescription()); assertEquals("Technology", relationship.getTechnology()); - assertEquals(null, relationship.getInteractionStyle()); + assertNull(relationship.getInteractionStyle()); } @Test - public void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { + void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); @@ -93,11 +93,11 @@ public void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInterac } @Test - public void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAndTagsAreSpecified() { + void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAndTagsAreSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); - element1.uses(element2, "Uses", "Technology", InteractionStyle.Asynchronous, new String[] { "Tag 1", "Tag 2" }); + element1.uses(element2, "Uses", "Technology", InteractionStyle.Asynchronous, new String[]{"Tag 1", "Tag 2"}); assertEquals(1, element1.getRelationships().size()); Relationship relationship = element1.getRelationships().iterator().next(); diff --git a/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java b/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java index 26ca1ff02..afd611ab5 100644 --- a/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java @@ -1,15 +1,15 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class DefaultImpliedRelationshipsStrategyTests extends AbstractWorkspaceTestBase { @Test - public void test_createImpliedRelationships_DoesNothing() { + void test_createImpliedRelationships_DoesNothing() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); diff --git a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java b/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java index 694741de9..86ae6a609 100644 --- a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java @@ -2,21 +2,21 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.util.MapUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class DeploymentNodeTests extends AbstractWorkspaceTestBase { @Test - public void test_getCanonicalName_WhenTheDeploymentNodeHasNoParent() { + void test_getCanonicalName_WhenTheDeploymentNodeHasNoParent() { DeploymentNode deploymentNode = model.addDeploymentNode("Ubuntu Server", "", ""); assertEquals("DeploymentNode://Default/Ubuntu Server", deploymentNode.getCanonicalName()); } @Test - public void test_getCanonicalName_WhenTheDeploymentNodeHasAParent() { + void test_getCanonicalName_WhenTheDeploymentNodeHasAParent() { DeploymentNode l1 = model.addDeploymentNode("Level 1", "", ""); DeploymentNode l2 = l1.addDeploymentNode("Level 2", "", ""); DeploymentNode l3 = l2.addDeploymentNode("Level 3", "", ""); @@ -27,7 +27,7 @@ public void test_getCanonicalName_WhenTheDeploymentNodeHasAParent() { } @Test - public void test_getParent_ReturnsTheParentDeploymentNode() { + void test_getParent_ReturnsTheParentDeploymentNode() { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); assertNull(parent.getParent()); @@ -37,7 +37,7 @@ public void test_getParent_ReturnsTheParentDeploymentNode() { } @Test - public void test_getRequiredTags() { + void test_getRequiredTags() { DeploymentNode deploymentNode = new DeploymentNode(); assertEquals(2, deploymentNode.getDefaultTags().size()); assertTrue(deploymentNode.getDefaultTags().contains(Tags.ELEMENT)); @@ -45,14 +45,14 @@ public void test_getRequiredTags() { } @Test - public void test_getTags() { + void test_getTags() { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.addTags("Tag 1", "Tag 2"); assertEquals("Element,Deployment Node,Tag 1,Tag 2", deploymentNode.getTags()); } @Test - public void test_add_ThrowsAnException_WhenASoftwareSystemIsNotSpecified() { + void test_add_ThrowsAnException_WhenASoftwareSystemIsNotSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); deploymentNode.add((SoftwareSystem) null); @@ -63,10 +63,10 @@ public void test_add_ThrowsAnException_WhenASoftwareSystemIsNotSpecified() { } @Test - public void test_add_ThrowsAnException_WhenAContainerIsNotSpecified() { + void test_add_ThrowsAnException_WhenAContainerIsNotSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); - deploymentNode.add((Container)null); + deploymentNode.add((Container) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("A container must be specified.", iae.getMessage()); @@ -74,7 +74,7 @@ public void test_add_ThrowsAnException_WhenAContainerIsNotSpecified() { } @Test - public void test_add_AddsAContainerInstance_WhenAContainerIsSpecified() { + void test_add_AddsAContainerInstance_WhenAContainerIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); @@ -87,7 +87,7 @@ public void test_add_AddsAContainerInstance_WhenAContainerIsSpecified() { } @Test - public void test_addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { + void test_addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { try { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); parent.addDeploymentNode(null, "", ""); @@ -98,7 +98,7 @@ public void test_addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { } @Test - public void test_addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { + void test_addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); DeploymentNode child = parent.addDeploymentNode("Child 1", "Description", "Technology"); @@ -134,10 +134,10 @@ public void test_addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified } @Test - public void test_uses_ThrowsAnException_WhenANullDestinationIsSpecified() { + void test_uses_ThrowsAnException_WhenANullDestinationIsSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); - deploymentNode.uses((DeploymentNode)null, "", ""); + deploymentNode.uses((DeploymentNode) null, "", ""); fail(); } catch (IllegalArgumentException iae) { assertEquals("The destination must be specified.", iae.getMessage()); @@ -145,7 +145,7 @@ public void test_uses_ThrowsAnException_WhenANullDestinationIsSpecified() { } @Test - public void test_uses_AddsARelationship() { + void test_uses_AddsARelationship() { DeploymentNode primaryNode = model.addDeploymentNode("MySQL - Primary", "", ""); DeploymentNode secondaryNode = model.addDeploymentNode("MySQL - Secondary", "", ""); Relationship relationship = primaryNode.uses(secondaryNode, "Replicates data to", "Some technology"); @@ -158,7 +158,7 @@ public void test_uses_AddsARelationship() { } @Test - public void test_getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpecified() { + void test_getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.getDeploymentNodeWithName(null); @@ -169,33 +169,33 @@ public void test_getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpeci } @Test - public void test_getDeploymentNodeWithName_ReturnsNull_WhenThereIsNoDeploymentNodeWithTheSpecifiedName() { + void test_getDeploymentNodeWithName_ReturnsNull_WhenThereIsNoDeploymentNodeWithTheSpecifiedName() { DeploymentNode deploymentNode = new DeploymentNode(); assertNull(deploymentNode.getDeploymentNodeWithName("foo")); } @Test - public void test_getDeploymentNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsADeploymentNodeWithTheSpecifiedName() { + void test_getDeploymentNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsADeploymentNodeWithTheSpecifiedName() { DeploymentNode parent = model.addDeploymentNode("parent", "", ""); DeploymentNode child = parent.addDeploymentNode("child", "", ""); assertSame(child, parent.getDeploymentNodeWithName("child")); } @Test - public void test_getInfrastructureNodeWithName_ReturnsNull_WhenThereIsNoInfrastructureNodeWithTheSpecifiedName() { + void test_getInfrastructureNodeWithName_ReturnsNull_WhenThereIsNoInfrastructureNodeWithTheSpecifiedName() { DeploymentNode deploymentNode = new DeploymentNode(); assertNull(deploymentNode.getInfrastructureNodeWithName("foo")); } @Test - public void test_getInfrastructureNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsAInfrastructureNodeWithTheSpecifiedName() { + void test_getInfrastructureNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsAInfrastructureNodeWithTheSpecifiedName() { DeploymentNode parent = model.addDeploymentNode("parent", "", ""); InfrastructureNode child = parent.addInfrastructureNode("child", "", ""); assertSame(child, parent.getInfrastructureNodeWithName("child")); } @Test - public void test_setInstances() { + void test_setInstances() { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.setInstances(8); @@ -203,7 +203,7 @@ public void test_setInstances() { } @Test - public void test_setInstances_ThrowsAnException_WhenZeroIsSpecified() { + void test_setInstances_ThrowsAnException_WhenZeroIsSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.setInstances(0); @@ -214,7 +214,7 @@ public void test_setInstances_ThrowsAnException_WhenZeroIsSpecified() { } @Test - public void test_setInstances_ThrowsAnException_WhenANegativeNumberIsSpecified() { + void test_setInstances_ThrowsAnException_WhenANegativeNumberIsSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.setInstances(-1); diff --git a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java b/structurizr-core/test/unit/com/structurizr/model/ElementTests.java index cf2b8b212..6dfbe398b 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ElementTests.java @@ -2,24 +2,24 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.Workspace; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ElementTests extends AbstractWorkspaceTestBase { - @Test - public void test_construction() { - Element element = model.addSoftwareSystem("Name", "Description"); - assertEquals("Name", element.getName()); - assertEquals("Description", element.getDescription()); - } + @Test + void test_construction() { + Element element = model.addSoftwareSystem("Name", "Description"); + assertEquals("Name", element.getName()); + assertEquals("Description", element.getDescription()); + } @Test - public void test_setName_ThrowsAnException_WhenANullValueIsSpecified() { + void test_setName_ThrowsAnException_WhenANullValueIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); try { element.setName(null); @@ -30,7 +30,7 @@ public void test_setName_ThrowsAnException_WhenANullValueIsSpecified() { } @Test - public void test_setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { + void test_setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); try { element.setName(" "); @@ -41,26 +41,26 @@ public void test_setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { } @Test - public void test_hasEfferentRelationshipWith_ReturnsFalse_WhenANullElementIsSpecified() { + void test_hasEfferentRelationshipWith_ReturnsFalse_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertFalse(softwareSystem1.hasEfferentRelationshipWith(null)); } @Test - public void test_hasEfferentRelationshipWith_ReturnsFalse_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { + void test_hasEfferentRelationshipWith_ReturnsFalse_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertFalse(softwareSystem1.hasEfferentRelationshipWith(softwareSystem1)); } @Test - public void test_hasEfferentRelationshipWith_ReturnsTrue_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { + void test_hasEfferentRelationshipWith_ReturnsTrue_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); softwareSystem1.uses(softwareSystem1, "uses"); assertTrue(softwareSystem1.hasEfferentRelationshipWith(softwareSystem1)); } @Test - public void test_hasEfferentRelationshipWith_ReturnsTrue_WhenThereIsARelationship() { + void test_hasEfferentRelationshipWith_ReturnsTrue_WhenThereIsARelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -68,13 +68,13 @@ public void test_hasEfferentRelationshipWith_ReturnsTrue_WhenThereIsARelationshi } @Test - public void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenANullElementIsSpecified() { + void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertFalse(softwareSystem1.hasEfferentRelationshipWith(null, null)); } @Test - public void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenThereIsNotAMatchingRelationship() { + void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenThereIsNotAMatchingRelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -83,7 +83,7 @@ public void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_W } @Test - public void test_hasEfferentRelationshipWithElementAndDescription_ReturnsTrue_WhenThereIsAMatchingRelationship() { + void test_hasEfferentRelationshipWithElementAndDescription_ReturnsTrue_WhenThereIsAMatchingRelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -92,19 +92,19 @@ public void test_hasEfferentRelationshipWithElementAndDescription_ReturnsTrue_Wh } @Test - public void test_getEfferentRelationshipWith_ReturnsNull_WhenANullElementIsSpecified() { + void test_getEfferentRelationshipWith_ReturnsNull_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertNull(softwareSystem1.getEfferentRelationshipWith(null)); } @Test - public void test_getEfferentRelationshipWith_ReturnsNull_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { + void test_getEfferentRelationshipWith_ReturnsNull_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertNull(softwareSystem1.getEfferentRelationshipWith(softwareSystem1)); } @Test - public void test_getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { + void test_getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); softwareSystem1.uses(softwareSystem1, "uses"); @@ -115,7 +115,7 @@ public void test_getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSa } @Test - public void test_getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsARelationship() { + void test_getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsARelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -127,7 +127,7 @@ public void test_getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsA } @Test - public void test_hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRelationships() { + void test_hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRelationships() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -136,7 +136,7 @@ public void test_hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRel } @Test - public void test_hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelationships() { + void test_hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelationships() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -145,7 +145,7 @@ public void test_hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelati } @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwareSystemIsAddedMoreThanOnce() { + void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwareSystemIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); @@ -164,7 +164,7 @@ public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwar } @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAContainerIsAddedMoreThanOnce() { + void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAContainerIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); Container bb = b.addContainer("BB", "", ""); @@ -184,7 +184,7 @@ public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAContain } @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAComponentIsAddedMoreThanOnce() { + void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAComponentIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); Container bb = b.addContainer("BB", "", ""); @@ -205,7 +205,7 @@ public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithACompone } @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonIsAddedMoreThanOnce() { + void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Person b = model.addPerson("B", ""); @@ -224,52 +224,54 @@ public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonI } @Test - public void test_equals_ReturnsFalse_WhenTestedAgainstNull() { + void test_equals_ReturnsFalse_WhenTestedAgainstNull() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - assertFalse(softwareSystem.equals(null)); + assertNotEquals(softwareSystem, null); } @Test - public void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferentTypes() { + void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferentTypes() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - assertFalse(softwareSystem.equals("hello world")); + assertNotEquals(softwareSystem, "hello world"); } @Test - public void test_equals_ReturnsTrue_WhenTestedAgainstItself() { + void test_equals_ReturnsTrue_WhenTestedAgainstItself() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - assertTrue(softwareSystem.equals(softwareSystem)); + assertEquals(softwareSystem, softwareSystem); } @Test - public void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferent() { + void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferent() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", "Description"); - assertFalse(softwareSystemA.equals(softwareSystemB)); + assertNotEquals(softwareSystemA, softwareSystemB); } @Test - public void test_equals_ReturnsFalse_WhenTheTwoElementsHaveTheSameCanonicalNameButAreDifferentTypes() { + void test_equals_ReturnsFalse_WhenTheTwoElementsHaveTheSameCanonicalNameButAreDifferentTypes() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); Person person = model.addPerson("Name", "Description"); - assertFalse(softwareSystem.equals(person)); + assertNotEquals(softwareSystem, person); } @Test - public void test_setUrl() { + void test_setUrl() { Element element = model.addSoftwareSystem("Name", "Description"); element.setUrl("https://structurizr.com"); assertEquals("https://structurizr.com", element.getUrl()); } - @Test(expected = IllegalArgumentException.class) - public void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - Element element = model.addSoftwareSystem("Name", "Description"); - element.setUrl("htt://blah"); + @Test + void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + Element element = model.addSoftwareSystem("Name", "Description"); + element.setUrl("htt://blah"); + }); } @Test - public void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { + void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.setUrl("https://structurizr.com"); element.setUrl(null); @@ -277,7 +279,7 @@ public void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { } @Test - public void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { + void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.setUrl("https://structurizr.com"); element.setUrl(" "); diff --git a/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java b/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java index abd70a3e3..bc91c32b9 100644 --- a/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java @@ -1,35 +1,35 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; public class GroupableElementTests extends AbstractWorkspaceTestBase { @Test - public void test_getGroup_ReturnsNullByDefault() { + void test_getGroup_ReturnsNullByDefault() { Person element = model.addPerson("Person"); assertNull(element.getGroup()); } @Test - public void test_setGroup() { + void test_setGroup() { Person element = model.addPerson("Person"); element.setGroup("Group"); assertEquals("Group", element.getGroup()); } @Test - public void test_setGroup_TrimsWhiteSpace() { + void test_setGroup_TrimsWhiteSpace() { Person element = model.addPerson("Person"); element.setGroup(" Group "); assertEquals("Group", element.getGroup()); } @Test - public void test_setGroup_HandlesEmptyAndNullValues() { + void test_setGroup_HandlesEmptyAndNullValues() { Person element = model.addPerson("Person"); element.setGroup("Group"); diff --git a/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java b/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java index 3610d812a..8b3ec7496 100644 --- a/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java @@ -1,22 +1,22 @@ package com.structurizr.model; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class HttpHealthCheckTests { private HttpHealthCheck healthCheck; @Test - public void test_defaultConstructorExists() { + void test_defaultConstructorExists() { // the default constructor is used when deserializing from JSON healthCheck = new HttpHealthCheck(); } @Test - public void test_construction() { + void test_construction() { healthCheck = new HttpHealthCheck("Name", "http://localhost", 120, 1000); assertEquals("Name", healthCheck.getName()); assertEquals("http://localhost", healthCheck.getUrl()); @@ -25,14 +25,14 @@ public void test_construction() { } @Test - public void test_addHeader() { + void test_addHeader() { healthCheck = new HttpHealthCheck(); healthCheck.addHeader("Name", "Value"); assertEquals("Value", healthCheck.getHeaders().get("Name")); } @Test - public void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsNull() { + void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsNull() { healthCheck = new HttpHealthCheck(); try { healthCheck.addHeader(null, "value"); @@ -43,7 +43,7 @@ public void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsNull() { } @Test - public void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsEmpty() { + void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsEmpty() { healthCheck = new HttpHealthCheck(); try { healthCheck.addHeader(" ", "value"); @@ -54,7 +54,7 @@ public void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsEmpty() { } @Test - public void test_addHeader_ThrowsAnException_WhenTheHeaderValueIsNull() { + void test_addHeader_ThrowsAnException_WhenTheHeaderValueIsNull() { healthCheck = new HttpHealthCheck(); try { healthCheck.addHeader("Name", null); @@ -65,7 +65,7 @@ public void test_addHeader_ThrowsAnException_WhenTheHeaderValueIsNull() { } @Test - public void test_addHeader_DoesNotThrowAnException_WhenTheHeaderValueIsEmpty() { + void test_addHeader_DoesNotThrowAnException_WhenTheHeaderValueIsEmpty() { healthCheck = new HttpHealthCheck(); healthCheck.addHeader("Name", ""); assertEquals("", healthCheck.getHeaders().get("Name")); diff --git a/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java b/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java index 11ec4c204..c910b2493 100644 --- a/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java @@ -1,14 +1,14 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class InfrastructureNodeTests extends AbstractWorkspaceTestBase { @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services", "", ""); InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Route 53", "", ""); @@ -16,7 +16,7 @@ public void test_getCanonicalName() { } @Test - public void test_getParent_ReturnsTheParentDeploymentNode() { + void test_getParent_ReturnsTheParentDeploymentNode() { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); InfrastructureNode child = parent.addInfrastructureNode("Child", "", ""); child.setParent(parent); @@ -24,7 +24,7 @@ public void test_getParent_ReturnsTheParentDeploymentNode() { } @Test - public void test_getRequiredTags() { + void test_getRequiredTags() { InfrastructureNode infrastructureNode = new InfrastructureNode(); assertEquals(2, infrastructureNode.getDefaultTags().size()); assertTrue(infrastructureNode.getDefaultTags().contains(Tags.ELEMENT)); @@ -32,7 +32,7 @@ public void test_getRequiredTags() { } @Test - public void test_getTags() { + void test_getTags() { InfrastructureNode infrastructureNode = new InfrastructureNode(); infrastructureNode.addTags("Tag 1", "Tag 2"); assertEquals("Element,Infrastructure Node,Tag 1,Tag 2", infrastructureNode.getTags()); diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java index da1df410c..8bdf44012 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java @@ -1,56 +1,53 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; public class ModelItemTests extends AbstractWorkspaceTestBase { - @Test - public void test_construction() { - Element element = model.addSoftwareSystem("Name", "Description"); - assertEquals("Name", element.getName()); - assertEquals("Description", element.getDescription()); - } + @Test + void test_construction() { + Element element = model.addSoftwareSystem("Name", "Description"); + assertEquals("Name", element.getName()); + assertEquals("Description", element.getDescription()); + } @Test - public void test_getTags_WhenThereAreNoTags() { + void test_getTags_WhenThereAreNoTags() { Element element = model.addSoftwareSystem("Name", "Description"); assertEquals("Element,Software System", element.getTags()); } @Test - public void test_hasTag_ChecksRequiredTags() { + void test_hasTag_ChecksRequiredTags() { SoftwareSystem system = model.addSoftwareSystem("Name", "Description"); - assertTrue("hasTag returns true for Software System", system.hasTag("Software System")); - assertTrue("hasTag returns true for Element", system.hasTag("Element")); + assertTrue(system.hasTag("Software System"), "hasTag returns true for Software System"); + assertTrue(system.hasTag("Element"), "hasTag returns true for Element"); } @Test - public void test_getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { + void test_getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags("tag1", "tag2", "tag3"); assertEquals("Element,Software System,tag1,tag2,tag3", element.getTags()); } @Test - public void test_setTags_DoesNotDoAnything_WhenPassedNull() { + void test_setTags_DoesNotDoAnything_WhenPassedNull() { Element element = model.addSoftwareSystem("Name", "Description"); element.setTags(null); assertEquals("Element,Software System", element.getTags()); } @Test - public void test_addTags_DoesNotDoAnything_WhenPassedNull() { + void test_addTags_DoesNotDoAnything_WhenPassedNull() { Element element = model.addSoftwareSystem("Name", "Description"); - element.addTags((String)null); + element.addTags((String) null); assertEquals("Element,Software System", element.getTags()); element.addTags(null, null, null); @@ -58,37 +55,38 @@ public void test_addTags_DoesNotDoAnything_WhenPassedNull() { } @Test - public void test_addTags_AddsTags_WhenPassedSomeTags() { + void test_addTags_AddsTags_WhenPassedSomeTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags(null, "tag1", null, "tag2"); assertEquals("Element,Software System,tag1,tag2", element.getTags()); } @Test - public void test_addTags_AddsTags_WhenPassedSomeTagsAndThereAreDuplicateTags() { + void test_addTags_AddsTags_WhenPassedSomeTagsAndThereAreDuplicateTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags(null, "tag1", null, "tag2", "tag2"); assertEquals("Element,Software System,tag1,tag2", element.getTags()); } @Test - public void test_removeTags() { + void test_removeTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags("tag1", "tag2"); - assertTrue("Remove an existing tag returns true", element.removeTag("tag1")); - assertFalse("Tag has been removed", element.hasTag("tag1")); + assertTrue(element.removeTag("tag1"), "Remove an existing tag returns true"); + assertFalse(element.hasTag("tag1"), "Tag has been removed"); - assertFalse("Remove a non-existing tag returns false", element.removeTag("no-such-tag")); - assertFalse("Remove a required tag returns false", element.removeTag("Element")); + assertFalse(element.removeTag("no-such-tag"), "Remove a non-existing tag returns false"); + assertFalse(element.removeTag("Element"), "Remove a required tag returns false"); } + @Test - public void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { Element element = model.addSoftwareSystem("Name", "Description"); assertEquals(0, element.getProperties().size()); } @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { + void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty(null, "value"); @@ -99,7 +97,7 @@ public void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty(" ", "value"); @@ -110,7 +108,7 @@ public void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { + void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty("name", null); @@ -121,7 +119,7 @@ public void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty("name", " "); @@ -132,21 +130,21 @@ public void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { } @Test - public void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty("AWS region", "us-east-1"); assertEquals("us-east-1", element.getProperties().get("AWS region")); } @Test - public void test_setProperties_DoesNothing_WhenNullIsSpecified() { + void test_setProperties_DoesNothing_WhenNullIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.setProperties(null); assertEquals(0, element.getProperties().size()); } @Test - public void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); Map properties = new HashMap<>(); properties.put("name", "value"); @@ -156,7 +154,7 @@ public void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { } @Test - public void test_addPerspective_ThrowsAnException_WhenANameIsNotSpecified() { + void test_addPerspective_ThrowsAnException_WhenANameIsNotSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective(null, null); @@ -167,7 +165,7 @@ public void test_addPerspective_ThrowsAnException_WhenANameIsNotSpecified() { } @Test - public void test_addPerspective_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void test_addPerspective_ThrowsAnException_WhenAnEmptyNameIsSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective(" ", null); @@ -176,8 +174,9 @@ public void test_addPerspective_ThrowsAnException_WhenAnEmptyNameIsSpecified() { assertEquals("A name must be specified.", iae.getMessage()); } } + @Test - public void test_addPerspective_ThrowsAnException_WhenADescriptionIsNotSpecified() { + void test_addPerspective_ThrowsAnException_WhenADescriptionIsNotSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective("Security", null); @@ -188,7 +187,7 @@ public void test_addPerspective_ThrowsAnException_WhenADescriptionIsNotSpecified } @Test - public void test_addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecified() { + void test_addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective("Security", " "); @@ -199,7 +198,7 @@ public void test_addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecif } @Test - public void test_addPerspective_AddsAPerspective() { + void test_addPerspective_AddsAPerspective() { Element element = model.addSoftwareSystem("Name", "Description"); Perspective perspective = element.addPerspective("Security", "Data is encrypted at rest."); assertEquals("Security", perspective.getName()); @@ -208,7 +207,7 @@ public void test_addPerspective_AddsAPerspective() { } @Test - public void test_addPerspective_ThrowsAnException_WhenTheNamedPerspectiveAlreadyExists() { + void test_addPerspective_ThrowsAnException_WhenTheNamedPerspectiveAlreadyExists() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective("Security", "Data is encrypted at rest."); diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java index 83c4b49cf..1fc5381a6 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java @@ -1,37 +1,45 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.Set; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ModelTests extends AbstractWorkspaceTestBase { - @Test(expected = IllegalArgumentException.class) - public void test_addSoftwareSystem_ThrowsAnException_WhenANullNameIsSpecified() { - model.addSoftwareSystem(null, ""); + @Test + void test_addSoftwareSystem_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addSoftwareSystem(null, ""); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_addSoftwareSystem_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - model.addSoftwareSystem(" ", ""); + @Test + void test_addSoftwareSystem_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addSoftwareSystem(" ", ""); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_addPerson_ThrowsAnException_WhenANullNameIsSpecified() { - model.addPerson(null, ""); + @Test + void test_addPerson_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addPerson(null, ""); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_addPerson_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - model.addPerson(" ", ""); + @Test + void test_addPerson_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addPerson(" ", ""); + }); } @Test - public void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { + void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { assertTrue(model.getSoftwareSystems().isEmpty()); SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); @@ -44,7 +52,7 @@ public void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoes } @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSameName() { + void test_addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSameName() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); @@ -57,7 +65,7 @@ public void test_addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWi } @Test - public void test_addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { + void test_addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { assertTrue(model.getSoftwareSystems().isEmpty()); SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); @@ -70,7 +78,7 @@ public void test_addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSyste } @Test - public void test_addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { + void test_addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { assertTrue(model.getPeople().isEmpty()); Person person = model.addPerson(Location.Internal, "Some internal user", "Some description"); assertEquals(1, model.getPeople().size()); @@ -83,7 +91,7 @@ public void test_addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName( } @Test - public void test_addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { + void test_addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { Person person = model.addPerson(Location.Internal, "Admin User", "Description"); assertEquals(1, model.getPeople().size()); @@ -96,7 +104,7 @@ public void test_addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() } @Test - public void test_addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNotExistWithTheSameName() { + void test_addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNotExistWithTheSameName() { assertTrue(model.getPeople().isEmpty()); Person person = model.addPerson("Some internal user", "Some description"); assertEquals(1, model.getPeople().size()); @@ -109,42 +117,42 @@ public void test_addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPerson } @Test - public void test_getElement_ReturnsNull_WhenAnElementWithTheSpecifiedIdDoesNotExist() { + void test_getElement_ReturnsNull_WhenAnElementWithTheSpecifiedIdDoesNotExist() { assertNull(model.getElement("100")); } @Test - public void test_getElement_ReturnsAnElement_WhenAnElementWithTheSpecifiedIdDoesExist() { + void test_getElement_ReturnsAnElement_WhenAnElementWithTheSpecifiedIdDoesExist() { Person person = model.addPerson(Location.Internal, "Name", "Description"); assertSame(person, model.getElement(person.getId())); } @Test - public void test_contains_ReturnsFalse_WhenTheSpecifiedElementIsNotInTheModel() { + void test_contains_ReturnsFalse_WhenTheSpecifiedElementIsNotInTheModel() { Model newModel = new Model(); SoftwareSystem softwareSystem = newModel.addSoftwareSystem(Location.Unspecified, "Name", "Description"); assertFalse(model.contains(softwareSystem)); } @Test - public void test_contains_ReturnsTrue_WhenTheSpecifiedElementIsInTheModel() { + void test_contains_ReturnsTrue_WhenTheSpecifiedElementIsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Unspecified, "Name", "Description"); assertTrue(model.contains(softwareSystem)); } @Test - public void test_getSoftwareSystemWithName_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedNameDoesNotExist() { + void test_getSoftwareSystemWithName_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedNameDoesNotExist() { assertNull(model.getSoftwareSystemWithName("System X")); } @Test - public void test_getSoftwareSystemWithName_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedNameExists() { + void test_getSoftwareSystemWithName_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedNameExists() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Description"); assertSame(softwareSystem, model.getSoftwareSystemWithName("System A")); } @Test - public void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedANullId() { + void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedANullId() { try { model.getSoftwareSystemWithId(null); fail(); @@ -154,7 +162,7 @@ public void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedANullId() { } @Test - public void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedAnEmptyId() { + void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedAnEmptyId() { try { model.getSoftwareSystemWithId(" "); fail(); @@ -164,29 +172,29 @@ public void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedAnEmptyId() } @Test - public void test_getSoftwareSystemWithId_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedIdDoesNotExist() { + void test_getSoftwareSystemWithId_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedIdDoesNotExist() { assertNull(model.getSoftwareSystemWithId("100")); } @Test - public void test_getSoftwareSystemWithId_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedIdDoesExist() { + void test_getSoftwareSystemWithId_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedIdDoesExist() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Description"); assertSame(softwareSystem, model.getSoftwareSystemWithId(softwareSystem.getId())); } @Test - public void test_getPersonWithName_ReturnsNull_WhenAPersonWithTheSpecifiedNameDoesNotExist() { + void test_getPersonWithName_ReturnsNull_WhenAPersonWithTheSpecifiedNameDoesNotExist() { assertNull(model.getPersonWithName("Admin User")); } @Test - public void test_getPersonWithName_ReturnsAPerson_WhenAPersonWithTheSpecifiedNameExists() { + void test_getPersonWithName_ReturnsAPerson_WhenAPersonWithTheSpecifiedNameExists() { Person person = model.addPerson(Location.External, "Admin User", "Description"); assertSame(person, model.getPersonWithName("Admin User")); } @Test - public void test_getRelationship_ThrowsAnException_WhenPassedANullId() { + void test_getRelationship_ThrowsAnException_WhenPassedANullId() { try { model.getRelationship(null); fail(); @@ -196,7 +204,7 @@ public void test_getRelationship_ThrowsAnException_WhenPassedANullId() { } @Test - public void test_getRelationship_ThrowsAnException_WhenPassedAnEmptyId() { + void test_getRelationship_ThrowsAnException_WhenPassedAnEmptyId() { try { model.getRelationship(" "); fail(); @@ -206,7 +214,7 @@ public void test_getRelationship_ThrowsAnException_WhenPassedAnEmptyId() { } @Test - public void test_addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnologyAndInteractionStyle() { + void test_addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnologyAndInteractionStyle() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); Relationship relationship = model.addRelationship(a, b, "Uses", "HTTPS", InteractionStyle.Asynchronous); @@ -221,7 +229,7 @@ public void test_addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAnd } @Test - public void test_addRelationship_DisallowsTheSameRelationshipToBeAddedMoreThanOnce() { + void test_addRelationship_DisallowsTheSameRelationshipToBeAddedMoreThanOnce() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship1 = element1.uses(element2, "Uses", ""); @@ -232,7 +240,7 @@ public void test_addRelationship_DisallowsTheSameRelationshipToBeAddedMoreThanOn } @Test - public void test_addRelationship_AllowsMultipleRelationshipsBetweenElements() { + void test_addRelationship_AllowsMultipleRelationshipsBetweenElements() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship1 = element1.uses(element2, "Uses in some way", ""); @@ -243,7 +251,7 @@ public void test_addRelationship_AllowsMultipleRelationshipsBetweenElements() { } @Test - public void test_addRelationship_ThrowsAnException_WhenTheDestinationIsAChildOfTheSource() { + void test_addRelationship_ThrowsAnException_WhenTheDestinationIsAChildOfTheSource() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); Component component = container.addComponent("Component", "", ""); @@ -274,7 +282,7 @@ public void test_addRelationship_ThrowsAnException_WhenTheDestinationIsAChildOfT } @Test - public void test_addRelationship_ThrowsAnException_WhenTheSourceIsAChildOfTheDestination() { + void test_addRelationship_ThrowsAnException_WhenTheSourceIsAChildOfTheDestination() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); Component component = container.addComponent("Component", "", ""); @@ -305,7 +313,7 @@ public void test_addRelationship_ThrowsAnException_WhenTheSourceIsAChildOfTheDes } @Test - public void test_modifyRelationship_ThrowsAnException_WhenARelationshipIsNotSpecified() { + void test_modifyRelationship_ThrowsAnException_WhenARelationshipIsNotSpecified() { try { model.modifyRelationship(null, "Uses", "Technology"); fail(); @@ -315,7 +323,7 @@ public void test_modifyRelationship_ThrowsAnException_WhenARelationshipIsNotSpec } @Test - public void test_modifyRelationship_ModifiesAnExistingRelationship_WhenThatRelationshipDoesNotAlreadyExist() { + void test_modifyRelationship_ModifiesAnExistingRelationship_WhenThatRelationshipDoesNotAlreadyExist() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship = element1.uses(element2, "", ""); @@ -326,7 +334,7 @@ public void test_modifyRelationship_ModifiesAnExistingRelationship_WhenThatRelat } @Test - public void test_modifyRelationship_ThrowsAnException_WhenThatRelationshipDoesAlreadyExist() { + void test_modifyRelationship_ThrowsAnException_WhenThatRelationshipDoesAlreadyExist() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship = element1.uses(element2, "Uses", "Technology"); @@ -340,7 +348,7 @@ public void test_modifyRelationship_ThrowsAnException_WhenThatRelationshipDoesAl } @Test - public void test_addSoftwareSystemInstance_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + void test_addSoftwareSystemInstance_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); deploymentNode.add((SoftwareSystem) null); @@ -351,10 +359,10 @@ public void test_addSoftwareSystemInstance_ThrowsAnException_WhenANullSoftwareSy } @Test - public void test_addContainerInstance_ThrowsAnException_WhenANullContainerIsSpecified() { + void test_addContainerInstance_ThrowsAnException_WhenANullContainerIsSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); - deploymentNode.add((Container)null); + deploymentNode.add((Container) null); fail(); } catch (Exception e) { assertEquals("A container must be specified.", e.getMessage()); @@ -362,7 +370,7 @@ public void test_addContainerInstance_ThrowsAnException_WhenANullContainerIsSpec } @Test - public void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsNotSpecified() { + void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsNotSpecified() { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); assertEquals("Deployment Node", deploymentNode.getName()); @@ -372,7 +380,7 @@ public void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmen } @Test - public void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsSpecified() { + void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsSpecified() { DeploymentNode deploymentNode = model.addDeploymentNode("Development", "Deployment Node", "Description", "Technology"); assertEquals("Deployment Node", deploymentNode.getName()); @@ -382,7 +390,7 @@ public void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmen } @Test - public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironment() { + void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironment() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); Container container1 = softwareSystem1.addContainer("Container 1", "Description", "Technology"); @@ -440,7 +448,7 @@ public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshi } @Test - public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndDefaultGroup() { + void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndDefaultGroup() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System"); Container api = softwareSystem1.addContainer("API"); Container database = softwareSystem1.addContainer("Database"); @@ -474,7 +482,7 @@ public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshi } @Test - public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroup() { + void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroup() { // in this test, container instances are added to two deployment groups: "Instance 1" and "Instance 2" // relationships are not replicated between element instances in other groups @@ -503,7 +511,7 @@ public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshi } @Test - public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroups() { + void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroups() { // in this test: // - API container instances are added to "Instance 1", "Instance 2" and "Shared" // - database container instances are added to "Instance 1" and "Instance 2" @@ -543,7 +551,7 @@ public void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshi } @Test - public void test_getElement_ThrowsAnException_WhenANullIdIsSpecified() { + void test_getElement_ThrowsAnException_WhenANullIdIsSpecified() { try { model.getElement(null); } catch (IllegalArgumentException iae) { @@ -552,7 +560,7 @@ public void test_getElement_ThrowsAnException_WhenANullIdIsSpecified() { } @Test - public void test_getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { + void test_getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { try { model.getElement(" "); } catch (IllegalArgumentException iae) { @@ -561,7 +569,7 @@ public void test_getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { } @Test - public void test_getElementWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIsSpecified() { + void test_getElementWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIsSpecified() { try { model.getElementWithCanonicalName(null); } catch (IllegalArgumentException iae) { @@ -570,7 +578,7 @@ public void test_getElementWithCanonicalName_ThrowsAnException_WhenANullCanonica } @Test - public void test_getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpecified() { + void test_getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpecified() { try { model.getElementWithCanonicalName(" "); } catch (IllegalArgumentException iae) { @@ -579,12 +587,12 @@ public void test_getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanoni } @Test - public void test_getElementWithCanonicalName_ReturnsNull_WhenAnElementWithTheSpecifiedCanonicalNameDoesNotExist() { + void test_getElementWithCanonicalName_ReturnsNull_WhenAnElementWithTheSpecifiedCanonicalNameDoesNotExist() { assertNull(model.getElementWithCanonicalName("Software System")); } @Test - public void test_getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpecifiedCanonicalNameExists() { + void test_getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpecifiedCanonicalNameExists() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Web Application", "Description", "Technology"); @@ -593,7 +601,7 @@ public void test_getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWith } @Test - public void test_addDeploymentNode_ThrowsAnException_WhenADeploymentNodeWithTheSameNameAlreadyExists() { + void test_addDeploymentNode_ThrowsAnException_WhenADeploymentNodeWithTheSameNameAlreadyExists() { model.addDeploymentNode("Amazon AWS", "Description", "Technology"); try { model.addDeploymentNode("Amazon AWS", "Description", "Technology"); @@ -604,7 +612,7 @@ public void test_addDeploymentNode_ThrowsAnException_WhenADeploymentNodeWithTheS } @Test - public void test_addDeploymentNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { + void test_addDeploymentNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addDeploymentNode("AWS Region"); try { @@ -616,7 +624,7 @@ public void test_addDeploymentNode_ThrowsAnException_WhenAChildDeploymentNodeWit } @Test - public void test_addDeploymentNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { + void test_addDeploymentNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addInfrastructureNode("Node"); try { @@ -628,7 +636,7 @@ public void test_addDeploymentNode_ThrowsAnException_WhenAChildInfrastructureNod } @Test - public void test_addInfrastructureNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { + void test_addInfrastructureNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addDeploymentNode("Node"); try { @@ -640,7 +648,7 @@ public void test_addInfrastructureNode_ThrowsAnException_WhenAChildDeploymentNod } @Test - public void test_addInfrastructureNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { + void test_addInfrastructureNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addInfrastructureNode("Node"); try { @@ -652,7 +660,7 @@ public void test_addInfrastructureNode_ThrowsAnException_WhenAChildInfrastructur } @Test - public void test_setIdGenerator_ThrowsAnException_WhenANullIdGeneratorIsSpecified() { + void test_setIdGenerator_ThrowsAnException_WhenANullIdGeneratorIsSpecified() { try { model.setIdGenerator(null); fail(); @@ -662,7 +670,7 @@ public void test_setIdGenerator_ThrowsAnException_WhenANullIdGeneratorIsSpecifie } @Test - public void test_hydrate() { + void test_hydrate() { Person person = new Person(); person.setId("1"); person.setName("Person"); @@ -742,7 +750,7 @@ public void test_hydrate() { } @Test - public void test_impliedRelationshipStrategy() { + void test_impliedRelationshipStrategy() { // default strategy initially assertTrue(model.getImpliedRelationshipsStrategy() instanceof DefaultImpliedRelationshipsStrategy); @@ -751,7 +759,7 @@ public void test_impliedRelationshipStrategy() { } @Test - public void test_setImpliedRelationshipStrategy_ResetsToTheDefaultStrategy_WhenPassedNull() { + void test_setImpliedRelationshipStrategy_ResetsToTheDefaultStrategy_WhenPassedNull() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); model.setImpliedRelationshipsStrategy(null); @@ -759,7 +767,7 @@ public void test_setImpliedRelationshipStrategy_ResetsToTheDefaultStrategy_WhenP } @Test - public void test_addSoftwareSystemInstance_AllocatesInstanceIdsPerDeploymentNode() { + void test_addSoftwareSystemInstance_AllocatesInstanceIdsPerDeploymentNode() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); DeploymentNode deploymentNodeA = model.addDeploymentNode("Deployment Node A", "", ""); DeploymentNode deploymentNodeB = model.addDeploymentNode("Deployment Node B", "", ""); @@ -775,7 +783,7 @@ public void test_addSoftwareSystemInstance_AllocatesInstanceIdsPerDeploymentNode } @Test - public void test_addContainerInstance_AllocatesInstanceIdsPerDeploymentNode() { + void test_addContainerInstance_AllocatesInstanceIdsPerDeploymentNode() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); DeploymentNode deploymentNodeA = model.addDeploymentNode("Deployment Node A", "", ""); diff --git a/structurizr-core/test/unit/com/structurizr/model/PersonTests.java b/structurizr-core/test/unit/com/structurizr/model/PersonTests.java index f51836712..6334a04f9 100644 --- a/structurizr-core/test/unit/com/structurizr/model/PersonTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/PersonTests.java @@ -1,33 +1,33 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class PersonTests extends AbstractWorkspaceTestBase { @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { Person person = model.addPerson("Person", "Description"); assertEquals("Person://Person", person.getCanonicalName()); } @Test - public void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { Person person = model.addPerson("Person", "Description"); person.setName("Name1/.Name2"); assertEquals("Person://Name1Name2", person.getCanonicalName()); } @Test - public void test_getParent_ReturnsNull() { + void test_getParent_ReturnsNull() { Person person = model.addPerson("Person", "Description"); assertNull(person.getParent()); } @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { + void test_removeTags_DoesNotRemoveRequiredTags() { Person person = model.addPerson("Person", "Description"); assertTrue(person.getTags().contains(Tags.ELEMENT)); assertTrue(person.getTags().contains(Tags.PERSON)); @@ -40,7 +40,7 @@ public void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - public void test_interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() { + void test_interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() { Person person1 = model.addPerson("Person 1", "Description"); Person person2 = model.addPerson("Person 2", "Description"); @@ -56,7 +56,7 @@ public void test_interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() } @Test - public void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { + void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { Person person1 = model.addPerson("Person 1", "Description"); Person person2 = model.addPerson("Person 2", "Description"); @@ -72,7 +72,7 @@ public void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyA } @Test - public void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { + void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { Person person1 = model.addPerson("Person 1", "Description"); Person person2 = model.addPerson("Person 2", "Description"); @@ -88,7 +88,7 @@ public void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyA } @Test - public void test_setLocation_SetsTheLocationToUnspecified_WhenNullIsPassed() { + void test_setLocation_SetsTheLocationToUnspecified_WhenNullIsPassed() { Person person = model.addPerson("Person", "Description"); person.setLocation(null); assertEquals(Location.Unspecified, person.getLocation()); diff --git a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java index da5fba715..c854ff1cb 100644 --- a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java @@ -1,42 +1,42 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class RelationshipTests extends AbstractWorkspaceTestBase { private SoftwareSystem softwareSystem1, softwareSystem2; - @Before + @BeforeEach public void setUp() { softwareSystem1 = model.addSoftwareSystem(Location.Internal, "Name1", "Description"); softwareSystem2 = model.addSoftwareSystem(Location.Internal, "Name2", "Description"); } @Test - public void test_getDescription_NeverReturnsNull() { + void test_getDescription_NeverReturnsNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, null); assertEquals("", relationship.getDescription()); } @Test - public void test_getTags_WhenThereAreNoTags() { + void test_getTags_WhenThereAreNoTags() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); assertEquals("Relationship", relationship.getTags()); } @Test - public void test_getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { + void test_getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); relationship.addTags("tag1", "tag2", "tag3"); assertEquals("Relationship,tag1,tag2,tag3", relationship.getTags()); } @Test - public void test_setTags_ClearsTheTags_WhenPassedNull() { + void test_setTags_ClearsTheTags_WhenPassedNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); relationship.addTags("Tag 1", "Tag 2"); assertEquals("Relationship,Tag 1,Tag 2", relationship.getTags()); @@ -45,9 +45,9 @@ public void test_setTags_ClearsTheTags_WhenPassedNull() { } @Test - public void test_addTags_DoesNotDoAnything_WhenPassedNull() { + void test_addTags_DoesNotDoAnything_WhenPassedNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - relationship.addTags((String)null); + relationship.addTags((String) null); assertEquals("Relationship", relationship.getTags()); relationship.addTags(null, null, null); @@ -55,20 +55,20 @@ public void test_addTags_DoesNotDoAnything_WhenPassedNull() { } @Test - public void test_addTags_AddsTags_WhenPassedSomeTags() { + void test_addTags_AddsTags_WhenPassedSomeTags() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); relationship.addTags(null, "tag1", null, "tag2"); assertEquals("Relationship,tag1,tag2", relationship.getTags()); } @Test - public void test_getInteractionStyle_ReturnsNull_WhenNotExplicitlySet() { + void test_getInteractionStyle_ReturnsNull_WhenNotExplicitlySet() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); assertNull(relationship.getInteractionStyle()); } @Test - public void test_getTags_IncludesTheInteractionStyleWhenSpecified() { + void test_getTags_IncludesTheInteractionStyleWhenSpecified() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); assertFalse(relationship.getTags().contains(Tags.SYNCHRONOUS)); assertFalse(relationship.getTags().contains(Tags.ASYNCHRONOUS)); @@ -83,20 +83,22 @@ public void test_getTags_IncludesTheInteractionStyleWhenSpecified() { } @Test - public void test_setUrl() { + void test_setUrl() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); relationship.setUrl("https://structurizr.com"); assertEquals("https://structurizr.com", relationship.getUrl()); } - @Test(expected = IllegalArgumentException.class) - public void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); - relationship.setUrl("htt://blah"); + @Test + void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); + relationship.setUrl("htt://blah"); + }); } @Test - public void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { + void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); relationship.setUrl("https://structurizr.com"); relationship.setUrl(null); @@ -104,7 +106,7 @@ public void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { } @Test - public void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { + void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); relationship.setUrl("https://structurizr.com"); relationship.setUrl(" "); @@ -112,7 +114,7 @@ public void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { } @Test - public void test_interactionStyle_CanBeSetToNull() { + void test_interactionStyle_CanBeSetToNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology", null); assertNull(relationship.getInteractionStyle()); diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java index 8930aac5e..0e7c352ec 100644 --- a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java @@ -1,9 +1,9 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class SoftwareSystemInstanceTests extends AbstractWorkspaceTestBase { @@ -11,7 +11,7 @@ public class SoftwareSystemInstanceTests extends AbstractWorkspaceTestBase { private DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @Test - public void test_construction() { + void test_construction() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertSame(softwareSystem, instance.getSoftwareSystem()); @@ -20,7 +20,7 @@ public void test_construction() { } @Test - public void test_getSoftwareSystemId() { + void test_getSoftwareSystemId() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals(softwareSystem.getId(), instance.getSoftwareSystemId()); @@ -30,7 +30,7 @@ public void test_getSoftwareSystemId() { } @Test - public void test_getName_CannotBeChanged() { + void test_getName_CannotBeChanged() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals("System", instance.getName()); @@ -40,28 +40,28 @@ public void test_getName_CannotBeChanged() { } @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals("SoftwareSystemInstance://Default/Deployment Node/System[1]", instance.getCanonicalName()); } @Test - public void test_getParent_ReturnsTheParentDeploymentNode() { + void test_getParent_ReturnsTheParentDeploymentNode() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals(deploymentNode, instance.getParent()); } @Test - public void test_getRequiredTags() { + void test_getRequiredTags() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertTrue(instance.getDefaultTags().isEmpty()); } @Test - public void test_getTags() { + void test_getTags() { softwareSystem.addTags("Tag 1"); SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); instance.addTags("Primary Instance"); @@ -70,7 +70,7 @@ public void test_getTags() { } @Test - public void test_removeTags_DoesNotRemoveAnyTags() { + void test_removeTags_DoesNotRemoveAnyTags() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertTrue(instance.getTags().contains(Tags.SOFTWARE_SYSTEM_INSTANCE)); @@ -81,7 +81,7 @@ public void test_removeTags_DoesNotRemoveAnyTags() { } @Test - public void test_addHealthCheck() { + void test_addHealthCheck() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertTrue(instance.getHealthChecks().isEmpty()); @@ -94,7 +94,7 @@ public void test_addHealthCheck() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { + void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -106,7 +106,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { + void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -118,7 +118,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { + void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -130,7 +130,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { + void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -142,7 +142,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { + void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -154,7 +154,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { + void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -166,7 +166,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero( } @Test - public void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { + void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -178,7 +178,7 @@ public void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() } @Test - public void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { + void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals(1, instance.getDeploymentGroups().size()); @@ -186,7 +186,7 @@ public void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { } @Test - public void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { + void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem, "Group 1"); assertEquals(1, instance.getDeploymentGroups().size()); @@ -194,7 +194,7 @@ public void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { } @Test - public void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { + void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem, "Group 1", "Group 2"); assertEquals(2, instance.getDeploymentGroups().size()); diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java index 9d7e020bb..415172f98 100644 --- a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java @@ -1,28 +1,32 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Iterator; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class SoftwareSystemTests extends AbstractWorkspaceTestBase { private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "Name", "Description"); - @Test(expected = IllegalArgumentException.class) - public void test_addContainer_ThrowsAnException_WhenANullNameIsSpecified() { - softwareSystem.addContainer(null, "", ""); + @Test + void test_addContainer_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + softwareSystem.addContainer(null, "", ""); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_addContainer_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - softwareSystem.addContainer(" ", "", ""); + @Test + void test_addContainer_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + softwareSystem.addContainer(" ", "", ""); + }); } @Test - public void test_addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNotExist() { + void test_addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNotExist() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertEquals("Web Application", container.getName()); assertEquals("Description", container.getDescription()); @@ -33,7 +37,7 @@ public void test_addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNo } @Test - public void test_addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlreadyExists() { + void test_addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlreadyExists() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertEquals(1, softwareSystem.getContainers().size()); @@ -46,29 +50,29 @@ public void test_addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlr } @Test - public void test_getContainerWithName_ReturnsNull_WhenAContainerWithTheSpecifiedNameDoesNotExist() { + void test_getContainerWithName_ReturnsNull_WhenAContainerWithTheSpecifiedNameDoesNotExist() { assertNull(softwareSystem.getContainerWithName("Web Application")); } @Test - public void test_GetContainerWithName_ReturnsAContainer_WhenAContainerWithTheSpecifiedNameDoesExist() { + void test_GetContainerWithName_ReturnsAContainer_WhenAContainerWithTheSpecifiedNameDoesExist() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertSame(container, softwareSystem.getContainerWithName("Web Application")); } @Test - public void test_getContainerWithId_ReturnsNull_WhenAContainerWithTheSpecifiedIdDoesNotExist() { + void test_getContainerWithId_ReturnsNull_WhenAContainerWithTheSpecifiedIdDoesNotExist() { assertNull(softwareSystem.getContainerWithId("100")); } @Test - public void test_GetContainerWithId_ReturnsAContainer_WhenAContainerWithTheSpecifiedIdDoesExist() { + void test_GetContainerWithId_ReturnsAContainer_WhenAContainerWithTheSpecifiedIdDoesExist() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertSame(container, softwareSystem.getContainerWithId(container.getId())); } @Test - public void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { + void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); systemA.uses(systemB, "Gets some data from"); @@ -82,7 +86,7 @@ public void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() } @Test - public void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADifferentRelationshipAlreadyExists() { + void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADifferentRelationshipAlreadyExists() { SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); systemA.uses(systemB, "Gets data using the REST API"); @@ -102,7 +106,7 @@ public void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_W } @Test - public void test_uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenTheSameRelationshipAlreadyExists() { + void test_uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenTheSameRelationshipAlreadyExists() { SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); systemA.uses(systemB, "Gets data using the REST API"); @@ -112,7 +116,7 @@ public void test_uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSys } @Test - public void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson() { + void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); Person person = model.addPerson(Location.Internal, "User", "Description"); system.delivers(person, "E-mails results to"); @@ -126,7 +130,7 @@ public void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemA } @Test - public void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenADifferentRelationshipAlreadyExists() { + void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenADifferentRelationshipAlreadyExists() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); Person person = model.addPerson(Location.Internal, "User", "Description"); system.delivers(person, "E-mails results to"); @@ -147,7 +151,7 @@ public void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemA } @Test - public void test_delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenTheSameRelationshipAlreadyExists() { + void test_delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenTheSameRelationshipAlreadyExists() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); Person person = model.addPerson(Location.Internal, "User", "Description"); system.delivers(person, "E-mails results to"); @@ -157,30 +161,30 @@ public void test_delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareS } @Test - public void test_getTags_IncludesSoftwareSystemByDefault() { + void test_getTags_IncludesSoftwareSystemByDefault() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); assertEquals("Element,Software System", system.getTags()); } @Test - public void test_getCanonicalName() { + void test_getCanonicalName() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); assertEquals("SoftwareSystem://System", system.getCanonicalName()); } @Test - public void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name1/.Name2", "Description"); assertEquals("SoftwareSystem://Name1Name2", softwareSystem.getCanonicalName()); } @Test - public void test_getParent_ReturnsNull() { + void test_getParent_ReturnsNull() { assertNull(softwareSystem.getParent()); } @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { + void test_removeTags_DoesNotRemoveRequiredTags() { assertTrue(softwareSystem.getTags().contains(Tags.ELEMENT)); assertTrue(softwareSystem.getTags().contains(Tags.SOFTWARE_SYSTEM)); @@ -192,7 +196,7 @@ public void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - public void test_getContainerWithName_ThrowsAnException_WhenANullNameIsSpecified() { + void test_getContainerWithName_ThrowsAnException_WhenANullNameIsSpecified() { try { softwareSystem.getContainerWithName(null); fail(); @@ -202,7 +206,7 @@ public void test_getContainerWithName_ThrowsAnException_WhenANullNameIsSpecified } @Test - public void test_getContainerWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void test_getContainerWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { try { softwareSystem.getContainerWithName(" "); fail(); @@ -212,7 +216,7 @@ public void test_getContainerWithName_ThrowsAnException_WhenAnEmptyNameIsSpecifi } @Test - public void test_getContainerWithId_ThrowsAnException_WhenANullIdIsSpecified() { + void test_getContainerWithId_ThrowsAnException_WhenANullIdIsSpecified() { try { softwareSystem.getContainerWithId(null); fail(); @@ -222,7 +226,7 @@ public void test_getContainerWithId_ThrowsAnException_WhenANullIdIsSpecified() { } @Test - public void test_getContainerWithId_ThrowsAnException_WhenAnEmptyIdIsSpecified() { + void test_getContainerWithId_ThrowsAnException_WhenAnEmptyIdIsSpecified() { try { softwareSystem.getContainerWithId(" "); fail(); diff --git a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java index 0d5ee691b..2c6c753e6 100644 --- a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java @@ -1,15 +1,15 @@ package com.structurizr.util; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.File; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ImageUtilsTests { @Test - public void test_getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + void test_getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { ImageUtils.getContentType(null); fail(); @@ -19,7 +19,7 @@ public void test_getContentType_ThrowsAnException_WhenANullFileIsSpecified() thr } @Test - public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { try { ImageUtils.getContentType(new File("../structurizr-core")); fail(); @@ -30,7 +30,7 @@ public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNot } @Test - public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { try { ImageUtils.getContentType(new File("../build.gradle")); fail(); @@ -41,7 +41,7 @@ public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNot } @Test - public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { try { ImageUtils.getContentType(new File("./foo.xml")); fail(); @@ -51,13 +51,13 @@ public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesN } @Test - public void test_getContentType_ReturnsTheContentType_WhenAFileIsSpecified() throws Exception { + void test_getContentType_ReturnsTheContentType_WhenAFileIsSpecified() throws Exception { String contentType = ImageUtils.getContentType(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); assertEquals("image/png", contentType); } @Test - public void test_getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + void test_getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { ImageUtils.getImageAsBase64(null); fail(); @@ -67,7 +67,7 @@ public void test_getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() t } @Test - public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { try { ImageUtils.getImageAsBase64(new File("../structurizr-core")); fail(); @@ -78,7 +78,7 @@ public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsN } @Test - public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { try { ImageUtils.getImageAsBase64(new File("../build.gradle")); fail(); @@ -89,7 +89,7 @@ public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsN } @Test - public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { try { ImageUtils.getImageAsBase64(new File("./foo.xml")); fail(); @@ -99,13 +99,13 @@ public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoe } @Test - public void test_getImageAsBase64_ReturnsTheImageAsABase64EncodedString_WhenAFileIsSpecified() throws Exception { + void test_getImageAsBase64_ReturnsTheImageAsABase64EncodedString_WhenAFileIsSpecified() throws Exception { String imageAsBase64 = ImageUtils.getImageAsBase64(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); assertTrue(imageAsBase64.startsWith("iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADApo5rAAA")); // the actual base64 encoded string varies between Java 8 and 9 } @Test - public void test_getImageAsDataUri_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + void test_getImageAsDataUri_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { ImageUtils.getImageAsDataUri(null); fail(); @@ -115,7 +115,7 @@ public void test_getImageAsDataUri_ThrowsAnException_WhenANullFileIsSpecified() } @Test - public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { try { ImageUtils.getImageAsDataUri(new File("../structurizr-core")); fail(); @@ -126,7 +126,7 @@ public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIs } @Test - public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { try { ImageUtils.getImageAsDataUri(new File("../build.gradle")); fail(); @@ -137,7 +137,7 @@ public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIs } @Test - public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { try { ImageUtils.getImageAsDataUri(new File("./foo.xml")); fail(); @@ -147,14 +147,14 @@ public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDo } @Test - public void test_getImageAsDataUri_ReturnsTheImageAsADataUri_WhenAFileIsSpecified() throws Exception { + void test_getImageAsDataUri_ReturnsTheImageAsADataUri_WhenAFileIsSpecified() throws Exception { String imageAsDataUri = ImageUtils.getImageAsDataUri(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); System.out.println(imageAsDataUri); assertTrue(imageAsDataUri.startsWith("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADA")); // the actual base64 encoded string varies between Java 8 and 9 } @Test - public void test_validateImage() { + void test_validateImage() { // allowed ImageUtils.validateImage("https://structurizr.com/image.png"); ImageUtils.validateImage("data:image/png;base64,iVBORw0KGg"); @@ -170,7 +170,7 @@ public void test_validateImage() { } @Test - public void test_isSupportedDataUri() { + void test_isSupportedDataUri() { assertTrue(ImageUtils.isSupportedDataUri("data:image/png;base64,iVBORw0KGg")); assertTrue(ImageUtils.isSupportedDataUri("data:image/jpeg;base64,iVBORw0KGg")); assertFalse(ImageUtils.isSupportedDataUri("data:image/svg+xml;base64,iVBORw0KGg")); diff --git a/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java index 6f7b9532c..98bf0a7f6 100644 --- a/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java @@ -1,25 +1,25 @@ package com.structurizr.util; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; public class StringUtilsTests { @Test - public void test_isNullOrEmpty_ReturnsTrue_WhenPassedNull() { + void test_isNullOrEmpty_ReturnsTrue_WhenPassedNull() { assertTrue(StringUtils.isNullOrEmpty(null)); } @Test - public void test_isNullOrEmpty_ReturnsTrue_WhenPassedAnEmptyString() { + void test_isNullOrEmpty_ReturnsTrue_WhenPassedAnEmptyString() { assertTrue(StringUtils.isNullOrEmpty("")); assertTrue(StringUtils.isNullOrEmpty(" ")); } @Test - public void test_isNullOrEmpty_ReturnsFalse_WhenPassedANonEmptyString() { + void test_isNullOrEmpty_ReturnsFalse_WhenPassedANonEmptyString() { assertFalse(StringUtils.isNullOrEmpty("Hello World!")); } diff --git a/structurizr-core/test/unit/com/structurizr/util/UrlTests.java b/structurizr-core/test/unit/com/structurizr/util/UrlTests.java index 6669463ba..70ba4f6db 100644 --- a/structurizr-core/test/unit/com/structurizr/util/UrlTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/UrlTests.java @@ -1,30 +1,30 @@ package com.structurizr.util; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; public class UrlTests { @Test - public void test_isUrl_ReturnsFalse_WhenPassedNull() { + void test_isUrl_ReturnsFalse_WhenPassedNull() { assertFalse(Url.isUrl(null)); } @Test - public void test_isUrl_ReturnsFalse_WhenPassedAnEmptyString() { + void test_isUrl_ReturnsFalse_WhenPassedAnEmptyString() { assertFalse(Url.isUrl("")); assertFalse(Url.isUrl(" ")); } @Test - public void test_isUrl_ReturnsFalse_WhenPassedAnInvalidUrl() { + void test_isUrl_ReturnsFalse_WhenPassedAnInvalidUrl() { assertFalse(Url.isUrl("www.google.com")); } @Test - public void test_isUrl_ReturnsTrue_WhenPassedAValidUrl() { + void test_isUrl_ReturnsTrue_WhenPassedAValidUrl() { assertTrue(Url.isUrl("https://www.google.com")); } diff --git a/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java b/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java index 2ae1d4e7e..cb2efd64c 100644 --- a/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java @@ -1,13 +1,13 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class AutomaticLayoutTests { @Test - public void test_setAutomaticLayout() { + void test_setAutomaticLayout() { AutomaticLayout automaticLayout = new AutomaticLayout(AutomaticLayout.Implementation.Dagre, AutomaticLayout.RankDirection.LeftRight, 100, 200, 300, true); assertEquals(AutomaticLayout.RankDirection.LeftRight, automaticLayout.getRankDirection()); @@ -18,7 +18,7 @@ public void test_setAutomaticLayout() { } @Test - public void test_setRankDirection_ThrowsAnException_WhenNullIsSpecified() { + void test_setRankDirection_ThrowsAnException_WhenNullIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setRankDirection(null); @@ -29,7 +29,7 @@ public void test_setRankDirection_ThrowsAnException_WhenNullIsSpecified() { } @Test - public void test_setRankSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void test_setRankSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setRankSeparation(-100); @@ -40,7 +40,7 @@ public void test_setRankSeparation_ThrowsAnException_WhenANegativeIntegerIsSpeci } @Test - public void test_setNodeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void test_setNodeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setNodeSeparation(-100); @@ -51,7 +51,7 @@ public void test_setNodeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpeci } @Test - public void test_setEdgeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void test_setEdgeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setEdgeSeparation(-100); diff --git a/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java b/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java index 9d07a52c5..dc01bf8b4 100644 --- a/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java @@ -1,51 +1,52 @@ package com.structurizr.view; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class BrandingTests { private Branding branding; - @Before + @BeforeEach public void setUp() { this.branding = new Branding(); } @Test - public void test_setLogo_WithAUrl() { + void test_setLogo_WithAUrl() { branding.setLogo("https://structurizr.com/static/img/structurizr-logo.png"); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", branding.getLogo()); } @Test - public void test_setLogo_WithAUrlThatHasATrailingSpace() { + void test_setLogo_WithAUrlThatHasATrailingSpace() { branding.setLogo("https://structurizr.com/static/img/structurizr-logo.png "); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", branding.getLogo()); } @Test - public void test_setLogo_WithADataUri() { + void test_setLogo_WithADataUri() { branding.setLogo("data:image/png;base64,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"); assertEquals("data:image/png;base64,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", branding.getLogo()); } - @Test(expected = IllegalArgumentException.class) - public void test_setLogo_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - branding.setLogo("htt://blah"); + @Test + void test_setLogo_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + branding.setLogo("htt://blah"); + }); } @Test - public void test_setLogo_DoesNothing_WhenANullUrlIsSpecified() { + void test_setLogo_DoesNothing_WhenANullUrlIsSpecified() { branding.setLogo(null); assertNull(branding.getLogo()); } @Test - public void test_setLogo_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void test_setLogo_DoesNothing_WhenAnEmptyUrlIsSpecified() { branding.setLogo(" "); assertNull(branding.getLogo()); } diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java index efc760b82..f85ec7794 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java @@ -1,28 +1,28 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class ColorPairTests { @Test - public void test_construction() { + void test_construction() { ColorPair colorPair = new ColorPair("#ffffff", "#000000"); assertEquals("#ffffff", colorPair.getBackground()); assertEquals("#000000", colorPair.getForeground()); } @Test - public void test_setBackground_WithAValidHtmlColorCode() { + void test_setBackground_WithAValidHtmlColorCode() { ColorPair colorPair = new ColorPair(); colorPair.setBackground("#ffffff"); assertEquals("#ffffff", colorPair.getBackground()); } @Test - public void test_setBackground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { + void test_setBackground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setBackground(null); @@ -33,7 +33,7 @@ public void test_setBackground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecifi } @Test - public void test_setBackground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { + void test_setBackground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setBackground(""); @@ -44,7 +44,7 @@ public void test_setBackground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpeci } @Test - public void test_setBackground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { + void test_setBackground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setBackground("ffffff"); @@ -55,14 +55,14 @@ public void test_setBackground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpe } @Test - public void test_setForeground_WithAValidHtmlColorCode() { + void test_setForeground_WithAValidHtmlColorCode() { ColorPair colorPair = new ColorPair(); colorPair.setForeground("#000000"); assertEquals("#000000", colorPair.getForeground()); } @Test - public void test_setForeground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { + void test_setForeground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setForeground(null); @@ -73,7 +73,7 @@ public void test_setForeground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecifi } @Test - public void test_setForeground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { + void test_setForeground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setForeground(""); @@ -84,7 +84,7 @@ public void test_setForeground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpeci } @Test - public void test_setForeground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { + void test_setForeground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setForeground("000000"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java index 074f8170d..c76263ce2 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java @@ -1,31 +1,31 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ColorTests { @Test - public void test_isHexColorCode_ReturnsFalse_WhenPassedNull() { + void test_isHexColorCode_ReturnsFalse_WhenPassedNull() { assertFalse(Color.isHexColorCode(null)); } @Test - public void test_isHexColorCode_ReturnsFalse_WhenPassedAnEmptyString() { + void test_isHexColorCode_ReturnsFalse_WhenPassedAnEmptyString() { assertFalse(Color.isHexColorCode("")); } @Test - public void test_isHexColorCode_ReturnsFalse_WhenPassedAnInvalidString() { + void test_isHexColorCode_ReturnsFalse_WhenPassedAnInvalidString() { assertFalse(Color.isHexColorCode("ffffff")); assertFalse(Color.isHexColorCode("#fffff")); assertFalse(Color.isHexColorCode("#gggggg")); } @Test - public void test_isHexColorCode_ReturnsTrue_WhenPassedAnValidString() { + void test_isHexColorCode_ReturnsTrue_WhenPassedAnValidString() { assertTrue(Color.isHexColorCode("#abcdef")); assertTrue(Color.isHexColorCode("#ABCDEF")); assertTrue(Color.isHexColorCode("#123456")); diff --git a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java index 0527ad3e4..eaeeb4300 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java @@ -2,13 +2,13 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.Set; import java.util.stream.Collectors; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ComponentViewTests extends AbstractWorkspaceTestBase { @@ -16,7 +16,7 @@ public class ComponentViewTests extends AbstractWorkspaceTestBase { private Container webApplication; private ComponentView view; - @Before + @BeforeEach public void setUp() { softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); @@ -24,7 +24,7 @@ public void setUp() { } @Test - public void test_construction() { + void test_construction() { assertEquals("The System - Web Application - Components", view.getName()); assertEquals("Some description", view.getDescription()); assertEquals(0, view.getElements().size()); @@ -35,14 +35,14 @@ public void test_construction() { } @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { assertEquals(0, view.getElements().size()); view.addAllSoftwareSystems(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -54,14 +54,14 @@ public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSo } @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { assertEquals(0, view.getElements().size()); view.addAllPeople(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson(Location.External, "User A", "Description"); Person userB = model.addPerson(Location.External, "User B", "Description"); @@ -73,14 +73,14 @@ public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { assertEquals(0, view.getElements().size()); view.addAllElements(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndComponents_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersAndComponentsInTheModel() { + void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndComponents_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersAndComponentsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); Person userA = model.addPerson(Location.External, "User A", "Description"); @@ -102,14 +102,14 @@ public void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndC } @Test - public void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { + void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { assertEquals(0, view.getElements().size()); view.addAllContainers(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { + void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); @@ -121,14 +121,14 @@ public void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() } @Test - public void test_addAllComponents_DoesNothing_WhenThereAreNoComponents() { + void test_addAllComponents_DoesNothing_WhenThereAreNoComponents() { assertEquals(0, view.getElements().size()); view.addAllComponents(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() { + void test_addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); @@ -140,7 +140,7 @@ public void test_addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() } @Test - public void test_add_ThrowsAnException_WhenANullContainerIsSpecified() { + void test_add_ThrowsAnException_WhenANullContainerIsSpecified() { assertEquals(0, view.getElements().size()); try { @@ -152,7 +152,7 @@ public void test_add_ThrowsAnException_WhenANullContainerIsSpecified() { } @Test - public void test_add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { + void test_add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); assertEquals(0, view.getElements().size()); @@ -162,7 +162,7 @@ public void test_add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { } @Test - public void test_add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { + void test_add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); view.add(database); assertEquals(1, view.getElements().size()); @@ -173,9 +173,9 @@ public void test_add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { } @Test - public void test_remove_ThrowsAndException_WhenANullContainerIsPassed() { + void test_remove_ThrowsAndException_WhenANullContainerIsPassed() { try { - view.remove((Container)null); + view.remove((Container) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("An element must be specified.", iae.getMessage()); @@ -183,7 +183,7 @@ public void test_remove_ThrowsAndException_WhenANullContainerIsPassed() { } @Test - public void test_remove_RemovesTheContainer_WhenTheContainerIsInTheView() { + void test_remove_RemovesTheContainer_WhenTheContainerIsInTheView() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); view.add(database); assertEquals(1, view.getElements().size()); @@ -194,7 +194,7 @@ public void test_remove_RemovesTheContainer_WhenTheContainerIsInTheView() { } @Test - public void test_remove_DoesNothing_WhenTheContainerIsNotInTheView() { + void test_remove_DoesNothing_WhenTheContainerIsNotInTheView() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); @@ -208,14 +208,14 @@ public void test_remove_DoesNothing_WhenTheContainerIsNotInTheView() { } @Test - public void test_add_DoesNothing_WhenANullComponentIsSpecified() { + void test_add_DoesNothing_WhenANullComponentIsSpecified() { assertEquals(0, view.getElements().size()); view.add((Component) null); assertEquals(0, view.getElements().size()); } @Test - public void test_add_AddsTheComponent_WhenTheComponentIsNotInTheViewAlready() { + void test_add_AddsTheComponent_WhenTheComponentIsNotInTheViewAlready() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); assertEquals(0, view.getElements().size()); @@ -225,7 +225,7 @@ public void test_add_AddsTheComponent_WhenTheComponentIsNotInTheViewAlready() { } @Test - public void test_add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { + void test_add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); view.add(componentA); assertEquals(1, view.getElements().size()); @@ -236,7 +236,7 @@ public void test_add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { } @Test - public void test_add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentContainer() { + void test_add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentContainer() { try { SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); @@ -253,7 +253,7 @@ public void test_add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentCo } @Test - public void test_add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { + void test_add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { try { view.add(webApplication); fail(); @@ -263,20 +263,20 @@ public void test_add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { } @Test - public void test_add_DoesNothing_WhenTheContainerOfTheViewIsAddedViaDependency() { + void test_add_DoesNothing_WhenTheContainerOfTheViewIsAddedViaDependency() { final SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "Some other system", "external system that uses our web application"); final Relationship relationshipFromExternalSystem = softwareSystem.uses(webApplication, ""); - assertEquals("the container itself is not added to the view", 0, view.getElements().stream().map(e -> e.getElement()).filter(e -> e.equals(webApplication)).count()); + assertEquals(0, view.getElements().stream().map(e -> e.getElement()).filter(e -> e.equals(webApplication)).count(), "the container itself is not added to the view"); view.add(relationshipFromExternalSystem); - assertEquals("the container itself is not added to the view", 0, view.getElements().stream().map(e -> e.getElement()).filter(e -> e.equals(webApplication)).count()); + assertEquals(0, view.getElements().stream().map(e -> e.getElement()).filter(e -> e.equals(webApplication)).count(), "the container itself is not added to the view"); } @Test - public void test_remove_DoesNothing_WhenANullComponentIsPassed() { + void test_remove_DoesNothing_WhenANullComponentIsPassed() { try { - view.remove((Component)null); + view.remove((Component) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("An element must be specified.", iae.getMessage()); @@ -284,7 +284,7 @@ public void test_remove_DoesNothing_WhenANullComponentIsPassed() { } @Test - public void test_remove_RemovesTheComponent_WhenTheComponentIsInTheView() { + void test_remove_RemovesTheComponent_WhenTheComponentIsInTheView() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); view.add(componentA); assertEquals(1, view.getElements().size()); @@ -295,7 +295,7 @@ public void test_remove_RemovesTheComponent_WhenTheComponentIsInTheView() { } @Test - public void test_remove_RemovesTheComponentAndRelationships_WhenTheComponentIsInTheViewAndHasArelationshipToAnotherElement() { + void test_remove_RemovesTheComponentAndRelationships_WhenTheComponentIsInTheViewAndHasArelationshipToAnotherElement() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); componentA.uses(componentB, "uses"); @@ -311,7 +311,7 @@ public void test_remove_RemovesTheComponentAndRelationships_WhenTheComponentIsIn } @Test - public void test_remove_DoesNothing_WhenTheComponentIsNotInTheView() { + void test_remove_DoesNothing_WhenTheComponentIsNotInTheView() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); @@ -325,14 +325,14 @@ public void test_remove_DoesNothing_WhenTheComponentIsNotInTheView() { } @Test - public void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { + void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { view.addNearestNeighbours(null); assertEquals(0, view.getElements().size()); } @Test - public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { Component component = webApplication.addComponent("Component", "", ""); view.add(component); assertEquals(1, view.getElements().size()); @@ -342,7 +342,7 @@ public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { } @Test - public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); Person userA = model.addPerson("User A", "Description"); @@ -400,7 +400,7 @@ public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNear } @Test - public void test_addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDirectRelationshipsWithAComponent() { + void test_addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDirectRelationshipsWithAComponent() { SoftwareSystem source = model.addSoftwareSystem("Source", ""); SoftwareSystem destination = model.addSoftwareSystem("Destination", ""); @@ -424,7 +424,7 @@ public void test_addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDire } @Test - public void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipToAContainerInTheSameSoftwareSystem() { + void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipToAContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -441,7 +441,7 @@ public void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARela } @Test - public void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipFromAContainerInTheSameSoftwareSystem() { + void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipFromAContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -458,7 +458,7 @@ public void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARela } @Test - public void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipToAComponentInADifferentContainerInTheSameSoftwareSystem() { + void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipToAComponentInADifferentContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -476,7 +476,7 @@ public void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHa } @Test - public void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipFromAComponentInADifferentContainerInTheSameSoftwareSystem() { + void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipFromAComponentInADifferentContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -494,7 +494,7 @@ public void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHa } @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAContainerInAnotherSoftwareSystem() { + void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAContainerInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -512,7 +512,7 @@ public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenACompon } @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAContainerInAnotherSoftwareSystem() { + void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAContainerInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -530,7 +530,7 @@ public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenACompon } @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAComponentInAnotherSoftwareSystem() { + void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAComponentInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -549,7 +549,7 @@ public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenACompon } @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAComponentInAnotherSoftwareSystem() { + void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAComponentInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -568,7 +568,7 @@ public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenACompon } @Test - public void test_addDefaultElements() { + void test_addDefaultElements() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); CustomElement element = model.addCustomElement("Custom"); @@ -615,7 +615,7 @@ public void test_addDefaultElements() { } @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { + void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); @@ -629,7 +629,7 @@ public void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheS } @Test - public void test_addContainer_ThrowsAnException_WhenTheContainerIsTheScopeOfTheView() { + void test_addContainer_ThrowsAnException_WhenTheContainerIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); @@ -643,7 +643,7 @@ public void test_addContainer_ThrowsAnException_WhenTheContainerIsTheScopeOfTheV } @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { + void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -665,7 +665,7 @@ public void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlread } @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { + void test_addSoftwareSystem_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -687,7 +687,7 @@ public void test_addSoftwareSystem_ThrowsAnException_WhenAChildComponentIsAlread } @Test - public void test_addContainer_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { + void test_addContainer_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -709,7 +709,7 @@ public void test_addContainer_ThrowsAnException_WhenAChildComponentIsAlreadyAdde } @Test - public void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -731,7 +731,7 @@ public void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { } @Test - public void test_addComponent_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + void test_addComponent_ThrowsAnException_WhenTheParentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java index c7e0ac2aa..ac004de8e 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java @@ -1,22 +1,21 @@ package com.structurizr.view; import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class ConfigurationTests extends AbstractWorkspaceTestBase { @Test - public void test_defaultView_DoesNothing_WhenPassedNull() { + void test_defaultView_DoesNothing_WhenPassedNull() { Configuration configuration = new Configuration(); - configuration.setDefaultView((View)null); + configuration.setDefaultView((View) null); assertNull(configuration.getDefaultView()); } @Test - public void test_defaultView() { + void test_defaultView() { SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); Configuration configuration = new Configuration(); configuration.setDefaultView(view); @@ -24,7 +23,7 @@ public void test_defaultView() { } @Test - public void test_copyConfigurationFrom() { + void test_copyConfigurationFrom() { Configuration source = new Configuration(); source.setLastSavedView("someKey"); @@ -34,34 +33,36 @@ public void test_copyConfigurationFrom() { } @Test - public void test_setTheme_WithAUrl() { + void test_setTheme_WithAUrl() { Configuration configuration = new Configuration(); configuration.setTheme("https://example.com/theme.json"); assertEquals("https://example.com/theme.json", configuration.getTheme()); } @Test - public void test_setTheme_WithAUrlThatHasATrailingSpace() { + void test_setTheme_WithAUrlThatHasATrailingSpace() { Configuration configuration = new Configuration(); configuration.setTheme("https://example.com/theme.json "); assertEquals("https://example.com/theme.json", configuration.getTheme()); } - @Test(expected = IllegalArgumentException.class) - public void test_setTheme_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - Configuration configuration = new Configuration(); - configuration.setTheme("htt://blah"); + @Test + void test_setTheme_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + Configuration configuration = new Configuration(); + configuration.setTheme("htt://blah"); + }); } @Test - public void test_setTheme_DoesNothing_WhenANullUrlIsSpecified() { + void test_setTheme_DoesNothing_WhenANullUrlIsSpecified() { Configuration configuration = new Configuration(); configuration.setTheme(null); assertNull(configuration.getTheme()); } @Test - public void test_setTheme_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void test_setTheme_DoesNothing_WhenAnEmptyUrlIsSpecified() { Configuration configuration = new Configuration(); configuration.setTheme(" "); assertNull(configuration.getTheme()); diff --git a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java index 8f41c6886..801e481bd 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java @@ -3,27 +3,27 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.Workspace; import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ContainerViewTests extends AbstractWorkspaceTestBase { private SoftwareSystem softwareSystem; private ContainerView view; - @Before + @BeforeEach public void setUp() { softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); view = new ContainerView(softwareSystem, "containers", "Description"); } @Test - public void test_construction() { + void test_construction() { assertEquals("The System - Containers", view.getName()); assertEquals("Description", view.getDescription()); assertEquals(0, view.getElements().size()); @@ -33,14 +33,14 @@ public void test_construction() { } @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { assertEquals(0, view.getElements().size()); view.addAllSoftwareSystems(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -52,14 +52,14 @@ public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSo } @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { assertEquals(0, view.getElements().size()); view.addAllPeople(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson(Location.External, "User A", "Description"); Person userB = model.addPerson(Location.External, "User B", "Description"); @@ -71,14 +71,14 @@ public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { assertEquals(0, view.getElements().size()); view.addAllElements(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersInTheModel() { + void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); Person userA = model.addPerson(Location.External, "User A", "Description"); @@ -98,14 +98,14 @@ public void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_Whe } @Test - public void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { + void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { assertEquals(0, view.getElements().size()); view.addAllContainers(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { + void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { Container webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); Container database = softwareSystem.addContainer("Database", "Does something", "MySQL"); @@ -117,21 +117,21 @@ public void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() } @Test - public void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { + void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { view.addNearestNeighbours(null); assertEquals(0, view.getElements().size()); } @Test - public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { view.addNearestNeighbours(softwareSystem); assertEquals(0, view.getElements().size()); } @Test - public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); Person userA = model.addPerson("User A", "Description"); @@ -190,7 +190,7 @@ public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNear } @Test - public void test_remove_RemovesContainer() { + void test_remove_RemovesContainer() { Container webApplication = softwareSystem.addContainer("Web Application", "", ""); Container database = softwareSystem.addContainer("Database", "", ""); @@ -203,7 +203,7 @@ public void test_remove_RemovesContainer() { } @Test - public void test_remove_ElementsWithTag() { + void test_remove_ElementsWithTag() { final String TAG = "myTag"; Container webApplication = softwareSystem.addContainer("Web Application", "", ""); Container database = softwareSystem.addContainer("Database", "", ""); @@ -218,7 +218,7 @@ public void test_remove_ElementsWithTag() { } @Test - public void test_remove_RelationshipWithTag() { + void test_remove_RelationshipWithTag() { final String TAG = "myTag"; Container webApplication = softwareSystem.addContainer("Web Application", "", ""); Container database = softwareSystem.addContainer("Database", "", ""); @@ -234,7 +234,7 @@ public void test_remove_RelationshipWithTag() { } @Test - public void test_addDependentSoftwareSystem() { + void test_addDependentSoftwareSystem() { assertEquals(0, view.getElements().size()); assertEquals(0, view.getRelationships().size()); @@ -253,7 +253,7 @@ public void test_addDependentSoftwareSystem() { } @Test - public void test_addDependentSoftwareSystem2() { + void test_addDependentSoftwareSystem2() { Container container1a = softwareSystem.addContainer("Container 1A", "", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem(Location.External, "SoftwareSystem 2", ""); @@ -270,7 +270,7 @@ public void test_addDependentSoftwareSystem2() { } @Test - public void test_addDefaultElements() { + void test_addDefaultElements() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); CustomElement element = model.addCustomElement("Custom"); @@ -311,7 +311,7 @@ public void test_addDefaultElements() { } @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { + void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); view = new ContainerView(softwareSystem, "containers", "Description"); @@ -324,7 +324,7 @@ public void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheS } @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { + void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -345,7 +345,7 @@ public void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlread } @Test - public void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); diff --git a/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java b/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java index e09e4462d..26e329dfd 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java @@ -3,14 +3,14 @@ import com.structurizr.Workspace; import com.structurizr.model.Container; import com.structurizr.model.SoftwareSystem; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class DefaultLayoutMergeStrategyTests { @Test - public void test_copyLayoutInformation_WhenCanonicalNamesHaveNotChanged() { + void test_copyLayoutInformation_WhenCanonicalNamesHaveNotChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "", ""); @@ -33,7 +33,7 @@ public void test_copyLayoutInformation_WhenCanonicalNamesHaveNotChanged() { } @Test - public void test_copyLayoutInformation_WhenAParentElementNameHasChanged() { + void test_copyLayoutInformation_WhenAParentElementNameHasChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "", ""); @@ -56,7 +56,7 @@ public void test_copyLayoutInformation_WhenAParentElementNameHasChanged() { } @Test - public void test_copyLayoutInformation_WhenAnElementNameHasChangedButTheDescriptionHasNotChanged() { + void test_copyLayoutInformation_WhenAnElementNameHasChangedButTheDescriptionHasNotChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); @@ -79,7 +79,7 @@ public void test_copyLayoutInformation_WhenAnElementNameHasChangedButTheDescript } @Test - public void test_copyLayoutInformation_WhenAnElementNameAndDescriptionHaveChangedButTheIdHasNotChanged() { + void test_copyLayoutInformation_WhenAnElementNameAndDescriptionHaveChangedButTheIdHasNotChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); @@ -102,7 +102,7 @@ public void test_copyLayoutInformation_WhenAnElementNameAndDescriptionHaveChange } @Test - public void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChanged() { + void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); @@ -126,7 +126,7 @@ public void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveC } @Test - public void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChangedAndDescriptionWasNull() { + void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChangedAndDescriptionWasNull() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container"); @@ -151,7 +151,7 @@ public void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveC } @Test - public void test_copyLayoutInformation_DoesNotThrowAnExceptionWhenAddingAnElementToAView() { + void test_copyLayoutInformation_DoesNotThrowAnExceptionWhenAddingAnElementToAView() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1A = workspace1.getModel().addSoftwareSystem("Software System A"); SoftwareSystem softwareSystem1B = workspace1.getModel().addSoftwareSystem("Software System B"); diff --git a/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java b/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java index 7d35e9220..36394293b 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java @@ -2,41 +2,41 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class DeploymentViewTests extends AbstractWorkspaceTestBase { private DeploymentView deploymentView; - @Before + @BeforeEach public void setup() { } @Test - public void test_getName_WithNoSoftwareSystemAndNoEnvironment() { + void test_getName_WithNoSoftwareSystemAndNoEnvironment() { deploymentView = views.createDeploymentView("deployment", "Description"); assertEquals("Deployment - Default", deploymentView.getName()); } @Test - public void test_getName_WithNoSoftwareSystemAndAnEnvironment() { + void test_getName_WithNoSoftwareSystemAndAnEnvironment() { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.setEnvironment("Live"); assertEquals("Deployment - Live", deploymentView.getName()); } @Test - public void test_getName_WithASoftwareSystemAndNoEnvironment() { + void test_getName_WithASoftwareSystemAndNoEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); assertEquals("Software System - Deployment - Default", deploymentView.getName()); } @Test - public void test_getName_WithASoftwareSystemAndAnEnvironment() { + void test_getName_WithASoftwareSystemAndAnEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); deploymentView.setEnvironment("Live"); @@ -44,10 +44,10 @@ public void test_getName_WithASoftwareSystemAndAnEnvironment() { } @Test - public void test_addDeploymentNode_ThrowsAnException_WhenPassedNull() { + void test_addDeploymentNode_ThrowsAnException_WhenPassedNull() { try { deploymentView = views.createDeploymentView("key", "Description"); - deploymentView.add((DeploymentNode)null); + deploymentView.add((DeploymentNode) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("A deployment node must be specified.", iae.getMessage()); @@ -55,10 +55,10 @@ public void test_addDeploymentNode_ThrowsAnException_WhenPassedNull() { } @Test - public void test_addRelationship_ThrowsAnException_WhenPassedNull() { + void test_addRelationship_ThrowsAnException_WhenPassedNull() { try { deploymentView = views.createDeploymentView("key", "Description"); - deploymentView.add((Relationship)null); + deploymentView.add((Relationship) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("A relationship must be specified.", iae.getMessage()); @@ -66,7 +66,7 @@ public void test_addRelationship_ThrowsAnException_WhenPassedNull() { } @Test - public void test_addAllDeploymentNodes_DoesNothing_WhenThereAreNoTopLevelDeploymentNodes() { + void test_addAllDeploymentNodes_DoesNothing_WhenThereAreNoTopLevelDeploymentNodes() { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.addAllDeploymentNodes(); @@ -74,7 +74,7 @@ public void test_addAllDeploymentNodes_DoesNothing_WhenThereAreNoTopLevelDeploym } @Test - public void test_addAllDeploymentNodes_DoesNothing_WhenThereAreTopLevelDeploymentNodesButNoContainerInstances() { + void test_addAllDeploymentNodes_DoesNothing_WhenThereAreTopLevelDeploymentNodesButNoContainerInstances() { deploymentView = views.createDeploymentView("deployment", "Description"); model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -83,7 +83,7 @@ public void test_addAllDeploymentNodes_DoesNothing_WhenThereAreTopLevelDeploymen } @Test - public void test_addAllDeploymentNodes_DoesNothing_WhenThereNoDeploymentNodesForTheDeploymentEnvironment() { + void test_addAllDeploymentNodes_DoesNothing_WhenThereNoDeploymentNodesForTheDeploymentEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -96,7 +96,7 @@ public void test_addAllDeploymentNodes_DoesNothing_WhenThereNoDeploymentNodesFor } @Test - public void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreTopLevelDeploymentNodesWithContainerInstances() { + void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreTopLevelDeploymentNodesWithContainerInstances() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -110,7 +110,7 @@ public void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_ } @Test - public void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreChildDeploymentNodesWithContainerInstances() { + void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreChildDeploymentNodesWithContainerInstances() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -126,7 +126,7 @@ public void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_ } @Test - public void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstancesOnlyForTheSoftwareSystemInScope() { + void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstancesOnlyForTheSoftwareSystemInScope() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", ""); Container container1 = softwareSystem1.addContainer("Container 1", "Description", "Technology"); DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); @@ -149,7 +149,7 @@ public void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstancesO } @Test - public void test_addDeploymentNode_AddsTheParentToo() { + void test_addDeploymentNode_AddsTheParentToo() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -165,7 +165,7 @@ public void test_addDeploymentNode_AddsTheParentToo() { } @Test - public void test_addDeploymentNode_ThrowsAnException_WhenAddingADeploymentNodeFromAnotherDeploymentEnvironment() { + void test_addDeploymentNode_ThrowsAnException_WhenAddingADeploymentNodeFromAnotherDeploymentEnvironment() { DeploymentNode devDeploymentNode = model.addDeploymentNode("Dev", "Deployment Node", "Description", "Technology"); devDeploymentNode.addInfrastructureNode("Load Balancer"); DeploymentNode liveDeploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); @@ -184,7 +184,7 @@ public void test_addDeploymentNode_ThrowsAnException_WhenAddingADeploymentNodeFr } @Test - public void test_addSoftwareSystemInstance_ThrowsAnException_WhenTheSoftwareSystemInstanceIsTheSoftwareSystemInScope() { + void test_addSoftwareSystemInstance_ThrowsAnException_WhenTheSoftwareSystemInstanceIsTheSoftwareSystemInScope() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); @@ -198,7 +198,7 @@ public void test_addSoftwareSystemInstance_ThrowsAnException_WhenTheSoftwareSyst } @Test - public void test_addSoftwareSystemInstance_DoesNotAddTheSoftwareSystemInstance_WhenAChildContainerInstanceHasAlreadyBeenAdded() { + void test_addSoftwareSystemInstance_DoesNotAddTheSoftwareSystemInstance_WhenAChildContainerInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); @@ -217,7 +217,7 @@ public void test_addSoftwareSystemInstance_DoesNotAddTheSoftwareSystemInstance_W } @Test - public void test_addContainerInstance_DoesNotAddTheContainerInstance_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { + void test_addContainerInstance_DoesNotAddTheContainerInstance_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); @@ -236,10 +236,10 @@ public void test_addContainerInstance_DoesNotAddTheContainerInstance_WhenThePare } @Test - public void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesAreSpecified() { + void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesAreSpecified() { try { deploymentView = views.createDeploymentView("deployment", "Description"); - deploymentView.addAnimation((ContainerInstance[])null); + deploymentView.addAnimation((ContainerInstance[]) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("One or more software system/container instances must be specified.", iae.getMessage()); @@ -247,10 +247,10 @@ public void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesAreSpe } @Test - public void test_addAnimationStep_ThrowsAnException_WhenNoInfrastructureNodesAreSpecified() { + void test_addAnimationStep_ThrowsAnException_WhenNoInfrastructureNodesAreSpecified() { try { deploymentView = views.createDeploymentView("deployment", "Description"); - deploymentView.addAnimation((InfrastructureNode[])null); + deploymentView.addAnimation((InfrastructureNode[]) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("One or more infrastructure nodes must be specified.", iae.getMessage()); @@ -258,7 +258,7 @@ public void test_addAnimationStep_ThrowsAnException_WhenNoInfrastructureNodesAre } @Test - public void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesOrInfrastructureNodesAreSpecified() { + void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesOrInfrastructureNodesAreSpecified() { try { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.addAnimation(null, null); @@ -269,7 +269,7 @@ public void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesOrInfr } @Test - public void test_addAnimationStep() { + void test_addAnimationStep() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); Container database = softwareSystem.addContainer("Database", "Description", "Technology"); @@ -303,7 +303,7 @@ public void test_addAnimationStep() { } @Test - public void test_addAnimationStep_IgnoresContainerInstancesThatDoNotExistInTheView() { + void test_addAnimationStep_IgnoresContainerInstancesThatDoNotExistInTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); Container database = softwareSystem.addContainer("Database", "Description", "Technology"); @@ -329,7 +329,7 @@ public void test_addAnimationStep_IgnoresContainerInstancesThatDoNotExistInTheVi } @Test - public void test_addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpecifiedButNoneOfThemExistInTheView() { + void test_addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpecifiedButNoneOfThemExistInTheView() { try { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); @@ -352,7 +352,7 @@ public void test_addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpe } @Test - public void test_remove_RemovesTheInfrastructureNode() { + void test_remove_RemovesTheInfrastructureNode() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -372,7 +372,7 @@ public void test_remove_RemovesTheInfrastructureNode() { } @Test - public void test_remove_RemovesTheSoftwareSystemInstance() { + void test_remove_RemovesTheSoftwareSystemInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -391,7 +391,7 @@ public void test_remove_RemovesTheSoftwareSystemInstance() { } @Test - public void test_remove_RemovesTheContainerInstance() { + void test_remove_RemovesTheContainerInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -411,7 +411,7 @@ public void test_remove_RemovesTheContainerInstance() { } @Test - public void test_remove_RemovesTheDeploymentNodeAndChildren() { + void test_remove_RemovesTheDeploymentNodeAndChildren() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -429,7 +429,7 @@ public void test_remove_RemovesTheDeploymentNodeAndChildren() { } @Test - public void test_remove_RemovesTheChildDeploymentNodeAndChildren() { + void test_remove_RemovesTheChildDeploymentNodeAndChildren() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -446,7 +446,7 @@ public void test_remove_RemovesTheChildDeploymentNodeAndChildren() { } @Test - public void test_add_AddsTheInfrastructureNode() { + void test_add_AddsTheInfrastructureNode() { DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); InfrastructureNode infrastructureNode1 = deploymentNodeChild.addInfrastructureNode("Infrastructure Node 1"); @@ -462,7 +462,7 @@ public void test_add_AddsTheInfrastructureNode() { } @Test - public void test_add_AddsTheSoftwareSystemInstance() { + void test_add_AddsTheSoftwareSystemInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -479,7 +479,7 @@ public void test_add_AddsTheSoftwareSystemInstance() { } @Test - public void test_addSoftwareSystemInstance_ThrowsAnException_WhenAChildContainerInstanceHasAlreadyBeenAdded() { + void test_addSoftwareSystemInstance_ThrowsAnException_WhenAChildContainerInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -499,7 +499,7 @@ public void test_addSoftwareSystemInstance_ThrowsAnException_WhenAChildContainer } @Test - public void test_add_AddsTheContainerInstance() { + void test_add_AddsTheContainerInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -517,7 +517,7 @@ public void test_add_AddsTheContainerInstance() { } @Test - public void test_addContainerInstance_ThrowsAnException_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { + void test_addContainerInstance_ThrowsAnException_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); diff --git a/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java b/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java index fa2f7ff0b..33758c0f2 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java @@ -1,13 +1,14 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class DimensionsTests { @Test - public void test_construction() { + void test_construction() { Dimensions dimensions = new Dimensions(123, 456); assertEquals(123, dimensions.getWidth()); @@ -15,7 +16,7 @@ public void test_construction() { } @Test - public void test_setWidth_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void test_setWidth_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { Dimensions dimensions = new Dimensions(); dimensions.setWidth(-100); @@ -26,7 +27,7 @@ public void test_setWidth_ThrowsAnException_WhenANegativeIntegerIsSpecified() { } @Test - public void test_setHeight_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void test_setHeight_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { Dimensions dimensions = new Dimensions(); dimensions.setHeight(-100); diff --git a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java index 065c0befc..81c98cbeb 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java @@ -3,14 +3,14 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.Workspace; import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class DynamicViewTests extends AbstractWorkspaceTestBase { @@ -28,7 +28,7 @@ public class DynamicViewTests extends AbstractWorkspaceTestBase { private Relationship relationship; - @Before + @BeforeEach public void setup() { person = model.addPerson("Person", ""); softwareSystemA = model.addSoftwareSystem("Software System A", ""); @@ -44,7 +44,7 @@ public void setup() { } @Test - public void test_add_ThrowsAnException_WhenPassedANullSourceElement() { + void test_add_ThrowsAnException_WhenPassedANullSourceElement() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(null, softwareSystemA); @@ -55,7 +55,7 @@ public void test_add_ThrowsAnException_WhenPassedANullSourceElement() { } @Test - public void test_add_ThrowsAnException_WhenPassedANullDestinationElement() { + void test_add_ThrowsAnException_WhenPassedANullDestinationElement() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(person, null); @@ -66,7 +66,7 @@ public void test_add_ThrowsAnException_WhenPassedANullDestinationElement() { } @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAContainerIsAdded() { + void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAContainerIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(containerA1, containerA1); @@ -77,7 +77,7 @@ public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifie } @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAComponentIsAdded() { + void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAComponentIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(componentA1, componentA1); @@ -88,7 +88,7 @@ public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifie } @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemButAComponentIsAdded() { + void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemButAComponentIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); dynamicView.add(componentA1, containerA1); @@ -99,7 +99,7 @@ public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSy } @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemAndTheSameSoftwareSystemIsAdded() { + void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemAndTheSameSoftwareSystemIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); dynamicView.add(softwareSystemA, containerA1); @@ -110,7 +110,7 @@ public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSy } @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheSameContainerIsAdded() { + void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheSameContainerIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); dynamicView.add(containerA1, containerA2); @@ -121,7 +121,7 @@ public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerA } @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheParentSoftwareSystemIsAdded() { + void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheParentSoftwareSystemIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); dynamicView.add(softwareSystemA, containerA2); @@ -132,7 +132,7 @@ public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerA } @Test - public void test_add_ThrowsAnException_WhenTheParentOfAnElementHasAlreadyBeenAdded() { + void test_add_ThrowsAnException_WhenTheParentOfAnElementHasAlreadyBeenAdded() { try { SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); Container container1 = softwareSystem.addContainer("Container 1", "", ""); @@ -154,7 +154,7 @@ public void test_add_ThrowsAnException_WhenTheParentOfAnElementHasAlreadyBeenAdd } @Test - public void test_add_ThrowsAnException_WhenTheChildOfAnElementHasAlreadyBeenAdded() { + void test_add_ThrowsAnException_WhenTheChildOfAnElementHasAlreadyBeenAdded() { try { SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); Container container1 = softwareSystem.addContainer("Container 1", "", ""); @@ -176,7 +176,7 @@ public void test_add_ThrowsAnException_WhenTheChildOfAnElementHasAlreadyBeenAdde } @Test - public void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsDoesNotExist() { + void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsDoesNotExist() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); SoftwareSystem ss1 = workspace.getModel().addSoftwareSystem("Software System 1", ""); @@ -189,7 +189,7 @@ public void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDesti } @Test - public void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsWithTheSpecifiedTechnologyDoesNotExist() { + void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsWithTheSpecifiedTechnologyDoesNotExist() { try { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -209,7 +209,7 @@ public void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDesti } @Test - public void test_addRelationshipWithOriginalDescription() { + void test_addRelationshipWithOriginalDescription() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -226,7 +226,7 @@ public void test_addRelationshipWithOriginalDescription() { } @Test - public void test_addRelationshipWithOveriddenDescription() { + void test_addRelationshipWithOveriddenDescription() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -243,14 +243,14 @@ public void test_addRelationshipWithOveriddenDescription() { } @Test - public void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExists() { + void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExists() { final DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); dynamicView.add(containerA1, containerA2); assertEquals(2, dynamicView.getElements().size()); } @Test - public void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExistsAndTheDestinationIsAnExternalSoftwareSystem() { + void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExistsAndTheDestinationIsAnExternalSoftwareSystem() { DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); containerA2.uses(softwareSystemB, "", ""); dynamicView.add(containerA2, softwareSystemB); @@ -258,7 +258,7 @@ public void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetwee } @Test - public void test_normalSequence() { + void test_normalSequence() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -280,7 +280,7 @@ public void test_normalSequence() { } @Test - public void test_normalSequence_WhenThereAreMultipleDescriptions() { + void test_normalSequence_WhenThereAreMultipleDescriptions() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -300,7 +300,7 @@ public void test_normalSequence_WhenThereAreMultipleDescriptions() { } @Test - public void test_normalSequence_WhenThereAreMultipleTechnologies() { + void test_normalSequence_WhenThereAreMultipleTechnologies() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -320,7 +320,7 @@ public void test_normalSequence_WhenThereAreMultipleTechnologies() { } @Test - public void test_parallelSequence() { + void test_parallelSequence() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", ""); @@ -359,7 +359,7 @@ public void test_parallelSequence() { } @Test - public void test_getRelationships_WhenTheOrderPropertyIsAnInteger() { + void test_getRelationships_WhenTheOrderPropertyIsAnInteger() { containerA1.uses(containerA2, "uses"); DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); for (int i = 0; i < 10; i++) { @@ -380,7 +380,7 @@ public void test_getRelationships_WhenTheOrderPropertyIsAnInteger() { } @Test - public void test_getRelationships_WhenTheOrderPropertyIsADecimal() { + void test_getRelationships_WhenTheOrderPropertyIsADecimal() { containerA1.uses(containerA2, "uses"); DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); for (int i = 0; i < 10; i++) { @@ -402,7 +402,7 @@ public void test_getRelationships_WhenTheOrderPropertyIsADecimal() { } @Test - public void test_getRelationships_WhenTheOrderPropertyIsAString() { + void test_getRelationships_WhenTheOrderPropertyIsAString() { String characters = "abcdefghij"; containerA1.uses(containerA2, "uses"); DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); @@ -425,14 +425,14 @@ public void test_getRelationships_WhenTheOrderPropertyIsAString() { } @Test - public void test_response() { + void test_response() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses"); - + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); view.add(softwareSystem1, "Asks for X", softwareSystem2); view.add(softwareSystem2, "Returns X", softwareSystem1); // this relationship doesn't exist, so is assumed to be a response diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java index 40ec16c6a..2b2f0692b 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java @@ -1,16 +1,16 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ElementStyleTests { @Test - public void test_setOpacity() { + void test_setOpacity() { ElementStyle style = new ElementStyle(); assertNull(style.getOpacity()); @@ -31,7 +31,7 @@ public void test_setOpacity() { } @Test - public void test_opacity() { + void test_opacity() { ElementStyle style = new ElementStyle(); assertNull(style.getOpacity()); @@ -52,7 +52,7 @@ public void test_opacity() { } @Test - public void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.setColor("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -65,7 +65,7 @@ public void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified } @Test - public void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.color("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -77,20 +77,24 @@ public void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() assertEquals("#123456", style.getColor()); } - @Test(expected = IllegalArgumentException.class) - public void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setColor("white"); + @Test + void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.setColor("white"); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.color("white"); + @Test + void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.color("white"); + }); } @Test - public void test_setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { + void test_setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.setBackground("#ffffff"); assertEquals("#ffffff", style.getBackground()); @@ -103,7 +107,7 @@ public void test_setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeI } @Test - public void test_background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { + void test_background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.background("#ffffff"); assertEquals("#ffffff", style.getBackground()); @@ -115,61 +119,67 @@ public void test_background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSp assertEquals("#123456", style.getBackground()); } - @Test(expected = IllegalArgumentException.class) - public void test_setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setBackground("white"); + @Test + void test_setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.setBackground("white"); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.background("white"); + @Test + void test_background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.background("white"); + }); } @Test - public void test_setIcon_WithAUrl() { + void test_setIcon_WithAUrl() { ElementStyle style = new ElementStyle(); style.setIcon("https://structurizr.com/static/img/structurizr-logo.png"); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", style.getIcon()); } @Test - public void test_setIcon_WithAUrlThatHasATrailingSpaceCharacter() { + void test_setIcon_WithAUrlThatHasATrailingSpaceCharacter() { ElementStyle style = new ElementStyle(); style.setIcon("https://structurizr.com/static/img/structurizr-logo.png "); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", style.getIcon()); } @Test - public void test_setIcon_WithADataUri() { + void test_setIcon_WithADataUri() { ElementStyle style = new ElementStyle(); style.setIcon("data:image/png;base64,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"); assertEquals("data:image/png;base64,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", style.getIcon()); } - @Test(expected = IllegalArgumentException.class) - public void test_setIcon_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setIcon("htt://blah"); + @Test + void test_setIcon_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.setIcon("htt://blah"); + }); } @Test - public void test_setIcon_DoesNothing_WhenANullUrlIsSpecified() { + void test_setIcon_DoesNothing_WhenANullUrlIsSpecified() { ElementStyle style = new ElementStyle(); style.setIcon(null); assertNull(style.getIcon()); } @Test - public void test_setIcon_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void test_setIcon_DoesNothing_WhenAnEmptyUrlIsSpecified() { ElementStyle style = new ElementStyle(); style.setIcon(" "); assertNull(style.getIcon()); } @Test - public void test_setStroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { + void test_setStroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.setStroke("#ffffff"); assertEquals("#ffffff", style.getStroke()); @@ -182,7 +192,7 @@ public void test_setStroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecifi } @Test - public void test_Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { + void test_Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.stroke("#ffffff"); assertEquals("#ffffff", style.getStroke()); @@ -194,26 +204,30 @@ public void test_Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified( assertEquals("#123456", style.getStroke()); } - @Test(expected = IllegalArgumentException.class) - public void test_setStroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setStroke("white"); + @Test + void test_setStroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.setStroke("white"); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_Stroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.stroke("white"); + @Test + void test_Stroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.stroke("white"); + }); } @Test - public void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { ElementStyle style = new ElementStyle(); assertEquals(0, style.getProperties().size()); } @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { + void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { try { ElementStyle style = new ElementStyle(); style.addProperty(null, "value"); @@ -224,7 +238,7 @@ public void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { try { ElementStyle style = new ElementStyle(); style.addProperty(" ", "value"); @@ -235,7 +249,7 @@ public void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { + void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { try { ElementStyle style = new ElementStyle(); style.addProperty("name", null); @@ -246,7 +260,7 @@ public void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { try { ElementStyle style = new ElementStyle(); style.addProperty("name", " "); @@ -257,21 +271,21 @@ public void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { } @Test - public void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { ElementStyle style = new ElementStyle(); style.addProperty("name", "value"); assertEquals("value", style.getProperties().get("name")); } @Test - public void test_setProperties_DoesNothing_WhenNullIsSpecified() { + void test_setProperties_DoesNothing_WhenNullIsSpecified() { ElementStyle style = new ElementStyle(); style.setProperties(null); assertEquals(0, style.getProperties().size()); } @Test - public void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { ElementStyle style = new ElementStyle(); Map properties = new HashMap<>(); properties.put("name", "value"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java index a1f1fcd63..a768f19f1 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java @@ -3,21 +3,21 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.Element; import com.structurizr.model.Location; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ElementViewTests extends AbstractWorkspaceTestBase { @Test - public void test_copyLayoutInformationFrom_DoesNothing_WhenNullIsPassed() { + void test_copyLayoutInformationFrom_DoesNothing_WhenNullIsPassed() { Element element = model.addSoftwareSystem(Location.External, "SystemA", ""); ElementView elementView = new ElementView(element); elementView.copyLayoutInformationFrom(null); } @Test - public void test_copyLayoutInformationFrom_CopiesXAndY_WhenANonNullElementViewIsPassed() { + void test_copyLayoutInformationFrom_CopiesXAndY_WhenANonNullElementViewIsPassed() { Element element = model.addSoftwareSystem(Location.External, "SystemA", ""); ElementView elementView1 = new ElementView(element); assertEquals(0, elementView1.getX()); diff --git a/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java b/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java index 83f4ee965..47837ceb9 100644 --- a/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java @@ -2,14 +2,14 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.SoftwareSystem; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class FilteredViewTests extends AbstractWorkspaceTestBase { @Test - public void test_construction() { + void test_construction() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); SystemContextView systemContextView = views.createSystemContextView(softwareSystem, "SystemContext", "Description"); FilteredView filteredView = views.createFilteredView( diff --git a/structurizr-core/test/unit/com/structurizr/view/FontTests.java b/structurizr-core/test/unit/com/structurizr/view/FontTests.java index 435c759df..4839fc961 100644 --- a/structurizr-core/test/unit/com/structurizr/view/FontTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/FontTests.java @@ -1,52 +1,53 @@ package com.structurizr.view; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class FontTests { private Font font; - @Before + @BeforeEach public void setUp() { this.font = new Font(); } @Test - public void construction_WithANameOnly() { + void construction_WithANameOnly() { this.font = new Font("Times New Roman"); assertEquals("Times New Roman", font.getName()); } @Test - public void construction_WithANameAndUrl() { + void construction_WithANameAndUrl() { this.font = new Font("Open Sans", "https://fonts.googleapis.com/css?family=Open+Sans:400,700"); assertEquals("Open Sans", font.getName()); assertEquals("https://fonts.googleapis.com/css?family=Open+Sans:400,700", font.getUrl()); } @Test - public void test_setUrl_WithAUrl() { + void test_setUrl_WithAUrl() { font.setUrl("https://fonts.googleapis.com/css?family=Open+Sans:400,700"); assertEquals("https://fonts.googleapis.com/css?family=Open+Sans:400,700", font.getUrl()); } - @Test(expected = IllegalArgumentException.class) - public void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - font.setUrl("htt://blah"); + @Test + void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + font.setUrl("htt://blah"); + }); } @Test - public void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { + void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { font.setUrl(null); assertNull(font.getUrl()); } @Test - public void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { font.setUrl(" "); assertNull(font.getUrl()); } diff --git a/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java b/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java index 70f4e1ac3..52cd80477 100644 --- a/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java @@ -1,15 +1,15 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; public class PaperSizeTests { @Test - public void test_getOrderedPaperSizes_WhenOrientationIsLandscape() { + void test_getOrderedPaperSizes_WhenOrientationIsLandscape() { List paperSizes = PaperSize.getOrderedPaperSizes(PaperSize.Orientation.Landscape); assertEquals(12, paperSizes.size()); @@ -29,7 +29,7 @@ public void test_getOrderedPaperSizes_WhenOrientationIsLandscape() { } @Test - public void test_getOrderedPaperSizes_WhenOrientationIsPortrait() { + void test_getOrderedPaperSizes_WhenOrientationIsPortrait() { List paperSizes = PaperSize.getOrderedPaperSizes(PaperSize.Orientation.Portrait); assertEquals(9, paperSizes.size()); @@ -46,7 +46,7 @@ public void test_getOrderedPaperSizes_WhenOrientationIsPortrait() { } @Test - public void test_getOrderedPaperSizes() { + void test_getOrderedPaperSizes() { List paperSizes = PaperSize.getOrderedPaperSizes(); assertEquals(21, paperSizes.size()); diff --git a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java index 939770e33..bc95ba423 100644 --- a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java @@ -1,36 +1,36 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class RelationshipStyleTests { private RelationshipStyle relationshipStyle = new RelationshipStyle("tag"); @Test - public void test_setPosition_SetsPositionToNull_WhenNullIsSpecified() { + void test_setPosition_SetsPositionToNull_WhenNullIsSpecified() { relationshipStyle.setPosition(null); assertNull(relationshipStyle.getPosition()); } @Test - public void test_setPosition_SetsPositionToZero_WhenANegativeNumberIsSpecified() { + void test_setPosition_SetsPositionToZero_WhenANegativeNumberIsSpecified() { relationshipStyle.setPosition(-1); assertEquals(Integer.valueOf(0), relationshipStyle.getPosition()); } @Test - public void test_setPosition_SetsPositionToOneHundred_WhenANumberGreaterThanOneHundredIsSpecified() { + void test_setPosition_SetsPositionToOneHundred_WhenANumberGreaterThanOneHundredIsSpecified() { relationshipStyle.setPosition(101); assertEquals(Integer.valueOf(100), relationshipStyle.getPosition()); } @Test - public void test_setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsSpecified() { + void test_setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsSpecified() { relationshipStyle.setPosition(0); assertEquals(Integer.valueOf(0), relationshipStyle.getPosition()); @@ -49,7 +49,7 @@ public void test_setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsS } @Test - public void test_setOpacity() { + void test_setOpacity() { RelationshipStyle style = new RelationshipStyle(); assertNull(style.getOpacity()); @@ -70,7 +70,7 @@ public void test_setOpacity() { } @Test - public void test_opacity() { + void test_opacity() { RelationshipStyle style = new RelationshipStyle(); assertNull(style.getOpacity()); @@ -91,7 +91,7 @@ public void test_opacity() { } @Test - public void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { RelationshipStyle style = new RelationshipStyle(); style.setColor("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -104,7 +104,7 @@ public void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified } @Test - public void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { RelationshipStyle style = new RelationshipStyle(); style.color("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -116,26 +116,30 @@ public void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() assertEquals("#123456", style.getColor()); } - @Test(expected = IllegalArgumentException.class) - public void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - RelationshipStyle style = new RelationshipStyle(); - style.setColor("white"); + @Test + void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + RelationshipStyle style = new RelationshipStyle(); + style.setColor("white"); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - RelationshipStyle style = new RelationshipStyle(); - style.color("white"); + @Test + void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + RelationshipStyle style = new RelationshipStyle(); + style.color("white"); + }); } @Test - public void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { RelationshipStyle style = new RelationshipStyle(); assertEquals(0, style.getProperties().size()); } @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { + void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty(null, "value"); @@ -146,7 +150,7 @@ public void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty(" ", "value"); @@ -157,7 +161,7 @@ public void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { + void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty("name", null); @@ -168,7 +172,7 @@ public void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { } @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty("name", " "); @@ -179,21 +183,21 @@ public void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { } @Test - public void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { RelationshipStyle style = new RelationshipStyle(); style.addProperty("name", "value"); assertEquals("value", style.getProperties().get("name")); } @Test - public void test_setProperties_DoesNothing_WhenNullIsSpecified() { + void test_setProperties_DoesNothing_WhenNullIsSpecified() { RelationshipStyle style = new RelationshipStyle(); style.setProperties(null); assertEquals(0, style.getProperties().size()); } @Test - public void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { RelationshipStyle style = new RelationshipStyle(); Map properties = new HashMap<>(); properties.put("name", "value"); diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java b/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java index 69da2fd5c..1b77117a8 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java @@ -1,13 +1,13 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class SequenceCounterTests { @Test - public void test_increment_IncrementsTheCounter_WhenThereIsNoParent() { + void test_increment_IncrementsTheCounter_WhenThereIsNoParent() { SequenceCounter counter = new SequenceCounter(); assertEquals("0", counter.toString()); diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java b/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java index 5984b6f39..bb5404cc2 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java @@ -1,20 +1,20 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class SequenceNumberTests { @Test - public void test_increment() { + void test_increment() { SequenceNumber sequenceNumber = new SequenceNumber(); assertEquals("1", sequenceNumber.getNext()); assertEquals("2", sequenceNumber.getNext()); } @Test - public void test_parallelSequences() { + void test_parallelSequences() { SequenceNumber sequenceNumber = new SequenceNumber(); assertEquals("1", sequenceNumber.getNext()); diff --git a/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java b/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java index b15f4aa05..c23526029 100644 --- a/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java @@ -3,14 +3,14 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.Relationship; import com.structurizr.model.SoftwareSystem; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class StaticViewTests extends AbstractWorkspaceTestBase { @Test - public void test_addAnimationStep_ThrowsAnException_WhenNoElementsAreSpecified() { + void test_addAnimationStep_ThrowsAnException_WhenNoElementsAreSpecified() { try { SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); view.addAnimation(); @@ -21,7 +21,7 @@ public void test_addAnimationStep_ThrowsAnException_WhenNoElementsAreSpecified() } @Test - public void test_addAnimationStep() { + void test_addAnimationStep() { SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); SoftwareSystem element2 = model.addSoftwareSystem("Software System 2", ""); SoftwareSystem element3 = model.addSoftwareSystem("Software System 3", ""); @@ -55,7 +55,7 @@ public void test_addAnimationStep() { } @Test - public void test_addAnimationStep_IgnoresElementsThatDoNotExistInTheView() { + void test_addAnimationStep_IgnoresElementsThatDoNotExistInTheView() { SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); SoftwareSystem element2 = model.addSoftwareSystem("Software System 2", ""); @@ -69,7 +69,7 @@ public void test_addAnimationStep_IgnoresElementsThatDoNotExistInTheView() { } @Test - public void test_addAnimationStep_ThrowsAnException_WhenElementsAreSpecifiedButNoneOfThemExistInTheView() { + void test_addAnimationStep_ThrowsAnException_WhenElementsAreSpecifiedButNoneOfThemExistInTheView() { try { SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); diff --git a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java index 5857a8076..99182c7a3 100644 --- a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java @@ -2,17 +2,17 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class StylesTests extends AbstractWorkspaceTestBase { private Styles styles = new Styles(); @Test - public void test_findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { - ElementStyle style = styles.findElementStyle((Element)null); + void test_findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { + ElementStyle style = styles.findElementStyle((Element) null); assertEquals(Integer.valueOf(450), style.getWidth()); assertEquals(Integer.valueOf(300), style.getHeight()); assertEquals("#dddddd", style.getBackground()); @@ -28,7 +28,7 @@ public void test_findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { } @Test - public void test_findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { + void test_findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); ElementStyle style = styles.findElementStyle(element); assertEquals(Integer.valueOf(450), style.getWidth()); @@ -46,7 +46,7 @@ public void test_findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined( } @Test - public void test_findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { + void test_findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); element.addTags("Some Tag"); @@ -69,7 +69,7 @@ public void test_findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() } @Test - public void test_findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDefined() { + void test_findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDefined() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name"); softwareSystem.addTags("Some Tag"); @@ -95,7 +95,7 @@ public void test_findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_Whe } @Test - public void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABox() { + void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABox() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); element.addTags("Some Tag"); @@ -109,7 +109,7 @@ public void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABo } @Test - public void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsAPerson() { + void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsAPerson() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); element.addTags("Some Tag"); @@ -123,8 +123,8 @@ public void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsAPe } @Test - public void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { - RelationshipStyle style = styles.findRelationshipStyle((Relationship)null); + void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { + RelationshipStyle style = styles.findRelationshipStyle((Relationship) null); assertEquals(Integer.valueOf(2), style.getThickness()); assertEquals("#707070", style.getColor()); assertTrue(style.getDashed()); @@ -136,7 +136,7 @@ public void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { } @Test - public void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { + void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); Relationship relationship = element.uses(element, "Uses"); RelationshipStyle style = styles.findRelationshipStyle(relationship); @@ -151,7 +151,7 @@ public void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDef } @Test - public void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { + void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); Relationship relationship = element.uses(element, "Uses"); relationship.addTags("Some Tag"); @@ -171,7 +171,7 @@ public void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefin } @Test - public void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationship() { + void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationship() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); Container container1 = softwareSystem.addContainer("Container 1", "Description", "Technology"); Container container2 = softwareSystem.addContainer("Container 2", "Description", "Technology"); @@ -194,7 +194,7 @@ public void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinked } @Test - public void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationshipBasedUponAnImpliedRelationship() { + void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationshipBasedUponAnImpliedRelationship() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); Container container1 = softwareSystem.addContainer("Container 1"); @@ -220,7 +220,7 @@ public void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinked } @Test - public void test_addElementStyle_ThrowsAnException_WhenATagIsNotSpecified() { + void test_addElementStyle_ThrowsAnException_WhenATagIsNotSpecified() { try { styles.addElementStyle(""); fail(); @@ -244,7 +244,7 @@ public void test_addElementStyle_ThrowsAnException_WhenATagIsNotSpecified() { } @Test - public void test_addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void test_addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); @@ -256,7 +256,7 @@ public void test_addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTag } @Test - public void test_addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void test_addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { ElementStyle style = styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); styles.add(style); @@ -268,7 +268,7 @@ public void test_addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExist } @Test - public void test_addRelationshipStyle_ThrowsAnException_WhenATagIsNotSpecified() { + void test_addRelationshipStyle_ThrowsAnException_WhenATagIsNotSpecified() { try { styles.addRelationshipStyle(""); fail(); @@ -292,7 +292,7 @@ public void test_addRelationshipStyle_ThrowsAnException_WhenATagIsNotSpecified() } @Test - public void test_addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void test_addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); @@ -304,7 +304,7 @@ public void test_addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSa } @Test - public void test_addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void test_addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { RelationshipStyle style = styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); styles.add(style); @@ -316,7 +316,7 @@ public void test_addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTag } @Test - public void test_clearElementStyles_RemovesAllElementStyles() { + void test_clearElementStyles_RemovesAllElementStyles() { styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); assertEquals(1, styles.getElements().size()); @@ -325,7 +325,7 @@ public void test_clearElementStyles_RemovesAllElementStyles() { } @Test - public void test_clearRelationshipStyles_RemovesAllRelationshipStyles() { + void test_clearRelationshipStyles_RemovesAllRelationshipStyles() { styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); assertEquals(1, styles.getRelationships().size()); diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java index 61517f64b..dc37a0cf1 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java @@ -2,24 +2,24 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class SystemContextViewTests extends AbstractWorkspaceTestBase { private SoftwareSystem softwareSystem; private SystemContextView view; - @Before + @BeforeEach public void setUp() { softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); view = new SystemContextView(softwareSystem, "context", "Description"); } @Test - public void test_construction() { + void test_construction() { assertEquals("The System - System Context", view.getName()); assertEquals(1, view.getElements().size()); assertSame(view.getElements().iterator().next().getElement(), softwareSystem); @@ -29,14 +29,14 @@ public void test_construction() { } @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { assertEquals(1, view.getElements().size()); view.addAllSoftwareSystems(); assertEquals(1, view.getElements().size()); } @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -49,14 +49,14 @@ public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSo } @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { assertEquals(1, view.getElements().size()); view.addAllPeople(); assertEquals(1, view.getElements().size()); } @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson(Location.External, "User A", "Description"); Person userB = model.addPerson(Location.External, "User B", "Description"); @@ -69,14 +69,14 @@ public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { assertEquals(1, view.getElements().size()); view.addAllElements(); assertEquals(1, view.getElements().size()); } @Test - public void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { + void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); Person userA = model.addPerson(Location.External, "User A", "Description"); @@ -93,7 +93,7 @@ public void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSome } @Test - public void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { + void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { try { view.addNearestNeighbours(null); fail(); @@ -103,7 +103,7 @@ public void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecif } @Test - public void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { + void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { Container container = softwareSystem.addContainer("Container", "Description", "Technology"); try { view.addNearestNeighbours(container); @@ -114,14 +114,14 @@ public void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAP } @Test - public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { view.addNearestNeighbours(softwareSystem); assertEquals(1, view.getElements().size()); } @Test - public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", "Description"); Person userA = model.addPerson("User A", "Description"); @@ -170,9 +170,9 @@ public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNear } @Test - public void test_removeSoftwareSystem_ThrowsAnException_WhenPassedNull() { + void test_removeSoftwareSystem_ThrowsAnException_WhenPassedNull() { try { - view.remove((SoftwareSystem)null); + view.remove((SoftwareSystem) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("An element must be specified.", iae.getMessage()); @@ -180,7 +180,7 @@ public void test_removeSoftwareSystem_ThrowsAnException_WhenPassedNull() { } @Test - public void test_removeSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTheView() { + void test_removeSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTheView() { SoftwareSystem anotherSoftwareSystem = model.addSoftwareSystem("Another software system", ""); assertEquals(1, view.getElements().size()); @@ -189,7 +189,7 @@ public void test_removeSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTh } @Test - public void test_removeSoftwareSystem_DoesNotRemoveTheSoftwareSystemInFocus() { + void test_removeSoftwareSystem_DoesNotRemoveTheSoftwareSystemInFocus() { try { view.remove(softwareSystem); fail(); @@ -199,7 +199,7 @@ public void test_removeSoftwareSystem_DoesNotRemoveTheSoftwareSystemInFocus() { } @Test - public void test_removeSoftwareSystem_RemovesTheSoftwareSystemAndRelationshipsFromTheView() { + void test_removeSoftwareSystem_RemovesTheSoftwareSystemAndRelationshipsFromTheView() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software system 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software system 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -215,9 +215,9 @@ public void test_removeSoftwareSystem_RemovesTheSoftwareSystemAndRelationshipsFr } @Test - public void test_removePerson_ThrowsAnException_WhenPassedNull() { + void test_removePerson_ThrowsAnException_WhenPassedNull() { try { - view.remove((Person)null); + view.remove((Person) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("An element must be specified.", iae.getMessage()); @@ -225,7 +225,7 @@ public void test_removePerson_ThrowsAnException_WhenPassedNull() { } @Test - public void test_removePerson_DoesNothing_WhenThePersonIsNotInTheView() { + void test_removePerson_DoesNothing_WhenThePersonIsNotInTheView() { Person person = model.addPerson("Person", ""); assertEquals(1, view.getElements().size()); @@ -234,7 +234,7 @@ public void test_removePerson_DoesNothing_WhenThePersonIsNotInTheView() { } @Test - public void test_removePerson_RemovesThePersonAndRelationshipsFromTheView() { + void test_removePerson_RemovesThePersonAndRelationshipsFromTheView() { Person person = model.addPerson("Person", ""); person.uses(softwareSystem, "uses"); softwareSystem.delivers(person, "delivers something to"); @@ -249,7 +249,7 @@ public void test_removePerson_RemovesThePersonAndRelationshipsFromTheView() { } @Test - public void test_addSoftwareSystemWithoutRelationships_DoesNotAddRelationships() { + void test_addSoftwareSystemWithoutRelationships_DoesNotAddRelationships() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software system 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software system 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -261,7 +261,7 @@ public void test_addSoftwareSystemWithoutRelationships_DoesNotAddRelationships() } @Test - public void test_addPersonWithoutRelationships_DoesNotAddRelationships() { + void test_addPersonWithoutRelationships_DoesNotAddRelationships() { Person user = model.addPerson("User", ""); SoftwareSystem softwareSystem = model.addSoftwareSystem("Software system 2", ""); user.uses(softwareSystem, "uses"); @@ -273,7 +273,7 @@ public void test_addPersonWithoutRelationships_DoesNotAddRelationships() { } @Test - public void test_isEnterpriseBoundaryVisible() { + void test_isEnterpriseBoundaryVisible() { assertTrue(view.isEnterpriseBoundaryVisible()); // default is true view.setEnterpriseBoundaryVisible(false); @@ -281,7 +281,7 @@ public void test_isEnterpriseBoundaryVisible() { } @Test - public void test_addDefaultElements() { + void test_addDefaultElements() { CustomElement element = model.addCustomElement("Custom"); Person user1 = model.addPerson("User 1"); Person user2 = model.addPerson("User 2"); diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java b/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java index 833bda34d..9e0838836 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java @@ -2,56 +2,60 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class SystemLandscapeViewTests extends AbstractWorkspaceTestBase { private SystemLandscapeView view; - @Before + @BeforeEach public void setUp() { view = new SystemLandscapeView(model, "context", "Description"); } @Test - public void test_construction() { + void test_construction() { assertEquals("System Landscape", view.getName()); assertEquals(0, view.getElements().size()); assertSame(model, view.getModel()); } @Test - public void test_getName_WhenNoEnterpriseIsSpecified() { + void test_getName_WhenNoEnterpriseIsSpecified() { assertEquals("System Landscape", view.getName()); } @Test - public void test_getName_WhenAnEnterpriseIsSpecified() { + void test_getName_WhenAnEnterpriseIsSpecified() { model.setEnterprise(new Enterprise("Widgets Limited")); assertEquals("System Landscape for Widgets Limited", view.getName()); } - @Test(expected = IllegalArgumentException.class) - public void test_getName_WhenAnEmptyEnterpriseNameIsSpecified() { - model.setEnterprise(new Enterprise("")); + @Test + void test_getName_WhenAnEmptyEnterpriseNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.setEnterprise(new Enterprise("")); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_getName_WhenANullEnterpriseNameIsSpecified() { - model.setEnterprise(new Enterprise(null)); + @Test + void test_getName_WhenANullEnterpriseNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.setEnterprise(new Enterprise(null)); + }); } @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { view.addAllSoftwareSystems(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -63,13 +67,13 @@ public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSo } @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { view.addAllPeople(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson("User A", "Description"); Person userB = model.addPerson("User B", "Description"); @@ -81,13 +85,13 @@ public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { view.addAllElements(); assertEquals(0, view.getElements().size()); } @Test - public void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { + void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Person person = model.addPerson("Person", "Description"); @@ -99,7 +103,7 @@ public void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSome } @Test - public void test_isEnterpriseBoundaryVisible() { + void test_isEnterpriseBoundaryVisible() { assertTrue(view.isEnterpriseBoundaryVisible()); // default is true view.setEnterpriseBoundaryVisible(false); @@ -107,7 +111,7 @@ public void test_isEnterpriseBoundaryVisible() { } @Test - public void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { + void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { try { view.addNearestNeighbours(null); fail(); @@ -117,7 +121,7 @@ public void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecif } @Test - public void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { + void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); try { @@ -129,7 +133,7 @@ public void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAP } @Test - public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); view.addNearestNeighbours(softwareSystem); @@ -137,7 +141,7 @@ public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { } @Test - public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); @@ -187,7 +191,7 @@ public void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNear } @Test - public void test_addDefaultElements() { + void test_addDefaultElements() { CustomElement element = model.addCustomElement("Custom"); Person user = model.addPerson("User"); SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); diff --git a/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java b/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java index 7c6bb38ea..13398c7f8 100644 --- a/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java @@ -2,14 +2,14 @@ import com.structurizr.Workspace; import com.structurizr.model.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TerminologyTests { @Test - public void test_findTerminology() { + void test_findTerminology() { Workspace workspace = new Workspace("Name", "Description"); Terminology terminology = workspace.getViews().getConfiguration().getTerminology(); Person person = workspace.getModel().addPerson("Name"); diff --git a/structurizr-core/test/unit/com/structurizr/view/VertexTests.java b/structurizr-core/test/unit/com/structurizr/view/VertexTests.java index a7f3be379..94e81fec0 100644 --- a/structurizr-core/test/unit/com/structurizr/view/VertexTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/VertexTests.java @@ -1,25 +1,25 @@ package com.structurizr.view; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; public class VertexTests { @Test - public void test_equals() { + void test_equals() { Vertex vertex1 = new Vertex(123, 456); Vertex vertex2 = new Vertex(123, 456); Vertex vertex3 = new Vertex(456, 123); - assertFalse(vertex1.equals(null)); - assertFalse(vertex1.equals("hello world")); + assertNotEquals(vertex1, null); + assertNotEquals(vertex1, "hello world"); - assertTrue(vertex1.equals(vertex1)); - assertTrue(vertex1.equals(vertex2)); - assertTrue(vertex2.equals(vertex1)); - assertFalse(vertex2.equals(vertex3)); + assertEquals(vertex1, vertex1); + assertEquals(vertex1, vertex2); + assertEquals(vertex2, vertex1); + assertNotEquals(vertex2, vertex3); } } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java b/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java index 086d5b280..b3741b231 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java @@ -2,12 +2,12 @@ import com.structurizr.Workspace; import com.structurizr.model.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.HashSet; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ViewSetTests { @@ -28,7 +28,7 @@ private Workspace createWorkspace() { } @Test - public void test_createCustomView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createCustomView_ThrowsAnException_WhenANullKeyIsSpecified() { try { new Workspace("", "").getViews().createCustomView(null, "Title", "Description"); fail(); @@ -38,7 +38,7 @@ public void test_createCustomView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - public void test_createCustomView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createCustomView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { new Workspace("", "").getViews().createCustomView(" ", "Title", "Description"); fail(); @@ -48,7 +48,7 @@ public void test_createCustomView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() } @Test - public void test_createCustomView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void test_createCustomView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createCustomView("key", "Title", "Description"); @@ -60,14 +60,14 @@ public void test_createCustomView_ThrowsAnException_WhenADuplicateKeyIsSpecified } @Test - public void test_createCustomView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { + void test_createCustomView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createCustomView("key1", "Title", "Description"); workspace.getViews().createCustomView("key2", "Title", "Description"); } @Test - public void test_createCustomView() { + void test_createCustomView() { Workspace workspace = new Workspace("Name", "Description"); CustomView customView = workspace.getViews().createCustomView("key", "Title", "Description"); assertEquals("key", customView.getKey()); @@ -78,7 +78,7 @@ public void test_createCustomView() { } @Test - public void test_createSystemLandscapeView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createSystemLandscapeView_ThrowsAnException_WhenANullKeyIsSpecified() { try { new Workspace("", "").getViews().createSystemLandscapeView(null, "Description"); fail(); @@ -88,7 +88,7 @@ public void test_createSystemLandscapeView_ThrowsAnException_WhenANullKeyIsSpeci } @Test - public void test_createSystemLandscapeView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createSystemLandscapeView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { new Workspace("", "").getViews().createSystemLandscapeView(" ", "Description"); fail(); @@ -98,7 +98,7 @@ public void test_createSystemLandscapeView_ThrowsAnException_WhenAnEmptyKeyIsSpe } @Test - public void test_createSystemLandscapeView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void test_createSystemLandscapeView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key", "Description"); @@ -110,14 +110,14 @@ public void test_createSystemLandscapeView_ThrowsAnException_WhenADuplicateKeyIs } @Test - public void test_createSystemLandscapeView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { + void test_createSystemLandscapeView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key1", "Description"); workspace.getViews().createSystemLandscapeView("key2", "Description"); } @Test - public void test_createSystemLandscapeView() { + void test_createSystemLandscapeView() { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView systemLandscapeView = workspace.getViews().createSystemLandscapeView("key", "Description"); assertEquals("key", systemLandscapeView.getKey()); @@ -125,7 +125,7 @@ public void test_createSystemLandscapeView() { } @Test - public void test_createSystemContextView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + void test_createSystemContextView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createSystemContextView(null, null, "Description"); fail(); @@ -135,7 +135,7 @@ public void test_createSystemContextView_ThrowsAnException_WhenASoftwareSystemIs } @Test - public void test_createSystemContextView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createSystemContextView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -147,7 +147,7 @@ public void test_createSystemContextView_ThrowsAnException_WhenANullKeyIsSpecifi } @Test - public void test_createSystemContextView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createSystemContextView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -159,7 +159,7 @@ public void test_createSystemContextView_ThrowsAnException_WhenAnEmptyKeyIsSpeci } @Test - public void test_createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void test_createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -172,7 +172,7 @@ public void test_createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsSp } @Test - public void test_createSystemContextView() { + void test_createSystemContextView() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "key", "Description"); @@ -182,7 +182,7 @@ public void test_createSystemContextView() { } @Test - public void test_createContainerView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + void test_createContainerView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createContainerView(null, null, "Description"); fail(); @@ -192,7 +192,7 @@ public void test_createContainerView_ThrowsAnException_WhenASoftwareSystemIsSpec } @Test - public void test_createContainerView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createContainerView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -204,7 +204,7 @@ public void test_createContainerView_ThrowsAnException_WhenANullKeyIsSpecified() } @Test - public void test_createContainerView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createContainerView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -216,7 +216,7 @@ public void test_createContainerView_ThrowsAnException_WhenAnEmptyKeyIsSpecified } @Test - public void test_createContainerView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void test_createContainerView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -229,7 +229,7 @@ public void test_createContainerView_ThrowsAnException_WhenADuplicateKeyIsSpecif } @Test - public void test_createContainerView() { + void test_createContainerView() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); ContainerView containerView = workspace.getViews().createContainerView(softwareSystem, "key", "Description"); @@ -239,7 +239,7 @@ public void test_createContainerView() { } @Test - public void test_createComponentView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + void test_createComponentView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createComponentView(null, null, "Description"); fail(); @@ -249,7 +249,7 @@ public void test_createComponentView_ThrowsAnException_WhenASoftwareSystemIsSpec } @Test - public void test_createComponentView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createComponentView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -262,7 +262,7 @@ public void test_createComponentView_ThrowsAnException_WhenANullKeyIsSpecified() } @Test - public void test_createComponentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createComponentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -275,7 +275,7 @@ public void test_createComponentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified } @Test - public void test_createComponentView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void test_createComponentView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -289,7 +289,7 @@ public void test_createComponentView_ThrowsAnException_WhenADuplicateKeyIsSpecif } @Test - public void test_createComponentView() { + void test_createComponentView() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); @@ -299,9 +299,9 @@ public void test_createComponentView() { assertSame(softwareSystem, componentView.getSoftwareSystem()); assertSame(container, componentView.getContainer()); } - + @Test - public void test_createDynamicView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createDynamicView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDynamicView(null, "Description"); @@ -312,7 +312,7 @@ public void test_createDynamicView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - public void test_createDynamicView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createDynamicView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDynamicView(" ", "Description"); @@ -323,7 +323,7 @@ public void test_createDynamicView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() } @Test - public void test_createDynamicView() { + void test_createDynamicView() { Workspace workspace = new Workspace("Name", "Description"); DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); @@ -334,9 +334,9 @@ public void test_createDynamicView() { } @Test - public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { try { - new Workspace("", "").getViews().createDynamicView((SoftwareSystem)null, "key", "Description"); + new Workspace("", "").getViews().createDynamicView((SoftwareSystem) null, "key", "Description"); fail(); } catch (IllegalArgumentException iae) { assertEquals("A software system must be specified.", iae.getMessage()); @@ -344,7 +344,7 @@ public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANull } @Test - public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -356,7 +356,7 @@ public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANull } @Test - public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -368,7 +368,7 @@ public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenAnEmp } @Test - public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -382,7 +382,7 @@ public void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenADupl } @Test - public void test_createDynamicViewForSoftwareSystem() { + void test_createDynamicViewForSoftwareSystem() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -394,9 +394,9 @@ public void test_createDynamicViewForSoftwareSystem() { } @Test - public void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullContainerIsSpecified() { + void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullContainerIsSpecified() { try { - new Workspace("", "").getViews().createDynamicView((Container)null, "key", "Description"); + new Workspace("", "").getViews().createDynamicView((Container) null, "key", "Description"); fail(); } catch (IllegalArgumentException iae) { assertEquals("A container must be specified.", iae.getMessage()); @@ -404,7 +404,7 @@ public void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullConta } @Test - public void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -417,7 +417,7 @@ public void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullKeyIs } @Test - public void test_createDynamicViewForAContainer_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createDynamicViewForAContainer_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -430,7 +430,7 @@ public void test_createDynamicViewForAContainer_ThrowsAnException_WhenAnEmptyKey } @Test - public void test_createDynamicViewForContainer() { + void test_createDynamicViewForContainer() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); @@ -443,7 +443,7 @@ public void test_createDynamicViewForContainer() { } @Test - public void test_createDeploymentView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createDeploymentView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDeploymentView(null, "Description"); @@ -454,7 +454,7 @@ public void test_createDeploymentView_ThrowsAnException_WhenANullKeyIsSpecified( } @Test - public void test_createDeploymentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createDeploymentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDeploymentView(" ", "Description"); @@ -465,7 +465,7 @@ public void test_createDeploymentView_ThrowsAnException_WhenAnEmptyKeyIsSpecifie } @Test - public void test_createDeploymentView_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void test_createDeploymentView_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -479,7 +479,7 @@ public void test_createDeploymentView_ThrowsAnException_WhenADuplicateKeyIsUsed( } @Test - public void test_createDeploymentView() { + void test_createDeploymentView() { Workspace workspace = new Workspace("Name", "Description"); DeploymentView deploymentView = workspace.getViews().createDeploymentView("key", "Description"); @@ -489,9 +489,9 @@ public void test_createDeploymentView() { } @Test - public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { try { - new Workspace("", "").getViews().createDeploymentView((SoftwareSystem)null, "key", "Description"); + new Workspace("", "").getViews().createDeploymentView((SoftwareSystem) null, "key", "Description"); fail(); } catch (IllegalArgumentException iae) { assertEquals("A software system must be specified.", iae.getMessage()); @@ -499,7 +499,7 @@ public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAN } @Test - public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -511,7 +511,7 @@ public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAN } @Test - public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -523,7 +523,7 @@ public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAn } @Test - public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -537,7 +537,7 @@ public void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAD } @Test - public void test_createDeploymentViewForSoftwareSystem() { + void test_createDeploymentViewForSoftwareSystem() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -548,7 +548,7 @@ public void test_createDeploymentViewForSoftwareSystem() { } @Test - public void test_createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() { + void test_createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createFilteredView(null, "key", "Description", FilterMode.Include, "tag1", "tag2"); @@ -559,7 +559,7 @@ public void test_createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() } @Test - public void test_createFilteredView_ThrowsAnException_WhenANullKeyIsSpecified() { + void test_createFilteredView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); @@ -571,7 +571,7 @@ public void test_createFilteredView_ThrowsAnException_WhenANullKeyIsSpecified() } @Test - public void test_createFilteredView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void test_createFilteredView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); @@ -583,7 +583,7 @@ public void test_createFilteredView_ThrowsAnException_WhenAnEmptyKeyIsSpecified( } @Test - public void test_createFilteredView_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void test_createFilteredView_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); workspace.getViews().createFilteredView(view, "filtered", "Description", FilterMode.Include, "tag1", "tag2"); @@ -596,7 +596,7 @@ public void test_createFilteredView_ThrowsAnException_WhenADuplicateKeyIsUsed() } @Test - public void test_createFilteredView() { + void test_createFilteredView() { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); FilteredView filteredView = workspace.getViews().createFilteredView(view, "key", "Description", FilterMode.Include, "tag1", "tag2"); @@ -610,7 +610,7 @@ public void test_createFilteredView() { } @Test - public void test_copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesMatch() { + void test_copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -631,7 +631,7 @@ public void test_copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesM } @Test - public void test_copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { + void test_copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -651,7 +651,7 @@ public void test_copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { } @Test - public void test_copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeIsSet() { + void test_copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeIsSet() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -667,7 +667,7 @@ public void test_copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeI } @Test - public void test_copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIsNotSet() { + void test_copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIsNotSet() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -682,7 +682,7 @@ public void test_copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIs } @Test - public void test_copyLayoutInformationFrom_WhenTheSystemLandscapeViewKeysMatch() { + void test_copyLayoutInformationFrom_WhenTheSystemLandscapeViewKeysMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("landscape", "Description"); @@ -701,7 +701,7 @@ public void test_copyLayoutInformationFrom_WhenTheSystemLandscapeViewKeysMatch() } @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemLandscapeViewToCopyInformationFrom() { + void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemLandscapeViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -715,7 +715,7 @@ public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemLan } @Test - public void test_copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { + void test_copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -734,7 +734,7 @@ public void test_copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { } @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemContextViewToCopyInformationFrom() { + void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemContextViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -748,7 +748,7 @@ public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemCon } @Test - public void test_copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { + void test_copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "containers", "Description"); @@ -767,7 +767,7 @@ public void test_copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { } @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainerViewToCopyInformationFrom() { + void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainerViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -781,7 +781,7 @@ public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainer } @Test - public void test_copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { + void test_copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { Workspace workspace1 = createWorkspace(); Container container1 = workspace1.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); ComponentView view1 = workspace1.getViews().createComponentView(container1, "containers", "Description"); @@ -800,7 +800,7 @@ public void test_copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { } @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponentViewToCopyInformationFrom() { + void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponentViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -814,7 +814,7 @@ public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponent } @Test - public void test_copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { + void test_copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { Workspace workspace1 = createWorkspace(); Person person1 = workspace1.getModel().getPersonWithName("Person"); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); @@ -835,7 +835,7 @@ public void test_copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { } @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicViewToCopyInformationFrom() { + void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -850,7 +850,7 @@ public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicVi } @Test - public void test_copyLayoutInformationFrom_WhenTheDeploymentViewKeysMatch() { + void test_copyLayoutInformationFrom_WhenTheDeploymentViewKeysMatch() { Workspace workspace1 = createWorkspace(); DeploymentNode deploymentNode1 = workspace1.getModel().getDeploymentNodeWithName("Deployment Node"); DeploymentView view1 = workspace1.getViews().createDeploymentView("key", "Description"); @@ -871,7 +871,7 @@ public void test_copyLayoutInformationFrom_WhenTheDeploymentViewKeysMatch() { } @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDeploymentViewToCopyInformationFrom() { + void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDeploymentViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -910,7 +910,7 @@ private HashSet relationshipViewsFor(Relationship... relations } @Test - public void test_hydrate() { + void test_hydrate() { Workspace workspace = new Workspace("Name", "Description"); Model model = workspace.getModel(); ViewSet views = workspace.getViews(); @@ -1004,7 +1004,7 @@ public void test_hydrate() { } @Test - public void test_setEnterpriseContextViews_IsSupportedForOlderWorkspaces() { + void test_setEnterpriseContextViews_IsSupportedForOlderWorkspaces() { ViewSet views = new Workspace("", "").getViews(); SystemLandscapeView systemLandscapeView = views.createSystemLandscapeView("key", "Description"); views.setEnterpriseContextViews(Collections.singleton(systemLandscapeView)); @@ -1013,7 +1013,7 @@ public void test_setEnterpriseContextViews_IsSupportedForOlderWorkspaces() { } @Test - public void test_createDefaultViews() { + void test_createDefaultViews() { Workspace workspace = new Workspace("Name", "Description"); Model model = workspace.getModel(); ViewSet views = workspace.getViews(); @@ -1075,7 +1075,7 @@ public void test_createDefaultViews() { } @Test - public void test_copyLayoutInformationFrom_DoesNothing_WhenMergeFromRemoteIsSetToFalse() { + void test_copyLayoutInformationFrom_DoesNothing_WhenMergeFromRemoteIsSetToFalse() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("landscape", "Description"); @@ -1095,7 +1095,7 @@ public void test_copyLayoutInformationFrom_DoesNothing_WhenMergeFromRemoteIsSetT } @Test - public void test_view_ordering() { + void test_view_ordering() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ViewTests.java index e093e84ec..8b2fde869 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ViewTests.java @@ -3,17 +3,17 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.Workspace; import com.structurizr.model.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Iterator; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ViewTests extends AbstractWorkspaceTestBase { @Test - public void test_construction() { + void test_construction() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "key", "Description"); assertEquals("key", view.getKey()); @@ -22,14 +22,14 @@ public void test_construction() { } @Test - public void test_construction_WhenTheViewKeyContainsAForwardSlashCharacter() { + void test_construction_WhenTheViewKeyContainsAForwardSlashCharacter() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); StaticView view = new SystemContextView(softwareSystem, "key/1", "Description"); assertEquals("key_1", view.getKey()); } @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsInTheModel() { + void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); assertEquals(1, view.getElements().size()); @@ -38,7 +38,7 @@ public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSy } @Test - public void test_addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSystemsInTheModel() { + void test_addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSystemsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.Unspecified, "System B", "Description"); @@ -56,12 +56,12 @@ public void test_addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSof } @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenGivenNull() { + void test_addSoftwareSystem_ThrowsAnException_WhenGivenNull() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); try { - view.add((SoftwareSystem)null); + view.add((SoftwareSystem) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("An element must be specified.", iae.getMessage()); @@ -69,7 +69,7 @@ public void test_addSoftwareSystem_ThrowsAnException_WhenGivenNull() { } @Test - public void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() { + void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); @@ -82,7 +82,7 @@ public void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIs } @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { + void test_addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); @@ -93,7 +93,7 @@ public void test_addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { } @Test - public void test_addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { + void test_addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); Person person2 = model.addPerson(Location.Unspecified, "Person 2", "Description"); @@ -111,11 +111,11 @@ public void test_addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { } @Test - public void test_addPerson_ThrowsAnException_WhenGivenNull() { + void test_addPerson_ThrowsAnException_WhenGivenNull() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); try { - view.add((Person)null); + view.add((Person) null); fail(); } catch (IllegalArgumentException iae) { assertEquals("An element must be specified.", iae.getMessage()); @@ -123,7 +123,7 @@ public void test_addPerson_ThrowsAnException_WhenGivenNull() { } @Test - public void test_addPerson_AddsThePerson_WhenThPersonIsInTheModel() { + void test_addPerson_AddsThePerson_WhenThPersonIsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); @@ -137,7 +137,7 @@ public void test_addPerson_AddsThePerson_WhenThPersonIsInTheModel() { } @Test - public void test_removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelationshipsBetweenElements() { + void test_removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelationshipsBetweenElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "Software System", "Description"); Person person = model.addPerson(Location.Unspecified, "Person", "Description"); @@ -150,7 +150,7 @@ public void test_removeElementsWithNoRelationships_RemovesAllElements_WhenTheVie } @Test - public void test_removeElementsWithNoRelationships_RemovesOnlyThoseElementsWithoutRelationships_WhenTheViewContainsSomeUnlinkedElements() { + void test_removeElementsWithNoRelationships_RemovesOnlyThoseElementsWithoutRelationships_WhenTheViewContainsSomeUnlinkedElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.Unspecified, "System B", "Description"); @@ -170,7 +170,7 @@ public void test_removeElementsWithNoRelationships_RemovesOnlyThoseElementsWitho } @Test - public void test_copyLayoutInformationFrom() { + void test_copyLayoutInformationFrom() { Workspace workspace1 = new Workspace("", ""); Model model1 = workspace1.getModel(); SoftwareSystem softwareSystem1A = model1.addSoftwareSystem("System A", "Description"); @@ -256,21 +256,21 @@ public void test_copyLayoutInformationFrom() { } @Test - public void test_getName() { + void test_getName() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SystemContextView systemContextView = new SystemContextView(softwareSystem, "context", "Description"); assertEquals("The System - System Context", systemContextView.getName()); } @Test - public void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenANullElementIsSpecified() { + void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); view.removeElementsThatAreUnreachableFrom(null); } @Test - public void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenAllElementsCanBeReached() { + void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenAllElementsCanBeReached() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); @@ -287,7 +287,7 @@ public void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenAllElement } @Test - public void test_removeElementsThatAreUnreachableFrom_RemovesOrphanedElements_WhenThereAreSomeOrphanedElements() { + void test_removeElementsThatAreUnreachableFrom_RemovesOrphanedElements_WhenThereAreSomeOrphanedElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); @@ -306,7 +306,7 @@ public void test_removeElementsThatAreUnreachableFrom_RemovesOrphanedElements_Wh } @Test - public void test_removeElementsThatAreUnreachableFrom_RemovesUnreachableElements_WhenThereAreSomeUnreachableElements() { + void test_removeElementsThatAreUnreachableFrom_RemovesUnreachableElements_WhenThereAreSomeUnreachableElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); @@ -324,7 +324,7 @@ public void test_removeElementsThatAreUnreachableFrom_RemovesUnreachableElements } @Test - public void test_removeElementsThatAreUnreachableFrom_DoesntIncludeAllElements_WhenThereIsACyclicGraph() { + void test_removeElementsThatAreUnreachableFrom_DoesntIncludeAllElements_WhenThereIsACyclicGraph() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); Person user = model.addPerson("User", ""); @@ -345,7 +345,7 @@ public void test_removeElementsThatAreUnreachableFrom_DoesntIncludeAllElements_W } @Test - public void test_removeRelationship_DoesNothing_WhenNullIsSpecified() { + void test_removeRelationship_DoesNothing_WhenNullIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); SoftwareSystem softwareSystem3 = model.addSoftwareSystem("Software System 3", "Description"); @@ -358,11 +358,11 @@ public void test_removeRelationship_DoesNothing_WhenNullIsSpecified() { view.addAllElements(); assertEquals(3, view.getRelationships().size()); - view.remove((Relationship)null); + view.remove((Relationship) null); } @Test - public void test_removeRelationship_RemovesARelationship_WhenAValidRelationshipIsSpecified() { + void test_removeRelationship_RemovesARelationship_WhenAValidRelationshipIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); SoftwareSystem softwareSystem3 = model.addSoftwareSystem("Software System 3", "Description"); @@ -382,20 +382,24 @@ public void test_removeRelationship_RemovesARelationship_WhenAValidRelationshipI assertTrue(view.getRelationships().contains(new RelationshipView(relationship23))); } - @Test(expected = IllegalArgumentException.class) - public void test_setKey_ThrowsAnException_WhenANullKeyIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - new SystemContextView(softwareSystem, null, "Description"); + @Test + void test_setKey_ThrowsAnException_WhenANullKeyIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + new SystemContextView(softwareSystem, null, "Description"); + }); } - @Test(expected = IllegalArgumentException.class) - public void test_setKey_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - new SystemContextView(softwareSystem, " ", "Description"); + @Test + void test_setKey_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + new SystemContextView(softwareSystem, " ", "Description"); + }); } @Test - public void test_addElement_ThrowsAnException_WhenTheSpecifiedElementDoesNotExistInTheModel() { + void test_addElement_ThrowsAnException_WhenTheSpecifiedElementDoesNotExistInTheModel() { try { Workspace workspace = new Workspace("1", ""); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); @@ -409,7 +413,7 @@ public void test_addElement_ThrowsAnException_WhenTheSpecifiedElementDoesNotExis } @Test - public void test_enableAutomaticLayout_EnablesAutoLayoutWithSomeDefaultValues_WhenTrueIsSpecified() { + void test_enableAutomaticLayout_EnablesAutoLayoutWithSomeDefaultValues_WhenTrueIsSpecified() { SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); view.enableAutomaticLayout(); @@ -422,7 +426,7 @@ public void test_enableAutomaticLayout_EnablesAutoLayoutWithSomeDefaultValues_Wh } @Test - public void test_enableAutomaticLayout_DisablesAutoLayout_WhenFalseIsSpecified() { + void test_enableAutomaticLayout_DisablesAutoLayout_WhenFalseIsSpecified() { SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); view.enableAutomaticLayout(); assertNotNull(view.getAutomaticLayout()); @@ -432,7 +436,7 @@ public void test_enableAutomaticLayout_DisablesAutoLayout_WhenFalseIsSpecified() } @Test - public void test_enableAutomaticLayout() { + void test_enableAutomaticLayout() { SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); view.enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 100, 200, 300, true); @@ -445,7 +449,7 @@ public void test_enableAutomaticLayout() { } @Test - public void test_addCustomElement_AddsTheCustomElementToTheView() { + void test_addCustomElement_AddsTheCustomElementToTheView() { Workspace workspace = new Workspace("", ""); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); @@ -463,7 +467,7 @@ public void test_addCustomElement_AddsTheCustomElementToTheView() { } @Test - public void test_addCustomElementWithoutRelationships_AddsTheCustomElementToTheView() { + void test_addCustomElementWithoutRelationships_AddsTheCustomElementToTheView() { Workspace workspace = new Workspace("", ""); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); @@ -481,7 +485,7 @@ public void test_addCustomElementWithoutRelationships_AddsTheCustomElementToTheV } @Test - public void test_removeCustomElement_RemovesTheCustomElementFromTheView() { + void test_removeCustomElement_RemovesTheCustomElementFromTheView() { Workspace workspace = new Workspace("", ""); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); From c8ab806d24b18129adca32a8bc12f751a1ee52b1 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 2 Aug 2022 15:05:51 +0200 Subject: [PATCH 006/418] Remove unused imports --- .../src/com/structurizr/io/json/JsonReader.java | 1 - .../test/unit/com/structurizr/util/WorkspaceUtilsTests.java | 1 - .../src/com/structurizr/documentation/Documentable.java | 2 -- .../src/com/structurizr/model/CustomElement.java | 2 -- structurizr-core/src/com/structurizr/model/Element.java | 1 - .../src/com/structurizr/model/GroupableElement.java | 6 ------ .../src/com/structurizr/model/Relationship.java | 1 - .../src/com/structurizr/model/StaticStructureElement.java | 2 -- .../structurizr/model/StaticStructureElementInstance.java | 1 - structurizr-core/src/com/structurizr/view/DynamicView.java | 1 - structurizr-core/src/com/structurizr/view/ElementStyle.java | 1 - structurizr-core/src/com/structurizr/view/Styles.java | 1 - .../test/unit/com/structurizr/WorkspaceTests.java | 2 -- .../test/unit/com/structurizr/model/ElementTests.java | 4 ---- .../test/unit/com/structurizr/model/ModelTests.java | 1 - .../test/unit/com/structurizr/view/ContainerViewTests.java | 4 ---- 16 files changed, 31 deletions(-) diff --git a/structurizr-client/src/com/structurizr/io/json/JsonReader.java b/structurizr-client/src/com/structurizr/io/json/JsonReader.java index c85ee6665..3cca175a8 100644 --- a/structurizr-client/src/com/structurizr/io/json/JsonReader.java +++ b/structurizr-client/src/com/structurizr/io/json/JsonReader.java @@ -5,7 +5,6 @@ import com.structurizr.io.WorkspaceReader; import com.structurizr.io.WorkspaceReaderException; import com.structurizr.model.IdGenerator; -import com.structurizr.model.SequentialIntegerIdGeneratorStrategy; import java.io.IOException; import java.io.Reader; diff --git a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java index 0657d4ebf..34650ecd2 100644 --- a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import java.io.File; -import java.io.FilenameFilter; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; diff --git a/structurizr-core/src/com/structurizr/documentation/Documentable.java b/structurizr-core/src/com/structurizr/documentation/Documentable.java index 9ed7a6705..495310feb 100644 --- a/structurizr-core/src/com/structurizr/documentation/Documentable.java +++ b/structurizr-core/src/com/structurizr/documentation/Documentable.java @@ -1,7 +1,5 @@ package com.structurizr.documentation; -import com.structurizr.documentation.Documentation; - /** * Marker interface for items that can have documentation attached (i.e. workspaces and software systems). */ diff --git a/structurizr-core/src/com/structurizr/model/CustomElement.java b/structurizr-core/src/com/structurizr/model/CustomElement.java index 0b745c3f7..53105607a 100644 --- a/structurizr-core/src/com/structurizr/model/CustomElement.java +++ b/structurizr-core/src/com/structurizr/model/CustomElement.java @@ -1,7 +1,5 @@ package com.structurizr.model; -import com.structurizr.util.StringUtils; - import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collections; diff --git a/structurizr-core/src/com/structurizr/model/Element.java b/structurizr-core/src/com/structurizr/model/Element.java index 01937349a..7d4c994cd 100644 --- a/structurizr-core/src/com/structurizr/model/Element.java +++ b/structurizr-core/src/com/structurizr/model/Element.java @@ -1,7 +1,6 @@ package com.structurizr.model; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.util.Url; import javax.annotation.Nonnull; import javax.annotation.Nullable; diff --git a/structurizr-core/src/com/structurizr/model/GroupableElement.java b/structurizr-core/src/com/structurizr/model/GroupableElement.java index 3ddcf516b..cae1cb4b8 100644 --- a/structurizr-core/src/com/structurizr/model/GroupableElement.java +++ b/structurizr-core/src/com/structurizr/model/GroupableElement.java @@ -2,12 +2,6 @@ import com.structurizr.util.StringUtils; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; - /** * Represents an element that can be included in a group. */ diff --git a/structurizr-core/src/com/structurizr/model/Relationship.java b/structurizr-core/src/com/structurizr/model/Relationship.java index 61b3521e0..5c3dc2b2e 100644 --- a/structurizr-core/src/com/structurizr/model/Relationship.java +++ b/structurizr-core/src/com/structurizr/model/Relationship.java @@ -1,7 +1,6 @@ package com.structurizr.model; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.util.Url; import java.util.Collections; import java.util.LinkedHashSet; diff --git a/structurizr-core/src/com/structurizr/model/StaticStructureElement.java b/structurizr-core/src/com/structurizr/model/StaticStructureElement.java index 56d7b055e..ff6566249 100644 --- a/structurizr-core/src/com/structurizr/model/StaticStructureElement.java +++ b/structurizr-core/src/com/structurizr/model/StaticStructureElement.java @@ -1,7 +1,5 @@ package com.structurizr.model; -import com.structurizr.util.StringUtils; - import javax.annotation.Nonnull; import javax.annotation.Nullable; diff --git a/structurizr-core/src/com/structurizr/model/StaticStructureElementInstance.java b/structurizr-core/src/com/structurizr/model/StaticStructureElementInstance.java index e79c7dab4..6ed26d0c2 100644 --- a/structurizr-core/src/com/structurizr/model/StaticStructureElementInstance.java +++ b/structurizr-core/src/com/structurizr/model/StaticStructureElementInstance.java @@ -5,7 +5,6 @@ import com.structurizr.util.Url; import javax.annotation.Nonnull; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; diff --git a/structurizr-core/src/com/structurizr/view/DynamicView.java b/structurizr-core/src/com/structurizr/view/DynamicView.java index f2b566454..a8ca3eaec 100644 --- a/structurizr-core/src/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/com/structurizr/view/DynamicView.java @@ -6,7 +6,6 @@ import javax.annotation.Nonnull; import java.util.*; -import java.util.stream.Collectors; /** * A dynamic view, used to describe behaviour between static elements at runtime. diff --git a/structurizr-core/src/com/structurizr/view/ElementStyle.java b/structurizr-core/src/com/structurizr/view/ElementStyle.java index fc61528e5..75999d2ea 100644 --- a/structurizr-core/src/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/com/structurizr/view/ElementStyle.java @@ -1,7 +1,6 @@ package com.structurizr.view; import com.fasterxml.jackson.annotation.JsonInclude; -import com.structurizr.PropertyHolder; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; diff --git a/structurizr-core/src/com/structurizr/view/Styles.java b/structurizr-core/src/com/structurizr/view/Styles.java index 44a290ddc..181ee0ce7 100644 --- a/structurizr-core/src/com/structurizr/view/Styles.java +++ b/structurizr-core/src/com/structurizr/view/Styles.java @@ -1,6 +1,5 @@ package com.structurizr.view; -import com.structurizr.Workspace; import com.structurizr.model.*; import com.structurizr.util.StringUtils; diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java index 447687bca..67e84dfd0 100644 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java @@ -7,8 +7,6 @@ import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; -import java.io.File; - import static org.junit.jupiter.api.Assertions.*; public class WorkspaceTests { diff --git a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java b/structurizr-core/test/unit/com/structurizr/model/ElementTests.java index 6dfbe398b..1bf94229e 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ElementTests.java @@ -1,12 +1,8 @@ package com.structurizr.model; import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.Workspace; import org.junit.jupiter.api.Test; -import java.util.HashMap; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.*; public class ElementTests extends AbstractWorkspaceTestBase { diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java index 1fc5381a6..3d429a746 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.Set; import static org.junit.jupiter.api.Assertions.*; diff --git a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java index 801e481bd..2c0c52b9a 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java @@ -1,14 +1,10 @@ package com.structurizr.view; import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.Workspace; import com.structurizr.model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.HashMap; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.*; public class ContainerViewTests extends AbstractWorkspaceTestBase { From 3b73e6a5b82bb0c434c81862d1128014b6a73062 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Aug 2022 16:27:43 +0100 Subject: [PATCH 007/418] Updated the build instructions. --- docs/building.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/building.md b/docs/building.md index f6f8b2402..fdb06e217 100644 --- a/docs/building.md +++ b/docs/building.md @@ -2,16 +2,10 @@ [![Build Status](https://travis-ci.org/structurizr/java.svg?branch=master)](https://travis-ci.org/structurizr/java) -To build "Structurizr for Java" from the sources (you'll need Java 8)... +To build "Structurizr for Java" from the sources (you'll need Java 11+ installed)... ``` -git clone https://github.com/structurizr/java.git -cd java +git clone https://github.com/structurizr/java.git structurizr-java +cd structurizr-java ./gradlew compileJava test -``` - -If necessary, after building, you can install "Structurizr for Java" into your local Maven repo using: - -``` -./gradlew publishToMavenLocal ``` \ No newline at end of file From 3e9fa2f7cbe9f7d40e0b399d8ac71e543d52d6bd Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Aug 2022 16:28:08 +0100 Subject: [PATCH 008/418] Added some Javadoc. --- .../src/com/structurizr/view/Styles.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/structurizr-core/src/com/structurizr/view/Styles.java b/structurizr-core/src/com/structurizr/view/Styles.java index 44a290ddc..8d4059a62 100644 --- a/structurizr-core/src/com/structurizr/view/Styles.java +++ b/structurizr-core/src/com/structurizr/view/Styles.java @@ -145,6 +145,16 @@ public RelationshipStyle findRelationshipStyle(String tag) { return style; } + /** + * Finds the element style used to render the specified element, according to the following rules: + * + * 1. Start with a default style. + * 2. Calculate set of tags associated with the element. + * 3. Find the style properties for each tag (themes first, followed by workspace styles) + * + * @param element an Element object + * @return an ElementStyle object + */ public ElementStyle findElementStyle(Element element) { ElementStyle style = new ElementStyle("").background("#dddddd").color("#000000").shape(Shape.Box).fontSize(24).border(Border.Solid).opacity(100).metadata(true).description(true); @@ -199,6 +209,16 @@ public ElementStyle findElementStyle(Element element) { return style; } + /** + * Finds the relationship style used to render the specified relationship, according to the following rules: + * + * 1. Start with a default style. + * 2. Calculate set of tags associated with the relationship, and any linked relationship(s). + * 3. Find the style properties for each tag (themes first, followed by workspace styles) + * + * @param relationship a Relationship object + * @return a RelationshipStyle object + */ public RelationshipStyle findRelationshipStyle(Relationship relationship) { RelationshipStyle style = new RelationshipStyle("").thickness(2).color("#707070").dashed(true).routing(Routing.Direct).fontSize(24).width(200).position(50).opacity(100); From 440e5595303cd8f2704e29d23d239b498ec2fc62 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Aug 2022 16:30:55 +0100 Subject: [PATCH 009/418] Removed the Travis build and status. --- .travis.yml | 9 --------- docs/building.md | 2 -- 2 files changed, 11 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 14e654e53..000000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: java - -jdk: - - oraclejdk8 - -install: true - -script: - - ./gradlew --info \ No newline at end of file diff --git a/docs/building.md b/docs/building.md index fdb06e217..8e9891a34 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,7 +1,5 @@ # Building -[![Build Status](https://travis-ci.org/structurizr/java.svg?branch=master)](https://travis-ci.org/structurizr/java) - To build "Structurizr for Java" from the sources (you'll need Java 11+ installed)... ``` From 63442f4b0e064d4d52cfdb7bc83b9e6895824e73 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Aug 2022 16:34:36 +0100 Subject: [PATCH 010/418] Updated build instructions again. --- docs/building.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index 8e9891a34..8eed1c9ae 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,6 +1,6 @@ # Building -To build "Structurizr for Java" from the sources (you'll need Java 11+ installed)... +To build this repo from the sources (you'll need `git` and Java 11+ installed)... ``` git clone https://github.com/structurizr/java.git structurizr-java From 6ac239077edac55b90225dd75d4743b793bd606e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Aug 2022 16:43:27 +0100 Subject: [PATCH 011/418] Updated build files. --- build.gradle | 2 +- docs/building.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5200d34b9..4c6b854f2 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ subprojects { proj -> compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' - sourceCompatibility = 1.8 + sourceCompatibility = 1.11 targetCompatibility = 1.8 java { diff --git a/docs/building.md b/docs/building.md index 8eed1c9ae..a68e36640 100644 --- a/docs/building.md +++ b/docs/building.md @@ -5,5 +5,5 @@ To build this repo from the sources (you'll need `git` and Java 11+ installed).. ``` git clone https://github.com/structurizr/java.git structurizr-java cd structurizr-java -./gradlew compileJava test +./gradlew ``` \ No newline at end of file From 2bbc461ccac077195900ea821475a0c1e545813e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Aug 2022 16:45:19 +0100 Subject: [PATCH 012/418] Fixed. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4c6b854f2..5200d34b9 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ subprojects { proj -> compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' - sourceCompatibility = 1.11 + sourceCompatibility = 1.8 targetCompatibility = 1.8 java { From 702a4de94ad76ed79c7b30d385067d76d2d8d085 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Aug 2022 16:49:37 +0100 Subject: [PATCH 013/418] Updated to reflect release. --- docs/changelog.md | 2 +- docs/getting-started.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 10c2116f9..ee2c041d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.14.0 (unreleased to Maven Central) +## 1.14.0 (14th August 2022) - Adds a helper method (`AbstractImpliedRelationshipsStrategy.createImpliedRelationship`) to create implied relationships, which can then be used by custom implementations. - Provides a way to add specific relationships to dynamic views. diff --git a/docs/getting-started.md b/docs/getting-started.md index 43673ba73..ff169ba21 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,7 +12,7 @@ The Structurizr for Java binaries are hosted on [Maven Central](https://repo1.ma Name | Description ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- -com.structurizr:structurizr-client:1.12.1 | The Structurizr API client library. +com.structurizr:structurizr-client:1.14.0 | The Structurizr API client library. ## 2. Create a Java program From d0697c50794a4f705b8e5cb4705d39394649289c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 12:12:36 +0100 Subject: [PATCH 014/418] Enables `structurizr-core` to be used as a transitive dependency by consumers of `structurizr-client`. --- build.gradle | 4 ++-- docs/changelog.md | 4 ++++ docs/getting-started.md | 2 +- structurizr-client/build.gradle | 2 +- structurizr-core/build.gradle | 1 + 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 5200d34b9..0136eac58 100644 --- a/build.gradle +++ b/build.gradle @@ -2,13 +2,13 @@ defaultTasks 'clean', 'compileJava', 'test' subprojects { proj -> - apply plugin: 'java' + apply plugin: 'java-library' apply plugin: 'maven-publish' apply plugin: 'signing' description = 'Structurizr' group = 'com.structurizr' - version = '1.14.0' + version = '1.14.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index ee2c041d4..e6fa45ba8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.14.1 (15th August 2022) + +- Enables `structurizr-core` to be used as a transitive dependency by consumers of `structurizr-client`. + ## 1.14.0 (14th August 2022) - Adds a helper method (`AbstractImpliedRelationshipsStrategy.createImpliedRelationship`) to create implied relationships, which can then be used by custom implementations. diff --git a/docs/getting-started.md b/docs/getting-started.md index ff169ba21..79b918070 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,7 +12,7 @@ The Structurizr for Java binaries are hosted on [Maven Central](https://repo1.ma Name | Description ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- -com.structurizr:structurizr-client:1.14.0 | The Structurizr API client library. +com.structurizr:structurizr-client:1.14.1 | The Structurizr API client library. ## 2. Create a Java program diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 6c5bd755d..5b059708c 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -1,6 +1,6 @@ dependencies { - implementation project(':structurizr-core') + api project(':structurizr-core') implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 4f395e47f..8b7fdb3ce 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -7,4 +7,5 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:3.9.1' + } \ No newline at end of file From c312f03a930f19d1620280a72f13253ca6bacbdf Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 12:34:16 +0100 Subject: [PATCH 015/418] Reintroduce client integration tests into the build ... these were removed during the Gradle 7 upgrade. --- build.gradle | 4 ++++ structurizr-client/build.gradle | 14 ++++++++++---- structurizr-core/build.gradle | 5 ++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 0136eac58..9099392bb 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,10 @@ subprojects { proj -> } } + test { + useJUnitPlatform() + } + compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 15e4035ee..2660a56ad 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -10,9 +10,15 @@ dependencies { implementation 'commons-logging:commons-logging:1.2' - testImplementation(platform('org.junit:junit-bom:5.9.0')) - testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' + } -test { - useJUnitPlatform() + +sourceSets { + test { + java { + srcDir 'test/unit' + srcDir 'test/integration' + } + } } \ No newline at end of file diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 25170ef20..08250abb3 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -5,8 +5,7 @@ dependencies { implementation 'commons-logging:commons-logging:1.2' - testImplementation(platform('org.junit:junit-bom:5.9.0')) - testImplementation('org.junit.jupiter:junit-jupiter') - testImplementation 'org.assertj:assertj-core:3.9.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' + testImplementation 'org.assertj:assertj-core:3.23.1' } \ No newline at end of file From 8a7d3552db815571504d95a62ad21162d0725970 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 12:45:26 +0100 Subject: [PATCH 016/418] Remove `test_` prefix from method names. --- .../StructurizrClientIntegrationTests.java | 8 +- .../api/WorkspaceRulesValidationTests.java | 36 ++--- .../com/structurizr/api/ApiResponseTests.java | 2 +- ...shBasedMessageAuthenticationCodeTests.java | 2 +- .../api/HmacAuthorizationHeaderTests.java | 4 +- .../com/structurizr/api/HmacContentTests.java | 4 +- .../com/structurizr/api/Md5DigestTests.java | 4 +- .../api/StructurizrClientTests.java | 34 ++--- .../AesEncryptionStrategyTests.java | 16 +- .../encryption/EncryptedWorkspaceTests.java | 8 +- .../io/json/EncryptedJsonTests.java | 2 +- .../io/json/EncryptedJsonWriterTests.java | 4 +- .../com/structurizr/io/json/JsonTests.java | 6 +- .../structurizr/io/json/JsonWriterTests.java | 4 +- .../structurizr/util/WorkspaceUtilsTests.java | 20 +-- .../com/structurizr/view/ThemeUtilsTests.java | 10 +- .../unit/com/structurizr/WorkspaceTests.java | 12 +- .../WorkspaceConfigurationTests.java | 8 +- .../documentation/DecisionTests.java | 2 +- .../documentation/DocumentationTests.java | 22 +-- .../documentation/SectionTests.java | 2 +- .../structurizr/model/CodeElementTests.java | 36 ++--- .../com/structurizr/model/ComponentTests.java | 32 ++-- .../model/ContainerInstanceTests.java | 38 ++--- .../com/structurizr/model/ContainerTests.java | 38 ++--- ...essAnyRelationshipExistsStrategyTests.java | 4 +- ...ssSameRelationshipExistsStrategyTests.java | 2 +- .../structurizr/model/CustomElementTests.java | 18 +-- ...aultImpliedRelationshipsStrategyTests.java | 2 +- .../model/DeploymentNodeTests.java | 40 ++--- .../com/structurizr/model/ElementTests.java | 58 +++---- .../model/GroupableElementTests.java | 8 +- .../model/HttpHealthCheckTests.java | 14 +- .../model/InfrastructureNodeTests.java | 8 +- .../com/structurizr/model/ModelItemTests.java | 46 +++--- .../com/structurizr/model/ModelTests.java | 114 +++++++------- .../com/structurizr/model/PersonTests.java | 16 +- .../structurizr/model/RelationshipTests.java | 26 ++-- .../model/SoftwareSystemInstanceTests.java | 38 ++--- .../model/SoftwareSystemTests.java | 46 +++--- .../com/structurizr/util/ImageUtilsTests.java | 34 ++--- .../structurizr/util/StringUtilsTests.java | 6 +- .../unit/com/structurizr/util/UrlTests.java | 8 +- .../view/AutomaticLayoutTests.java | 10 +- .../com/structurizr/view/BrandingTests.java | 12 +- .../com/structurizr/view/ColorPairTests.java | 18 +-- .../unit/com/structurizr/view/ColorTests.java | 8 +- .../structurizr/view/ComponentViewTests.java | 94 ++++++------ .../structurizr/view/ConfigurationTests.java | 16 +- .../structurizr/view/ContainerViewTests.java | 42 ++--- .../view/DefaultLayoutMergeStrategyTests.java | 14 +- .../structurizr/view/DeploymentViewTests.java | 66 ++++---- .../com/structurizr/view/DimensionsTests.java | 6 +- .../structurizr/view/DynamicViewTests.java | 48 +++--- .../structurizr/view/ElementStyleTests.java | 56 +++---- .../structurizr/view/ElementViewTests.java | 4 +- .../structurizr/view/FilteredViewTests.java | 2 +- .../unit/com/structurizr/view/FontTests.java | 8 +- .../com/structurizr/view/PaperSizeTests.java | 6 +- .../view/RelationshipStyleTests.java | 36 ++--- .../view/SequenceCounterTests.java | 2 +- .../structurizr/view/SequenceNumberTests.java | 4 +- .../com/structurizr/view/StaticViewTests.java | 8 +- .../com/structurizr/view/StylesTests.java | 38 ++--- .../view/SystemContextViewTests.java | 44 +++--- .../view/SystemLandscapeViewTests.java | 34 ++--- .../structurizr/view/TerminologyTests.java | 2 +- .../com/structurizr/view/VertexTests.java | 2 +- .../com/structurizr/view/ViewSetTests.java | 144 +++++++++--------- .../unit/com/structurizr/view/ViewTests.java | 60 ++++---- 70 files changed, 813 insertions(+), 813 deletions(-) diff --git a/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java b/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java index 3c0fd2ba7..48712d3ce 100644 --- a/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java +++ b/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java @@ -51,7 +51,7 @@ private File getArchivedWorkspace() { } @Test - void test_putAndGetWorkspace_WithoutEncryption() throws Exception { + void putAndGetWorkspace_WithoutEncryption() throws Exception { Workspace workspace = new Workspace("Structurizr client library tests - without encryption", "A test workspace for the Structurizr client library"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); Person person = workspace.getModel().addPerson("Person", "Description"); @@ -77,7 +77,7 @@ void test_putAndGetWorkspace_WithoutEncryption() throws Exception { } @Test - void test_putAndGetWorkspace_WithEncryption() throws Exception { + void putAndGetWorkspace_WithEncryption() throws Exception { structurizrClient.setEncryptionStrategy(new AesEncryptionStrategy("password")); Workspace workspace = new Workspace("Structurizr client library tests - with encryption", "A test workspace for the Structurizr client library"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -104,14 +104,14 @@ void test_putAndGetWorkspace_WithEncryption() throws Exception { } @Test - void test_lockWorkspace() throws Exception { + void lockWorkspace() throws Exception { structurizrClient.unlockWorkspace(20081); assertTrue(structurizrClient.lockWorkspace(20081)); } @Test - void test_unlockWorkspace() throws Exception { + void unlockWorkspace() throws Exception { structurizrClient.lockWorkspace(20081); assertTrue(structurizrClient.unlockWorkspace(20081)); } diff --git a/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java b/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java index 4319f7128..efd07a640 100644 --- a/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java +++ b/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java @@ -13,7 +13,7 @@ public class WorkspaceRulesValidationTests { private static final File PATH_TO_WORKSPACE_FILES = new File("test/integration/workspaceValidation"); @Test - void test_exceptionThrown_WhenElementIdsAreNotUnique() throws Exception { + void exceptionThrown_WhenElementIdsAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementIdsAreNotUnique.json")); fail(); @@ -24,7 +24,7 @@ void test_exceptionThrown_WhenElementIdsAreNotUnique() throws Exception { } @Test - void test_exceptionThrown_WhenRelationshipIdsAreNotUnique() throws Exception { + void exceptionThrown_WhenRelationshipIdsAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipIdsAreNotUnique.json")); fail(); @@ -35,7 +35,7 @@ void test_exceptionThrown_WhenRelationshipIdsAreNotUnique() throws Exception { } @Test - void test_exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { + void exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ViewKeysAreNotUnique.json")); fail(); @@ -45,7 +45,7 @@ void test_exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { } @Test - void test_exceptionThrown_WhenPeopleAndSoftwareSystemNamesAreNotUnique() throws Exception { + void exceptionThrown_WhenPeopleAndSoftwareSystemNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "PeopleAndSoftwareSystemNamesAreNotUnique.json")); fail(); @@ -55,7 +55,7 @@ void test_exceptionThrown_WhenPeopleAndSoftwareSystemNamesAreNotUnique() throws } @Test - void test_exceptionThrown_WhenContainerNamesAreNotUnique() throws Exception { + void exceptionThrown_WhenContainerNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ContainerNamesAreNotUnique.json")); fail(); @@ -65,7 +65,7 @@ void test_exceptionThrown_WhenContainerNamesAreNotUnique() throws Exception { } @Test - void test_exceptionThrown_WhenComponentNamesAreNotUnique() throws Exception { + void exceptionThrown_WhenComponentNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ComponentNamesAreNotUnique.json")); fail(); @@ -75,7 +75,7 @@ void test_exceptionThrown_WhenComponentNamesAreNotUnique() throws Exception { } @Test - void test_exceptionThrown_WhenTopLevelDeploymentNodeNamesAreNotUnique() throws Exception { + void exceptionThrown_WhenTopLevelDeploymentNodeNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "TopLevelDeploymentNodeNamesAreNotUnique.json")); fail(); @@ -85,12 +85,12 @@ void test_exceptionThrown_WhenTopLevelDeploymentNodeNamesAreNotUnique() throws E } @Test - void test_exceptionNotThrown_WhenTopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments() throws Exception { + void exceptionNotThrown_WhenTopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments() throws Exception { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json")); } @Test - void test_exceptionThrown_WhenChildDeploymentNodeNamesAreNotUnique() throws Exception { + void exceptionThrown_WhenChildDeploymentNodeNamesAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ChildDeploymentNodeNamesAreNotUnique.json")); fail(); @@ -100,7 +100,7 @@ void test_exceptionThrown_WhenChildDeploymentNodeNamesAreNotUnique() throws Exce } @Test - void test_exceptionThrown_WhenRelationshipDescriptionsAreNotUnique() throws Exception { + void exceptionThrown_WhenRelationshipDescriptionsAreNotUnique() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipDescriptionsAreNotUnique.json")); fail(); @@ -110,7 +110,7 @@ void test_exceptionThrown_WhenRelationshipDescriptionsAreNotUnique() throws Exce } @Test - void test_exceptionThrown_WhenSoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel() throws Exception { + void exceptionThrown_WhenSoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json")); fail(); @@ -120,7 +120,7 @@ void test_exceptionThrown_WhenSoftwareSystemAssociatedWithSystemContextViewIsMis } @Test - void test_exceptionThrown_WhenSoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel() throws Exception { + void exceptionThrown_WhenSoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json")); fail(); @@ -130,7 +130,7 @@ void test_exceptionThrown_WhenSoftwareSystemAssociatedWithContainerViewIsMissing } @Test - void test_exceptionThrown_WhenContainerAssociatedWithComponentViewIsMissingFromTheModel() throws Exception { + void exceptionThrown_WhenContainerAssociatedWithComponentViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ContainerAssociatedWithComponentViewIsMissingFromTheModel.json")); fail(); @@ -140,7 +140,7 @@ void test_exceptionThrown_WhenContainerAssociatedWithComponentViewIsMissingFromT } @Test - void test_exceptionThrown_WhenElementAssociatedWithDynamicViewIsMissingFromTheModel() throws Exception { + void exceptionThrown_WhenElementAssociatedWithDynamicViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementAssociatedWithDynamicViewIsMissingFromTheModel.json")); fail(); @@ -150,7 +150,7 @@ void test_exceptionThrown_WhenElementAssociatedWithDynamicViewIsMissingFromTheMo } @Test - void test_exceptionThrown_WhenSoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel() throws Exception { + void exceptionThrown_WhenSoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json")); fail(); @@ -160,7 +160,7 @@ void test_exceptionThrown_WhenSoftwareSystemAssociatedWithDeploymentViewIsMissin } @Test - void test_exceptionThrown_WhenViewAssociatedWithFilteredViewIsMissingFromTheWorkspace() throws Exception { + void exceptionThrown_WhenViewAssociatedWithFilteredViewIsMissingFromTheWorkspace() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json")); fail(); @@ -170,7 +170,7 @@ void test_exceptionThrown_WhenViewAssociatedWithFilteredViewIsMissingFromTheWork } @Test - void test_exceptionThrown_WhenElementReferencedByViewIsMissingFromTheModel() throws Exception { + void exceptionThrown_WhenElementReferencedByViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementReferencedByViewIsMissingFromTheModel.json")); fail(); @@ -180,7 +180,7 @@ void test_exceptionThrown_WhenElementReferencedByViewIsMissingFromTheModel() thr } @Test - void test_exceptionThrown_WhenRelationshipReferencedByViewIsMissingFromTheModel() throws Exception { + void exceptionThrown_WhenRelationshipReferencedByViewIsMissingFromTheModel() throws Exception { try { WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipReferencedByViewIsMissingFromTheModel.json")); fail(); diff --git a/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java b/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java index 0c416cc28..d3546f95a 100644 --- a/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java @@ -7,7 +7,7 @@ public class ApiResponseTests { @Test - void test_parse_createsAnApiErrorObjectWithTheSpecifiedErrorMessage() throws Exception { + void parse_createsAnApiErrorObjectWithTheSpecifiedErrorMessage() throws Exception { ApiResponse apiResponse = ApiResponse.parse("{\"message\": \"Hello\"}"); assertEquals("Hello", apiResponse.getMessage()); } diff --git a/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java b/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java index 08a930ccd..539d7a733 100644 --- a/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java @@ -9,7 +9,7 @@ public class HashBasedMessageAuthenticationCodeTests { private HashBasedMessageAuthenticationCode code; @Test - void test_generate() throws Exception { + void generate() throws Exception { // this example is taken from http://en.wikipedia.org/wiki/Hash-based_message_authentication_code code = new HashBasedMessageAuthenticationCode("key"); assertEquals("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", code.generate("The quick brown fox jumps over the lazy dog")); diff --git a/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java b/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java index df5ff9166..86c79a6c6 100644 --- a/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java @@ -9,13 +9,13 @@ public class HmacAuthorizationHeaderTests { private HmacAuthorizationHeader header; @Test - void test_format() { + void format() { header = new HmacAuthorizationHeader("apiKey", "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"); assertEquals("apiKey:ZjdiYzgzZjQzMDUzODQyNGIxMzI5OGU2YWE2ZmIxNDNlZjRkNTlhMTQ5NDYxNzU5OTc0NzlkYmMyZDFhM2NkOA==", header.format()); } @Test - void test_parse() { + void parse() { header = HmacAuthorizationHeader.parse("apiKey:ZjdiYzgzZjQzMDUzODQyNGIxMzI5OGU2YWE2ZmIxNDNlZjRkNTlhMTQ5NDYxNzU5OTc0NzlkYmMyZDFhM2NkOA=="); assertEquals("apiKey", header.getApiKey()); assertEquals("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", header.getHmac()); diff --git a/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java b/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java index 3ee024bdd..ba81a6b96 100644 --- a/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java @@ -8,12 +8,12 @@ public class HmacContentTests { @Test - void test_toString_WhenThereAreNoStrings() { + void toString_WhenThereAreNoStrings() { assertEquals("", new HmacContent().toString()); } @Test - void test_toString_WhenThereAreSomeStrings() { + void toString_WhenThereAreSomeStrings() { assertEquals("String1\nString2\nString3\n", new HmacContent("String1", "String2", "String3").toString()); } diff --git a/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java b/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java index c880e38b3..220270830 100644 --- a/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java @@ -9,12 +9,12 @@ public class Md5DigestTests { private Md5Digest md5 = new Md5Digest(); @Test - void test_generate_TreatsNullAsEmptyContent() throws Exception { + void generate_TreatsNullAsEmptyContent() throws Exception { assertEquals(md5.generate(null), md5.generate("")); } @Test - void test_generate() throws Exception { + void generate() throws Exception { assertEquals("ed076287532e86365e841e92bfc50d8c", md5.generate("Hello World!")); assertEquals("d41d8cd98f00b204e9800998ecf8427e", md5.generate("")); } diff --git a/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java b/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java index c7d4ecd2a..4b7fdef59 100644 --- a/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java +++ b/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java @@ -10,7 +10,7 @@ public class StructurizrClientTests { private StructurizrClient structurizrClient; @Test - void test_construction_WithTwoParameters() { + void construction_WithTwoParameters() { structurizrClient = new StructurizrClient("key", "secret"); assertEquals("https://api.structurizr.com", structurizrClient.getUrl()); assertEquals("key", structurizrClient.getApiKey()); @@ -18,7 +18,7 @@ void test_construction_WithTwoParameters() { } @Test - void test_construction_WithThreeParameters() { + void construction_WithThreeParameters() { structurizrClient = new StructurizrClient("https://localhost", "key", "secret"); assertEquals("https://localhost", structurizrClient.getUrl()); assertEquals("key", structurizrClient.getApiKey()); @@ -26,7 +26,7 @@ void test_construction_WithThreeParameters() { } @Test - void test_construction_WithThreeParameters_TruncatesTheApiUrl_WhenTheApiUrlHasATrailingSlashCharacter() { + void construction_WithThreeParameters_TruncatesTheApiUrl_WhenTheApiUrlHasATrailingSlashCharacter() { structurizrClient = new StructurizrClient("https://localhost/", "key", "secret"); assertEquals("https://localhost", structurizrClient.getUrl()); assertEquals("key", structurizrClient.getApiKey()); @@ -34,7 +34,7 @@ void test_construction_WithThreeParameters_TruncatesTheApiUrl_WhenTheApiUrlHasAT } @Test - void test_construction_ThrowsAnException_WhenANullApiKeyIsUsed() { + void construction_ThrowsAnException_WhenANullApiKeyIsUsed() { try { structurizrClient = new StructurizrClient(null, "secret"); fail(); @@ -44,7 +44,7 @@ void test_construction_ThrowsAnException_WhenANullApiKeyIsUsed() { } @Test - void test_construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { + void construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { try { structurizrClient = new StructurizrClient(" ", "secret"); fail(); @@ -54,7 +54,7 @@ void test_construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { } @Test - void test_construction_ThrowsAnException_WhenANullApiSecretIsUsed() { + void construction_ThrowsAnException_WhenANullApiSecretIsUsed() { try { structurizrClient = new StructurizrClient("key", null); fail(); @@ -64,7 +64,7 @@ void test_construction_ThrowsAnException_WhenANullApiSecretIsUsed() { } @Test - void test_construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { + void construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { try { structurizrClient = new StructurizrClient("key", " "); fail(); @@ -74,7 +74,7 @@ void test_construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { } @Test - void test_construction_ThrowsAnException_WhenANullApiUrlIsUsed() { + void construction_ThrowsAnException_WhenANullApiUrlIsUsed() { try { structurizrClient = new StructurizrClient(null, "key", "secret"); fail(); @@ -84,7 +84,7 @@ void test_construction_ThrowsAnException_WhenANullApiUrlIsUsed() { } @Test - void test_construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { + void construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { try { structurizrClient = new StructurizrClient(" ", "key", "secret"); fail(); @@ -94,7 +94,7 @@ void test_construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { } @Test - void test_getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { + void getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { try { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.getWorkspace(0); @@ -105,7 +105,7 @@ void test_getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws E } @Test - void test_putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { + void putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { try { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.putWorkspace(0, new Workspace("Name", "Description")); @@ -116,7 +116,7 @@ void test_putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws E } @Test - void test_putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws Exception { + void putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws Exception { try { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.putWorkspace(1234, null); @@ -127,7 +127,7 @@ void test_putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws } @Test - void test_constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropertiesAreFound() { + void constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropertiesAreFound() { try { structurizrClient = new StructurizrClient(); fail(); @@ -137,20 +137,20 @@ void test_constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropertiesAreF } @Test - void test_getAgent() { + void getAgent() { structurizrClient = new StructurizrClient("key", "secret"); assertTrue(structurizrClient.getAgent().startsWith("structurizr-java/")); } @Test - void test_setAgent() { + void setAgent() { structurizrClient = new StructurizrClient("key", "secret"); structurizrClient.setAgent("new_agent"); assertEquals("new_agent", structurizrClient.getAgent()); } @Test - void test_setAgent_ThrowsAnException_WhenPassedNull() { + void setAgent_ThrowsAnException_WhenPassedNull() { structurizrClient = new StructurizrClient("key", "secret"); try { @@ -162,7 +162,7 @@ void test_setAgent_ThrowsAnException_WhenPassedNull() { } @Test - void test_setAgent_ThrowsAnException_WhenPassedAnEmptyString() { + void setAgent_ThrowsAnException_WhenPassedAnEmptyString() { structurizrClient = new StructurizrClient("key", "secret"); try { diff --git a/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java b/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java index 8b390f8f6..3af514905 100644 --- a/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java +++ b/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java @@ -10,7 +10,7 @@ public class AesEncryptionStrategyTests { @Test - void test_encrypt_EncryptsPlaintext() throws Exception { + void encrypt_EncryptsPlaintext() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "06DC30A48ADEEE72D98E33C2CEAEAD3E", "ED124530AF64A5CAD8EF463CF5628434", "password"); String ciphertext = strategy.encrypt("Hello world"); @@ -18,7 +18,7 @@ void test_encrypt_EncryptsPlaintext() throws Exception { } @Test - void test_decrypt_decryptsTheCiphertext_WhenTheSameStrategyInstanceIsUsed() throws Exception { + void decrypt_decryptsTheCiphertext_WhenTheSameStrategyInstanceIsUsed() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); String ciphertext = strategy.encrypt("Hello world"); @@ -26,7 +26,7 @@ void test_decrypt_decryptsTheCiphertext_WhenTheSameStrategyInstanceIsUsed() thro } @Test - void test_decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() throws Exception { + void decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); String ciphertext = strategy.encrypt("Hello world"); @@ -36,7 +36,7 @@ void test_decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() throws } @Test - void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectKeySizeIsUsed() throws Exception { + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectKeySizeIsUsed() throws Exception { try { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); @@ -53,7 +53,7 @@ void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectKeySizeIsUsed() th } @Test - void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUsed() throws Exception { + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUsed() throws Exception { assertThrows(BadPaddingException.class, () -> { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); @@ -65,7 +65,7 @@ void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUs } @Test - void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throws Exception { + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throws Exception { assertThrows(BadPaddingException.class, () -> { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); @@ -77,7 +77,7 @@ void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throw } @Test - void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws Exception { + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws Exception { assertThrows(BadPaddingException.class, () -> { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); @@ -89,7 +89,7 @@ void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws } @Test - void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectPassphraseIsUsed() throws Exception { + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectPassphraseIsUsed() throws Exception { assertThrows(BadPaddingException.class, () -> { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); diff --git a/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java b/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java index 81a87b6a0..a7e508781 100644 --- a/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java +++ b/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java @@ -29,7 +29,7 @@ public void setUp() throws Exception { } @Test - void test_construction_WhenTwoParametersAreSpecified() throws Exception { + void construction_WhenTwoParametersAreSpecified() throws Exception { encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); assertEquals("Name", encryptedWorkspace.getName()); @@ -53,7 +53,7 @@ void test_construction_WhenTwoParametersAreSpecified() throws Exception { } @Test - void test_construction_WhenThreeParametersAreSpecified() throws Exception { + void construction_WhenThreeParametersAreSpecified() throws Exception { JsonWriter jsonWriter = new JsonWriter(false); StringWriter stringWriter = new StringWriter(); jsonWriter.write(workspace, stringWriter); @@ -75,7 +75,7 @@ void test_construction_WhenThreeParametersAreSpecified() throws Exception { } @Test - void test_getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws Exception { + void getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws Exception { encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); String cipherText = encryptedWorkspace.getCiphertext(); @@ -87,7 +87,7 @@ void test_getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws Except } @Test - void test_getWorkspace_ReturnsTheWorkspace_WhenACipherextIsSpecified() throws Exception { + void getWorkspace_ReturnsTheWorkspace_WhenACipherextIsSpecified() throws Exception { JsonWriter jsonWriter = new JsonWriter(false); StringWriter stringWriter = new StringWriter(); jsonWriter.write(workspace, stringWriter); diff --git a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java index 14ed4f11e..c598cd06c 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java @@ -13,7 +13,7 @@ public class EncryptedJsonTests { @Test - void test_write_and_read() throws Exception { + void write_and_read() throws Exception { final Workspace workspace1 = new Workspace("Name", "Description"); // output the model as JSON diff --git a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java index eccb79d45..74b40614c 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java @@ -13,7 +13,7 @@ public class EncryptedJsonWriterTests { @Test - void test_write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorkspaceIsSpecified() throws Exception { + void write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorkspaceIsSpecified() throws Exception { try { EncryptedJsonWriter writer = new EncryptedJsonWriter(true); writer.write(null, new StringWriter()); @@ -24,7 +24,7 @@ void test_write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorkspaceIsSp } @Test - void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { + void write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { try { EncryptedJsonWriter writer = new EncryptedJsonWriter(true); Workspace workspace = new Workspace("Name", "Description"); diff --git a/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java b/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java index 2c436c2a1..f394c1ace 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java @@ -14,7 +14,7 @@ public class JsonTests { @Test - void test_write_and_read() throws Exception { + void write_and_read() throws Exception { final Workspace workspace1 = new Workspace("Name", "Description"); // output the model as JSON @@ -31,7 +31,7 @@ void test_write_and_read() throws Exception { } @Test - void test_backwardsCompatibilityOfRenamingEnterpriseContextViewsToSystemLandscapeViews() throws Exception { + void backwardsCompatibilityOfRenamingEnterpriseContextViewsToSystemLandscapeViews() throws Exception { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key", "description"); @@ -48,7 +48,7 @@ void test_backwardsCompatibilityOfRenamingEnterpriseContextViewsToSystemLandscap } @Test - void test_write_and_read_withCustomIdGenerator() throws Exception { + void write_and_read_withCustomIdGenerator() throws Exception { Workspace workspace1 = new Workspace("Name", "Description"); workspace1.getModel().setIdGenerator(new CustomIdGenerator()); Person user = workspace1.getModel().addPerson("User"); diff --git a/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java b/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java index bb7a8301a..4db62acb8 100644 --- a/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java +++ b/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java @@ -11,7 +11,7 @@ public class JsonWriterTests { @Test - void test_write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpecified() throws Exception { + void write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpecified() throws Exception { try { JsonWriter writer = new JsonWriter(true); writer.write(null, new StringWriter()); @@ -22,7 +22,7 @@ void test_write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpecified() } @Test - void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { + void write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { try { JsonWriter writer = new JsonWriter(true); Workspace workspace = new Workspace("Name", "Description"); diff --git a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java index 34650ecd2..c6e3c0188 100644 --- a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java @@ -11,7 +11,7 @@ public class WorkspaceUtilsTests { @Test - void test_loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecified() { + void loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecified() { try { WorkspaceUtils.loadWorkspaceFromJson(null); fail(); @@ -21,7 +21,7 @@ void test_loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecified() { } @Test - void test_loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist() { + void loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist() { try { WorkspaceUtils.loadWorkspaceFromJson(new File("test/unit/com/structurizr/util/other-workspace.json")); fail(); @@ -31,7 +31,7 @@ void test_loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist() { } @Test - void test_saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpecified() { + void saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpecified() { try { WorkspaceUtils.saveWorkspaceToJson(null, null); fail(); @@ -41,7 +41,7 @@ void test_saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpecified() } @Test - void test_saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified() { + void saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified() { try { WorkspaceUtils.saveWorkspaceToJson(new Workspace("Name", "Description"), null); fail(); @@ -51,7 +51,7 @@ void test_saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified() { } @Test - void test_saveWorkspaceToJson_and_loadWorkspaceFromJson() throws Exception { + void saveWorkspaceToJson_and_loadWorkspaceFromJson() throws Exception { File file = new File("build/workspace-utils.json"); Workspace workspace = new Workspace("Name", "Description"); WorkspaceUtils.saveWorkspaceToJson(workspace, file); @@ -61,7 +61,7 @@ void test_saveWorkspaceToJson_and_loadWorkspaceFromJson() throws Exception { } @Test - void test_toJson_ThrowsAnException_WhenANullWorkspaceIsProvided() throws Exception { + void toJson_ThrowsAnException_WhenANullWorkspaceIsProvided() throws Exception { try { WorkspaceUtils.toJson(null, true); fail(); @@ -71,7 +71,7 @@ void test_toJson_ThrowsAnException_WhenANullWorkspaceIsProvided() throws Excepti } @Test - void test_toJson() throws Exception { + void toJson() throws Exception { Workspace workspace = new Workspace("Name", "Description"); String indentedOutput = WorkspaceUtils.toJson(workspace, true); String unindentedOutput = WorkspaceUtils.toJson(workspace, false); @@ -95,7 +95,7 @@ void test_toJson() throws Exception { } @Test - void test_fromJson_ThrowsAnException_WhenANullJsonStringIsProvided() throws Exception { + void fromJson_ThrowsAnException_WhenANullJsonStringIsProvided() throws Exception { try { WorkspaceUtils.fromJson(null); fail(); @@ -105,7 +105,7 @@ void test_fromJson_ThrowsAnException_WhenANullJsonStringIsProvided() throws Exce } @Test - void test_fromJson_ThrowsAnException_WhenAnEmptyJsonStringIsProvided() throws Exception { + void fromJson_ThrowsAnException_WhenAnEmptyJsonStringIsProvided() throws Exception { try { WorkspaceUtils.fromJson(" "); fail(); @@ -115,7 +115,7 @@ void test_fromJson_ThrowsAnException_WhenAnEmptyJsonStringIsProvided() throws Ex } @Test - void test_fromJson() throws Exception { + void fromJson() throws Exception { Workspace workspace = WorkspaceUtils.fromJson("{\"id\":0,\"name\":\"Name\",\"description\":\"Description\",\"model\":{},\"documentation\":{},\"views\":{\"configuration\":{\"branding\":{},\"styles\":{},\"terminology\":{}}}}"); assertEquals("Name", workspace.getName()); assertEquals("Description", workspace.getDescription()); diff --git a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java index 0714d6a7f..afe40f9d0 100644 --- a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java @@ -14,7 +14,7 @@ public class ThemeUtilsTests { @Test - void test_loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { + void loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); ThemeUtils.loadThemes(workspace); @@ -23,7 +23,7 @@ void test_loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { } @Test - void test_loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { + void loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); softwareSystem.addTags("Amazon Web Services - Alexa For Business"); @@ -43,7 +43,7 @@ void test_loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { } @Test - void test_toJson() throws Exception { + void toJson() throws Exception { Workspace workspace = new Workspace("Name", "Description"); assertEquals("{\n" + " \"name\" : \"Name\",\n" + @@ -67,7 +67,7 @@ void test_toJson() throws Exception { } @Test - void test_findElementStyle_WithThemes() { + void findElementStyle_WithThemes() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); workspace.getViews().getConfiguration().getStyles().addElementStyle("Element").shape(Shape.RoundedBox); @@ -100,7 +100,7 @@ void test_findElementStyle_WithThemes() { } @Test - void test_findRelationshipStyle_WithThemes() { + void findRelationshipStyle_WithThemes() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); Relationship relationship = softwareSystem.uses(softwareSystem, "Uses"); diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java index 67e84dfd0..651057bbe 100644 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java @@ -14,27 +14,27 @@ public class WorkspaceTests { private Workspace workspace = new Workspace("Name", "Description"); @Test - void test_isEmpty_ReturnsTrue_WhenThereAreNoElementsViewsOrDocumentation() { + void isEmpty_ReturnsTrue_WhenThereAreNoElementsViewsOrDocumentation() { workspace = new Workspace("Name", "Description"); assertTrue(workspace.isEmpty()); } @Test - void test_isEmpty_ReturnsFalse_WhenThereAreElements() { + void isEmpty_ReturnsFalse_WhenThereAreElements() { workspace = new Workspace("Name", "Description"); workspace.getModel().addPerson("Name", "Description"); assertFalse(workspace.isEmpty()); } @Test - void test_isEmpty_ReturnsFalse_WhenThereAreViews() { + void isEmpty_ReturnsFalse_WhenThereAreViews() { workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key", "Description"); assertFalse(workspace.isEmpty()); } @Test - void test_isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { + void isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { workspace = new Workspace("Name", "Description"); Decision d = new Decision("1"); d.setTitle("Title"); @@ -46,7 +46,7 @@ void test_isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { } @Test - void test_countAndLogWarnings() { + void countAndLogWarnings() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1", null); SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2", " "); @@ -64,7 +64,7 @@ void test_countAndLogWarnings() { } @Test - void test_hydrate_DoesNotCrash() { + void hydrate_DoesNotCrash() { Workspace workspace = new Workspace("Name", "Description"); assertNotNull(workspace.getViews()); assertNotNull(workspace.getDocumentation()); diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java index 23c39802e..16d067eb3 100644 --- a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -8,7 +8,7 @@ public class WorkspaceConfigurationTests { @Test - void test_addUser_ThrowsAnException_WhenANullUsernameIsSpecified() { + void addUser_ThrowsAnException_WhenANullUsernameIsSpecified() { try { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser(null, Role.ReadWrite); @@ -19,7 +19,7 @@ void test_addUser_ThrowsAnException_WhenANullUsernameIsSpecified() { } @Test - void test_addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { + void addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { try { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser(" ", Role.ReadWrite); @@ -30,7 +30,7 @@ void test_addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { } @Test - void test_addUser_ThrowsAnException_WhenANullRoleIsSpecified() { + void addUser_ThrowsAnException_WhenANullRoleIsSpecified() { try { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser("user@domain.com", null); @@ -41,7 +41,7 @@ void test_addUser_ThrowsAnException_WhenANullRoleIsSpecified() { } @Test - void test_addUser_AddsAUser() { + void addUser_AddsAUser() { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); configuration.addUser("user@domain.com", Role.ReadOnly); diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java b/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java index b107f7568..82efda419 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java @@ -9,7 +9,7 @@ public class DecisionTests extends AbstractWorkspaceTestBase { @Test - void test_hasLinkTo() { + void hasLinkTo() { Decision d1 = new Decision("1"); Decision d2 = new Decision("2"); Decision d3 = new Decision("3"); diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java index e290f5bdd..ee4ea8957 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java @@ -16,7 +16,7 @@ public void setUp() { } @Test - void test_addSection_ThrowsAnException_WhenTheTitleIsNotSpecified() { + void addSection_ThrowsAnException_WhenTheTitleIsNotSpecified() { try { Section section = new Section(); @@ -28,7 +28,7 @@ void test_addSection_ThrowsAnException_WhenTheTitleIsNotSpecified() { } @Test - void test_addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { + void addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { try { Section section = new Section(); section.setTitle("Title"); @@ -41,7 +41,7 @@ void test_addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { } @Test - void test_addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { + void addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { try { Section section = new Section(); section.setTitle("Title"); @@ -55,7 +55,7 @@ void test_addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { } @Test - void test_addSection_ThrowsAnException_WhenASectionExistsWithTheSameTitle() { + void addSection_ThrowsAnException_WhenASectionExistsWithTheSameTitle() { try { Section section = new Section(); section.setTitle("Title"); @@ -71,7 +71,7 @@ void test_addSection_ThrowsAnException_WhenASectionExistsWithTheSameTitle() { } @Test - void test_addSection() { + void addSection() { Section section = new Section(); section.setTitle("Title"); section.setContent("Content"); @@ -88,7 +88,7 @@ void test_addSection() { } @Test - void test_addSection_IncrementsTheSectionOrderNumber() { + void addSection_IncrementsTheSectionOrderNumber() { Section section1 = new Section("Title 1", Format.Markdown, "Content"); Section section2 = new Section("Title 2", Format.Markdown, "Content"); Section section3 = new Section("Title 3", Format.Markdown, "Content"); @@ -103,7 +103,7 @@ void test_addSection_IncrementsTheSectionOrderNumber() { } @Test - void test_addDecision_ThrowsAnException_WhenTheTitleIsNotSpecified() { + void addDecision_ThrowsAnException_WhenTheTitleIsNotSpecified() { try { Decision decision = new Decision("1"); @@ -115,7 +115,7 @@ void test_addDecision_ThrowsAnException_WhenTheTitleIsNotSpecified() { } @Test - void test_addDecision_ThrowsAnException_WhenTheContentIsNotSpecified() { + void addDecision_ThrowsAnException_WhenTheContentIsNotSpecified() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); @@ -128,7 +128,7 @@ void test_addDecision_ThrowsAnException_WhenTheContentIsNotSpecified() { } @Test - void test_addDecision_ThrowsAnException_WhenTheStatusIsNotSpecified() { + void addDecision_ThrowsAnException_WhenTheStatusIsNotSpecified() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); @@ -142,7 +142,7 @@ void test_addDecision_ThrowsAnException_WhenTheStatusIsNotSpecified() { } @Test - void test_addDecision_ThrowsAnException_WhenTheFormatIsNotSpecified() { + void addDecision_ThrowsAnException_WhenTheFormatIsNotSpecified() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); @@ -157,7 +157,7 @@ void test_addDecision_ThrowsAnException_WhenTheFormatIsNotSpecified() { } @Test - void test_addDecision_ThrowsAnException_WhenADecisionExistsWithTheSameId() { + void addDecision_ThrowsAnException_WhenADecisionExistsWithTheSameId() { try { Decision decision = new Decision("1"); decision.setTitle("Title"); diff --git a/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java b/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java index 92016904a..282e92c5e 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java @@ -7,7 +7,7 @@ public class SectionTests { @Test - void test_construction() { + void construction() { Section section = new Section("Title", Format.Markdown, "Content"); assertEquals("Title", section.getTitle()); diff --git a/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java b/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java index d011785a0..79cb47c86 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java @@ -7,21 +7,21 @@ public class CodeElementTests { @Test - void test_construction_WhenAFullyQualifiedNameIsSpecified() { + void construction_WhenAFullyQualifiedNameIsSpecified() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals("SomeComponent", codeElement.getName()); assertEquals("com.structurizr.component.SomeComponent", codeElement.getType()); } @Test - void test_construction_WhenAFullyQualifiedNameIsSpecifiedInTheDefaultPackage() { + void construction_WhenAFullyQualifiedNameIsSpecifiedInTheDefaultPackage() { CodeElement codeElement = new CodeElement("SomeComponent"); assertEquals("SomeComponent", codeElement.getName()); assertEquals("SomeComponent", codeElement.getType()); } @Test - void test_descriptionProperty() { + void descriptionProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNull(codeElement.getDescription()); @@ -30,7 +30,7 @@ void test_descriptionProperty() { } @Test - void test_sizeProperty() { + void sizeProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals(0, codeElement.getSize()); @@ -39,7 +39,7 @@ void test_sizeProperty() { } @Test - void test_languageProperty() { + void languageProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals("Java", codeElement.getLanguage()); @@ -48,7 +48,7 @@ void test_languageProperty() { } @Test - void test_categoryProperty() { + void categoryProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNull(codeElement.getCategory()); @@ -57,7 +57,7 @@ void test_categoryProperty() { } @Test - void test_visibilityProperty() { + void visibilityProperty() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNull(codeElement.getVisibility()); @@ -66,14 +66,14 @@ void test_visibilityProperty() { } @Test - void test_setUrl() { + void setUrl() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); codeElement.setUrl("https://structurizr.com"); assertEquals("https://structurizr.com", codeElement.getUrl()); } @Test - void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + void setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); codeElement.setUrl("htt://blah"); @@ -81,61 +81,61 @@ void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() } @Test - void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { + void setUrl_DoesNothing_WhenANullUrlIsSpecified() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); codeElement.setUrl(null); assertNull(codeElement.getUrl()); } @Test - void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); codeElement.setUrl(" "); assertNull(codeElement.getUrl()); } @Test - void test_construction_ThrowsAnIllegalArgumentException_WhenANullFullyQualifiedNameIsSpecified() { + void construction_ThrowsAnIllegalArgumentException_WhenANullFullyQualifiedNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { new CodeElement(null); }); } @Test - void test_construction_ThrowsAnIllegalArgumentException_WhenAnEmptyFullyQualifiedNameIsSpecified() { + void construction_ThrowsAnIllegalArgumentException_WhenAnEmptyFullyQualifiedNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { new CodeElement(" "); }); } @Test - void test_equals_ReturnsFalse_WhenComparedToNull() { + void equals_ReturnsFalse_WhenComparedToNull() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNotEquals(codeElement, null); } @Test - void test_equals_ReturnsFalse_WhenComparedToDifferentTypeOfObject() { + void equals_ReturnsFalse_WhenComparedToDifferentTypeOfObject() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertNotEquals(codeElement, "hello"); } @Test - void test_equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithADifferentType() { + void equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithADifferentType() { CodeElement codeElement1 = new CodeElement("com.structurizr.component.SomeComponent1"); CodeElement codeElement2 = new CodeElement("com.structurizr.component.SomeComponent2"); assertNotEquals(codeElement1, codeElement2); } @Test - void test_equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithTheSameType() { + void equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithTheSameType() { CodeElement codeElement1 = new CodeElement("com.structurizr.component.SomeComponent1"); CodeElement codeElement2 = new CodeElement("com.structurizr.component.SomeComponent1"); assertEquals(codeElement1, codeElement2); } @Test - void test_getPackage_ReturnsThePackageName() { + void getPackage_ReturnsThePackageName() { CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); assertEquals("com.structurizr.component", codeElement.getPackage()); } diff --git a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java index 0036db291..82f72d2a4 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java @@ -13,38 +13,38 @@ public class ComponentTests extends AbstractWorkspaceTestBase { private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); @Test - void test_getName_ReturnsTheGivenName_WhenANameIsGiven() { + void getName_ReturnsTheGivenName_WhenANameIsGiven() { Component component = new Component(); component.setName("Some name"); assertEquals("Some name", component.getName()); } @Test - void test_getCanonicalName() { + void getCanonicalName() { Component component = container.addComponent("Component", "Description"); assertEquals("Component://System.Container.Component", component.getCanonicalName()); } @Test - void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { Component component = container.addComponent("Name1/.Name2", "Description"); assertEquals("Component://System.Container.Name1Name2", component.getCanonicalName()); } @Test - void test_getParent_ReturnsTheParentContainer() { + void getParent_ReturnsTheParentContainer() { Component component = container.addComponent("Component", "Description"); assertEquals(container, component.getParent()); } @Test - void test_getContainer_ReturnsTheParentContainer() { + void getContainer_ReturnsTheParentContainer() { Component component = container.addComponent("Name", "Description"); assertEquals(container, component.getContainer()); } @Test - void test_removeTags_DoesNotRemoveRequiredTags() { + void removeTags_DoesNotRemoveRequiredTags() { Component component = new Component(); assertTrue(component.getTags().contains(Tags.ELEMENT)); assertTrue(component.getTags().contains(Tags.COMPONENT)); @@ -57,7 +57,7 @@ void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - void test_technologyProperty() { + void technologyProperty() { Component component = new Component(); assertNull(component.getTechnology()); @@ -66,7 +66,7 @@ void test_technologyProperty() { } @Test - void test_sizeProperty() { + void sizeProperty() { Component component = new Component(); assertEquals(0, component.getSize()); @@ -75,7 +75,7 @@ void test_sizeProperty() { } @Test - void test_setType_ThrowsAnExceptionWhenPassedNull() { + void setType_ThrowsAnExceptionWhenPassedNull() { Component component = new Component(); try { component.setType(null); @@ -86,7 +86,7 @@ void test_setType_ThrowsAnExceptionWhenPassedNull() { } @Test - void test_setType_AddsAPrimaryCodeElement_WhenPassedAFullyQualifiedTypeName() { + void setType_AddsAPrimaryCodeElement_WhenPassedAFullyQualifiedTypeName() { Component component = new Component(); component.setType("com.structurizr.web.HomePageController"); @@ -99,7 +99,7 @@ void test_setType_AddsAPrimaryCodeElement_WhenPassedAFullyQualifiedTypeName() { } @Test - void test_setType_OverwritesThePrimaryCodeElement_WhenCalledMoreThanOnce() { + void setType_OverwritesThePrimaryCodeElement_WhenCalledMoreThanOnce() { Component component = new Component(); component.setType("com.structurizr.web.HomePageController"); component.setType("com.structurizr.web.SomeOtherController"); @@ -114,7 +114,7 @@ void test_setType_OverwritesThePrimaryCodeElement_WhenCalledMoreThanOnce() { } @Test - void test_addSupportingType_ThrowsAnExceptionWhenPassedNull() { + void addSupportingType_ThrowsAnExceptionWhenPassedNull() { Component component = new Component(); try { component.addSupportingType(null); @@ -125,7 +125,7 @@ void test_addSupportingType_ThrowsAnExceptionWhenPassedNull() { } @Test - void test_addSupportingType_AddsASupportingCodeElement_WhenPassedAFullyQualifiedTypeName() { + void addSupportingType_AddsASupportingCodeElement_WhenPassedAFullyQualifiedTypeName() { Component component = new Component(); component.addSupportingType("com.structurizr.web.HomePageViewModel"); @@ -138,20 +138,20 @@ void test_addSupportingType_AddsASupportingCodeElement_WhenPassedAFullyQualified } @Test - void test_getType_ReturnsNull_WhenThereAreNoCodeElements() { + void getType_ReturnsNull_WhenThereAreNoCodeElements() { Component component = new Component(); assertNull(component.getType()); } @Test - void test_getType_ReturnsNull_WhenThereAreNoPrimaryCodeElements() { + void getType_ReturnsNull_WhenThereAreNoPrimaryCodeElements() { Component component = new Component(); component.addSupportingType("com.structurizr.SomeType"); assertNull(component.getType()); } @Test - void test_getType_ReturnsThePrimaryCodeElement_WhenThereIsAPrimaryCodeElement() { + void getType_ReturnsThePrimaryCodeElement_WhenThereIsAPrimaryCodeElement() { Component component = new Component(); component.setType("com.structurizr.SomeType"); CodeElement codeElement = component.getType(); diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java index 12e62a53c..445a11f22 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java @@ -12,7 +12,7 @@ public class ContainerInstanceTests extends AbstractWorkspaceTestBase { private DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @Test - void test_construction() { + void construction() { ContainerInstance instance = deploymentNode.add(database); assertSame(database, instance.getContainer()); @@ -21,7 +21,7 @@ void test_construction() { } @Test - void test_getContainerId() { + void getContainerId() { ContainerInstance instance = deploymentNode.add(database); assertEquals(database.getId(), instance.getContainerId()); @@ -31,7 +31,7 @@ void test_getContainerId() { } @Test - void test_getName() { + void getName() { ContainerInstance instance = deploymentNode.add(database); assertEquals("Database Schema", instance.getName()); @@ -41,28 +41,28 @@ void test_getName() { } @Test - void test_getCanonicalName() { + void getCanonicalName() { ContainerInstance instance = deploymentNode.add(database); assertEquals("ContainerInstance://Default/Deployment Node/System.Database Schema[1]", instance.getCanonicalName()); } @Test - void test_getParent_ReturnsTheParentDeploymentNode() { + void getParent_ReturnsTheParentDeploymentNode() { ContainerInstance instance = deploymentNode.add(database); assertEquals(deploymentNode, instance.getParent()); } @Test - void test_getRequiredTags() { + void getRequiredTags() { ContainerInstance instance = deploymentNode.add(database); assertTrue(instance.getDefaultTags().isEmpty()); } @Test - void test_getTags() { + void getTags() { database.addTags("Database"); ContainerInstance instance = deploymentNode.add(database); instance.addTags("Primary Instance"); @@ -71,7 +71,7 @@ void test_getTags() { } @Test - void test_removeTags_DoesNotRemoveAnyTags() { + void removeTags_DoesNotRemoveAnyTags() { ContainerInstance instance = deploymentNode.add(database); assertTrue(instance.getTags().contains(Tags.CONTAINER_INSTANCE)); @@ -82,7 +82,7 @@ void test_removeTags_DoesNotRemoveAnyTags() { } @Test - void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { + void getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { ContainerInstance instance = deploymentNode.add(database); assertEquals(1, instance.getDeploymentGroups().size()); @@ -90,7 +90,7 @@ void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { } @Test - void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { + void getDeploymentGroups_WhenOneGroupHasBeenSpecified() { ContainerInstance instance = deploymentNode.add(database, "Group 1"); assertEquals(1, instance.getDeploymentGroups().size()); @@ -98,7 +98,7 @@ void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { } @Test - void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { + void getDeploymentGroups_WhenMultipleGroupsAreSpecified() { ContainerInstance instance = deploymentNode.add(database, "Group 1", "Group 2"); assertEquals(2, instance.getDeploymentGroups().size()); @@ -107,7 +107,7 @@ void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { } @Test - void test_addHealthCheck() { + void addHealthCheck() { ContainerInstance instance = deploymentNode.add(database); assertTrue(instance.getHealthChecks().isEmpty()); @@ -120,7 +120,7 @@ void test_addHealthCheck() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { + void addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { ContainerInstance instance = deploymentNode.add(database); try { @@ -132,7 +132,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { + void addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { ContainerInstance instance = deploymentNode.add(database); try { @@ -144,7 +144,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { + void addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { ContainerInstance instance = deploymentNode.add(database); try { @@ -156,7 +156,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { + void addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { ContainerInstance instance = deploymentNode.add(database); try { @@ -168,7 +168,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { + void addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { ContainerInstance instance = deploymentNode.add(database); try { @@ -180,7 +180,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { + void addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { ContainerInstance instance = deploymentNode.add(database); try { @@ -192,7 +192,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { + void addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { ContainerInstance instance = deploymentNode.add(database); try { diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java index 80bd6af86..3665f5194 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java @@ -11,7 +11,7 @@ public class ContainerTests extends AbstractWorkspaceTestBase { private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); @Test - void test_technologyProperty() { + void technologyProperty() { assertEquals("Some technology", container.getTechnology()); container.setTechnology("Some other technology"); @@ -19,29 +19,29 @@ void test_technologyProperty() { } @Test - void test_getCanonicalName() { + void getCanonicalName() { assertEquals("Container://System.Container", container.getCanonicalName()); } @Test - void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { container = softwareSystem.addContainer("Name1/.Name2", "Description", "Some technology"); assertEquals("Container://System.Name1Name2", container.getCanonicalName()); } @Test - void test_getParent_ReturnsTheParentSoftwareSystem() { + void getParent_ReturnsTheParentSoftwareSystem() { assertEquals(softwareSystem, container.getParent()); } @Test - void test_getSoftwareSystem_ReturnsTheParentSoftwareSystem() { + void getSoftwareSystem_ReturnsTheParentSoftwareSystem() { assertEquals(softwareSystem, container.getSoftwareSystem()); } @Test - void test_removeTags_DoesNotRemoveRequiredTags() { + void removeTags_DoesNotRemoveRequiredTags() { assertTrue(container.getTags().contains(Tags.ELEMENT)); assertTrue(container.getTags().contains(Tags.CONTAINER)); @@ -53,21 +53,21 @@ void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - void test_addComponent_ThrowsAnException_WhenANullNameIsSpecified() { + void addComponent_ThrowsAnException_WhenANullNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { container.addComponent(null, ""); }); } @Test - void test_addComponent_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void addComponent_ThrowsAnException_WhenAnEmptyNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { container.addComponent(" ", ""); }); } @Test - void test_addComponent_ThrowsAnException_WhenAComponentWithTheSameNameAlreadyExists() { + void addComponent_ThrowsAnException_WhenAComponentWithTheSameNameAlreadyExists() { container.addComponent("Component 1", ""); try { container.addComponent("Component 1", ""); @@ -78,7 +78,7 @@ void test_addComponent_ThrowsAnException_WhenAComponentWithTheSameNameAlreadyExi } @Test - void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() { + void addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() { Component component = container.addComponent("Name", "Description"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -90,7 +90,7 @@ void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() { } @Test - void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnology() { + void addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnology() { Component component = container.addComponent("Name", "Description", "Technology"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -102,7 +102,7 @@ void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechno } @Test - void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndStringType() { + void addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndStringType() { Component component = container.addComponent("Name", "SomeType", "Description", "Technology"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -114,7 +114,7 @@ void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechno } @Test - void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndClassType() { + void addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndClassType() { Component component = container.addComponent("Name", this.getClass(), "Description", "Technology"); assertTrue(container.getComponents().contains(component)); assertEquals("Name", component.getName()); @@ -126,7 +126,7 @@ void test_addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechno } @Test - void test_getComponentWithName_ThrowsAnException_WhenANullNameIsSpecified() { + void getComponentWithName_ThrowsAnException_WhenANullNameIsSpecified() { try { container.getComponentWithName(null); fail(); @@ -136,7 +136,7 @@ void test_getComponentWithName_ThrowsAnException_WhenANullNameIsSpecified() { } @Test - void test_getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { try { container.getComponentWithName(" "); fail(); @@ -146,7 +146,7 @@ void test_getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { } @Test - void test_getComponentOfType_ThrowsAnException_WhenANullTypeIsSpecified() { + void getComponentOfType_ThrowsAnException_WhenANullTypeIsSpecified() { try { container.getComponentOfType(null); fail(); @@ -156,7 +156,7 @@ void test_getComponentOfType_ThrowsAnException_WhenANullTypeIsSpecified() { } @Test - void test_getComponentOfType_ThrowsAnException_WhenAnEmptyTypeIsSpecified() { + void getComponentOfType_ThrowsAnException_WhenAnEmptyTypeIsSpecified() { try { container.getComponentOfType(" "); fail(); @@ -166,12 +166,12 @@ void test_getComponentOfType_ThrowsAnException_WhenAnEmptyTypeIsSpecified() { } @Test - void test_getComponentOfType_ReturnsNull_WhenNoComponentWithTheSpecifiedTypeExists() { + void getComponentOfType_ReturnsNull_WhenNoComponentWithTheSpecifiedTypeExists() { assertNull(container.getComponentOfType("SomeType")); } @Test - void test_getComponentOfType_ReturnsAComponent_WhenAComponentWithTheSpecifiedTypeExists() { + void getComponentOfType_ReturnsAComponent_WhenAComponentWithTheSpecifiedTypeExists() { container.addComponent("Name", "SomeType", "Description", "Technology"); Component component = container.getComponentOfType("SomeType"); diff --git a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java index d02cd43b4..aeb8b6839 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java @@ -10,7 +10,7 @@ public class CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests extends AbstractWorkspaceTestBase { @Test - void test_impliedRelationshipsAreCreated() { + void impliedRelationshipsAreCreated() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); @@ -54,7 +54,7 @@ void test_impliedRelationshipsAreCreated() { } @Test - void test_impliedRelationshipsAreCreated_UnlessAnyRelationshipExists() { + void impliedRelationshipsAreCreated_UnlessAnyRelationshipExists() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); diff --git a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java index 38ebb887b..1b8fda1ce 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java @@ -10,7 +10,7 @@ public class CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests extends AbstractWorkspaceTestBase { @Test - void test_impliedRelationships_WhenNoSummaryRelationshipsExist() { + void impliedRelationships_WhenNoSummaryRelationshipsExist() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); diff --git a/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java b/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java index 81fdbaf82..f90ef36a2 100644 --- a/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java @@ -8,7 +8,7 @@ public class CustomElementTests extends AbstractWorkspaceTestBase { @Test - void test_basicProperties() { + void basicProperties() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertEquals("Name", element.getName()); assertEquals("Type", element.getMetadata()); @@ -16,26 +16,26 @@ void test_basicProperties() { } @Test - void test_getCanonicalName() { + void getCanonicalName() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertEquals("Custom://Name", element.getCanonicalName()); } @Test - void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); element.setName("Name1/.Name2"); assertEquals("Custom://Name1Name2", element.getCanonicalName()); } @Test - void test_getParent_ReturnsNull() { + void getParent_ReturnsNull() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertNull(element.getParent()); } @Test - void test_removeTags_DoesNotRemoveRequiredTags() { + void removeTags_DoesNotRemoveRequiredTags() { CustomElement element = model.addCustomElement("Name", "Type", "Description"); assertTrue(element.getTags().contains(Tags.ELEMENT)); @@ -45,7 +45,7 @@ void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - void test_uses_AddsARelationshipWhenTheDescriptionIsSpecified() { + void uses_AddsARelationshipWhenTheDescriptionIsSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); @@ -61,7 +61,7 @@ void test_uses_AddsARelationshipWhenTheDescriptionIsSpecified() { } @Test - void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { + void uses_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); @@ -77,7 +77,7 @@ void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { } @Test - void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { + void uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); @@ -93,7 +93,7 @@ void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionSty } @Test - void test_uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAndTagsAreSpecified() { + void uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAndTagsAreSpecified() { CustomElement element1 = model.addCustomElement("Box 1"); CustomElement element2 = model.addCustomElement("Box 2"); diff --git a/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java b/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java index afd611ab5..7f0e261ae 100644 --- a/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java @@ -9,7 +9,7 @@ public class DefaultImpliedRelationshipsStrategyTests extends AbstractWorkspaceTestBase { @Test - void test_createImpliedRelationships_DoesNothing() { + void createImpliedRelationships_DoesNothing() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Container aa = a.addContainer("AA", "", ""); Component aaa = aa.addComponent("AAA", "", ""); diff --git a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java b/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java index 86ae6a609..e7e0abd3f 100644 --- a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java @@ -9,14 +9,14 @@ public class DeploymentNodeTests extends AbstractWorkspaceTestBase { @Test - void test_getCanonicalName_WhenTheDeploymentNodeHasNoParent() { + void getCanonicalName_WhenTheDeploymentNodeHasNoParent() { DeploymentNode deploymentNode = model.addDeploymentNode("Ubuntu Server", "", ""); assertEquals("DeploymentNode://Default/Ubuntu Server", deploymentNode.getCanonicalName()); } @Test - void test_getCanonicalName_WhenTheDeploymentNodeHasAParent() { + void getCanonicalName_WhenTheDeploymentNodeHasAParent() { DeploymentNode l1 = model.addDeploymentNode("Level 1", "", ""); DeploymentNode l2 = l1.addDeploymentNode("Level 2", "", ""); DeploymentNode l3 = l2.addDeploymentNode("Level 3", "", ""); @@ -27,7 +27,7 @@ void test_getCanonicalName_WhenTheDeploymentNodeHasAParent() { } @Test - void test_getParent_ReturnsTheParentDeploymentNode() { + void getParent_ReturnsTheParentDeploymentNode() { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); assertNull(parent.getParent()); @@ -37,7 +37,7 @@ void test_getParent_ReturnsTheParentDeploymentNode() { } @Test - void test_getRequiredTags() { + void getRequiredTags() { DeploymentNode deploymentNode = new DeploymentNode(); assertEquals(2, deploymentNode.getDefaultTags().size()); assertTrue(deploymentNode.getDefaultTags().contains(Tags.ELEMENT)); @@ -45,14 +45,14 @@ void test_getRequiredTags() { } @Test - void test_getTags() { + void getTags() { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.addTags("Tag 1", "Tag 2"); assertEquals("Element,Deployment Node,Tag 1,Tag 2", deploymentNode.getTags()); } @Test - void test_add_ThrowsAnException_WhenASoftwareSystemIsNotSpecified() { + void add_ThrowsAnException_WhenASoftwareSystemIsNotSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); deploymentNode.add((SoftwareSystem) null); @@ -63,7 +63,7 @@ void test_add_ThrowsAnException_WhenASoftwareSystemIsNotSpecified() { } @Test - void test_add_ThrowsAnException_WhenAContainerIsNotSpecified() { + void add_ThrowsAnException_WhenAContainerIsNotSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); deploymentNode.add((Container) null); @@ -74,7 +74,7 @@ void test_add_ThrowsAnException_WhenAContainerIsNotSpecified() { } @Test - void test_add_AddsAContainerInstance_WhenAContainerIsSpecified() { + void add_AddsAContainerInstance_WhenAContainerIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); @@ -87,7 +87,7 @@ void test_add_AddsAContainerInstance_WhenAContainerIsSpecified() { } @Test - void test_addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { + void addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { try { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); parent.addDeploymentNode(null, "", ""); @@ -98,7 +98,7 @@ void test_addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { } @Test - void test_addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { + void addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); DeploymentNode child = parent.addDeploymentNode("Child 1", "Description", "Technology"); @@ -134,7 +134,7 @@ void test_addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { } @Test - void test_uses_ThrowsAnException_WhenANullDestinationIsSpecified() { + void uses_ThrowsAnException_WhenANullDestinationIsSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); deploymentNode.uses((DeploymentNode) null, "", ""); @@ -145,7 +145,7 @@ void test_uses_ThrowsAnException_WhenANullDestinationIsSpecified() { } @Test - void test_uses_AddsARelationship() { + void uses_AddsARelationship() { DeploymentNode primaryNode = model.addDeploymentNode("MySQL - Primary", "", ""); DeploymentNode secondaryNode = model.addDeploymentNode("MySQL - Secondary", "", ""); Relationship relationship = primaryNode.uses(secondaryNode, "Replicates data to", "Some technology"); @@ -158,7 +158,7 @@ void test_uses_AddsARelationship() { } @Test - void test_getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpecified() { + void getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.getDeploymentNodeWithName(null); @@ -169,33 +169,33 @@ void test_getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpecified() } @Test - void test_getDeploymentNodeWithName_ReturnsNull_WhenThereIsNoDeploymentNodeWithTheSpecifiedName() { + void getDeploymentNodeWithName_ReturnsNull_WhenThereIsNoDeploymentNodeWithTheSpecifiedName() { DeploymentNode deploymentNode = new DeploymentNode(); assertNull(deploymentNode.getDeploymentNodeWithName("foo")); } @Test - void test_getDeploymentNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsADeploymentNodeWithTheSpecifiedName() { + void getDeploymentNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsADeploymentNodeWithTheSpecifiedName() { DeploymentNode parent = model.addDeploymentNode("parent", "", ""); DeploymentNode child = parent.addDeploymentNode("child", "", ""); assertSame(child, parent.getDeploymentNodeWithName("child")); } @Test - void test_getInfrastructureNodeWithName_ReturnsNull_WhenThereIsNoInfrastructureNodeWithTheSpecifiedName() { + void getInfrastructureNodeWithName_ReturnsNull_WhenThereIsNoInfrastructureNodeWithTheSpecifiedName() { DeploymentNode deploymentNode = new DeploymentNode(); assertNull(deploymentNode.getInfrastructureNodeWithName("foo")); } @Test - void test_getInfrastructureNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsAInfrastructureNodeWithTheSpecifiedName() { + void getInfrastructureNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsAInfrastructureNodeWithTheSpecifiedName() { DeploymentNode parent = model.addDeploymentNode("parent", "", ""); InfrastructureNode child = parent.addInfrastructureNode("child", "", ""); assertSame(child, parent.getInfrastructureNodeWithName("child")); } @Test - void test_setInstances() { + void setInstances() { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.setInstances(8); @@ -203,7 +203,7 @@ void test_setInstances() { } @Test - void test_setInstances_ThrowsAnException_WhenZeroIsSpecified() { + void setInstances_ThrowsAnException_WhenZeroIsSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.setInstances(0); @@ -214,7 +214,7 @@ void test_setInstances_ThrowsAnException_WhenZeroIsSpecified() { } @Test - void test_setInstances_ThrowsAnException_WhenANegativeNumberIsSpecified() { + void setInstances_ThrowsAnException_WhenANegativeNumberIsSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); deploymentNode.setInstances(-1); diff --git a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java b/structurizr-core/test/unit/com/structurizr/model/ElementTests.java index 1bf94229e..05a151574 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ElementTests.java @@ -8,14 +8,14 @@ public class ElementTests extends AbstractWorkspaceTestBase { @Test - void test_construction() { + void construction() { Element element = model.addSoftwareSystem("Name", "Description"); assertEquals("Name", element.getName()); assertEquals("Description", element.getDescription()); } @Test - void test_setName_ThrowsAnException_WhenANullValueIsSpecified() { + void setName_ThrowsAnException_WhenANullValueIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); try { element.setName(null); @@ -26,7 +26,7 @@ void test_setName_ThrowsAnException_WhenANullValueIsSpecified() { } @Test - void test_setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { + void setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); try { element.setName(" "); @@ -37,26 +37,26 @@ void test_setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { } @Test - void test_hasEfferentRelationshipWith_ReturnsFalse_WhenANullElementIsSpecified() { + void hasEfferentRelationshipWith_ReturnsFalse_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertFalse(softwareSystem1.hasEfferentRelationshipWith(null)); } @Test - void test_hasEfferentRelationshipWith_ReturnsFalse_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { + void hasEfferentRelationshipWith_ReturnsFalse_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertFalse(softwareSystem1.hasEfferentRelationshipWith(softwareSystem1)); } @Test - void test_hasEfferentRelationshipWith_ReturnsTrue_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { + void hasEfferentRelationshipWith_ReturnsTrue_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); softwareSystem1.uses(softwareSystem1, "uses"); assertTrue(softwareSystem1.hasEfferentRelationshipWith(softwareSystem1)); } @Test - void test_hasEfferentRelationshipWith_ReturnsTrue_WhenThereIsARelationship() { + void hasEfferentRelationshipWith_ReturnsTrue_WhenThereIsARelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -64,13 +64,13 @@ void test_hasEfferentRelationshipWith_ReturnsTrue_WhenThereIsARelationship() { } @Test - void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenANullElementIsSpecified() { + void hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertFalse(softwareSystem1.hasEfferentRelationshipWith(null, null)); } @Test - void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenThereIsNotAMatchingRelationship() { + void hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenThereIsNotAMatchingRelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -79,7 +79,7 @@ void test_hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenTher } @Test - void test_hasEfferentRelationshipWithElementAndDescription_ReturnsTrue_WhenThereIsAMatchingRelationship() { + void hasEfferentRelationshipWithElementAndDescription_ReturnsTrue_WhenThereIsAMatchingRelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -88,19 +88,19 @@ void test_hasEfferentRelationshipWithElementAndDescription_ReturnsTrue_WhenThere } @Test - void test_getEfferentRelationshipWith_ReturnsNull_WhenANullElementIsSpecified() { + void getEfferentRelationshipWith_ReturnsNull_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertNull(softwareSystem1.getEfferentRelationshipWith(null)); } @Test - void test_getEfferentRelationshipWith_ReturnsNull_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { + void getEfferentRelationshipWith_ReturnsNull_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); assertNull(softwareSystem1.getEfferentRelationshipWith(softwareSystem1)); } @Test - void test_getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { + void getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); softwareSystem1.uses(softwareSystem1, "uses"); @@ -111,7 +111,7 @@ void test_getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSameEleme } @Test - void test_getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsARelationship() { + void getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsARelationship() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -123,7 +123,7 @@ void test_getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsARelatio } @Test - void test_hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRelationships() { + void hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRelationships() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -132,7 +132,7 @@ void test_hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRelationsh } @Test - void test_hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelationships() { + void hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelationships() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); softwareSystem1.uses(softwareSystem2, "Uses"); @@ -141,7 +141,7 @@ void test_hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelationships } @Test - void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwareSystemIsAddedMoreThanOnce() { + void addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwareSystemIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); @@ -160,7 +160,7 @@ void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwareSystem } @Test - void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAContainerIsAddedMoreThanOnce() { + void addRelationship_DoesNothing_WhenTheSameRelationshipWithAContainerIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); Container bb = b.addContainer("BB", "", ""); @@ -180,7 +180,7 @@ void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAContainerIsAdd } @Test - void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAComponentIsAddedMoreThanOnce() { + void addRelationship_DoesNothing_WhenTheSameRelationshipWithAComponentIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); Container bb = b.addContainer("BB", "", ""); @@ -201,7 +201,7 @@ void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAComponentIsAdd } @Test - void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonIsAddedMoreThanOnce() { + void addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonIsAddedMoreThanOnce() { SoftwareSystem a = model.addSoftwareSystem("A", ""); Person b = model.addPerson("B", ""); @@ -220,46 +220,46 @@ void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonIsAddedM } @Test - void test_equals_ReturnsFalse_WhenTestedAgainstNull() { + void equals_ReturnsFalse_WhenTestedAgainstNull() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); assertNotEquals(softwareSystem, null); } @Test - void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferentTypes() { + void equals_ReturnsFalse_WhenTheTwoObjectsAreDifferentTypes() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); assertNotEquals(softwareSystem, "hello world"); } @Test - void test_equals_ReturnsTrue_WhenTestedAgainstItself() { + void equals_ReturnsTrue_WhenTestedAgainstItself() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); assertEquals(softwareSystem, softwareSystem); } @Test - void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferent() { + void equals_ReturnsFalse_WhenTheTwoObjectsAreDifferent() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", "Description"); assertNotEquals(softwareSystemA, softwareSystemB); } @Test - void test_equals_ReturnsFalse_WhenTheTwoElementsHaveTheSameCanonicalNameButAreDifferentTypes() { + void equals_ReturnsFalse_WhenTheTwoElementsHaveTheSameCanonicalNameButAreDifferentTypes() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); Person person = model.addPerson("Name", "Description"); assertNotEquals(softwareSystem, person); } @Test - void test_setUrl() { + void setUrl() { Element element = model.addSoftwareSystem("Name", "Description"); element.setUrl("https://structurizr.com"); assertEquals("https://structurizr.com", element.getUrl()); } @Test - void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + void setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { Element element = model.addSoftwareSystem("Name", "Description"); element.setUrl("htt://blah"); @@ -267,7 +267,7 @@ void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() } @Test - void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { + void setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.setUrl("https://structurizr.com"); element.setUrl(null); @@ -275,7 +275,7 @@ void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { } @Test - void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { + void setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.setUrl("https://structurizr.com"); element.setUrl(" "); diff --git a/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java b/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java index bc91c32b9..3c981f390 100644 --- a/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java @@ -9,27 +9,27 @@ public class GroupableElementTests extends AbstractWorkspaceTestBase { @Test - void test_getGroup_ReturnsNullByDefault() { + void getGroup_ReturnsNullByDefault() { Person element = model.addPerson("Person"); assertNull(element.getGroup()); } @Test - void test_setGroup() { + void setGroup() { Person element = model.addPerson("Person"); element.setGroup("Group"); assertEquals("Group", element.getGroup()); } @Test - void test_setGroup_TrimsWhiteSpace() { + void setGroup_TrimsWhiteSpace() { Person element = model.addPerson("Person"); element.setGroup(" Group "); assertEquals("Group", element.getGroup()); } @Test - void test_setGroup_HandlesEmptyAndNullValues() { + void setGroup_HandlesEmptyAndNullValues() { Person element = model.addPerson("Person"); element.setGroup("Group"); diff --git a/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java b/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java index 8b3ec7496..57935806b 100644 --- a/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java @@ -10,13 +10,13 @@ public class HttpHealthCheckTests { private HttpHealthCheck healthCheck; @Test - void test_defaultConstructorExists() { + void defaultConstructorExists() { // the default constructor is used when deserializing from JSON healthCheck = new HttpHealthCheck(); } @Test - void test_construction() { + void construction() { healthCheck = new HttpHealthCheck("Name", "http://localhost", 120, 1000); assertEquals("Name", healthCheck.getName()); assertEquals("http://localhost", healthCheck.getUrl()); @@ -25,14 +25,14 @@ void test_construction() { } @Test - void test_addHeader() { + void addHeader() { healthCheck = new HttpHealthCheck(); healthCheck.addHeader("Name", "Value"); assertEquals("Value", healthCheck.getHeaders().get("Name")); } @Test - void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsNull() { + void addHeader_ThrowsAnException_WhenTheHeaderNameIsNull() { healthCheck = new HttpHealthCheck(); try { healthCheck.addHeader(null, "value"); @@ -43,7 +43,7 @@ void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsNull() { } @Test - void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsEmpty() { + void addHeader_ThrowsAnException_WhenTheHeaderNameIsEmpty() { healthCheck = new HttpHealthCheck(); try { healthCheck.addHeader(" ", "value"); @@ -54,7 +54,7 @@ void test_addHeader_ThrowsAnException_WhenTheHeaderNameIsEmpty() { } @Test - void test_addHeader_ThrowsAnException_WhenTheHeaderValueIsNull() { + void addHeader_ThrowsAnException_WhenTheHeaderValueIsNull() { healthCheck = new HttpHealthCheck(); try { healthCheck.addHeader("Name", null); @@ -65,7 +65,7 @@ void test_addHeader_ThrowsAnException_WhenTheHeaderValueIsNull() { } @Test - void test_addHeader_DoesNotThrowAnException_WhenTheHeaderValueIsEmpty() { + void addHeader_DoesNotThrowAnException_WhenTheHeaderValueIsEmpty() { healthCheck = new HttpHealthCheck(); healthCheck.addHeader("Name", ""); assertEquals("", healthCheck.getHeaders().get("Name")); diff --git a/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java b/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java index c910b2493..8ec8bb364 100644 --- a/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java @@ -8,7 +8,7 @@ public class InfrastructureNodeTests extends AbstractWorkspaceTestBase { @Test - void test_getCanonicalName() { + void getCanonicalName() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services", "", ""); InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Route 53", "", ""); @@ -16,7 +16,7 @@ void test_getCanonicalName() { } @Test - void test_getParent_ReturnsTheParentDeploymentNode() { + void getParent_ReturnsTheParentDeploymentNode() { DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); InfrastructureNode child = parent.addInfrastructureNode("Child", "", ""); child.setParent(parent); @@ -24,7 +24,7 @@ void test_getParent_ReturnsTheParentDeploymentNode() { } @Test - void test_getRequiredTags() { + void getRequiredTags() { InfrastructureNode infrastructureNode = new InfrastructureNode(); assertEquals(2, infrastructureNode.getDefaultTags().size()); assertTrue(infrastructureNode.getDefaultTags().contains(Tags.ELEMENT)); @@ -32,7 +32,7 @@ void test_getRequiredTags() { } @Test - void test_getTags() { + void getTags() { InfrastructureNode infrastructureNode = new InfrastructureNode(); infrastructureNode.addTags("Tag 1", "Tag 2"); assertEquals("Element,Infrastructure Node,Tag 1,Tag 2", infrastructureNode.getTags()); diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java index 8bdf44012..ae9c33852 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java @@ -11,41 +11,41 @@ public class ModelItemTests extends AbstractWorkspaceTestBase { @Test - void test_construction() { + void construction() { Element element = model.addSoftwareSystem("Name", "Description"); assertEquals("Name", element.getName()); assertEquals("Description", element.getDescription()); } @Test - void test_getTags_WhenThereAreNoTags() { + void getTags_WhenThereAreNoTags() { Element element = model.addSoftwareSystem("Name", "Description"); assertEquals("Element,Software System", element.getTags()); } @Test - void test_hasTag_ChecksRequiredTags() { + void hasTag_ChecksRequiredTags() { SoftwareSystem system = model.addSoftwareSystem("Name", "Description"); assertTrue(system.hasTag("Software System"), "hasTag returns true for Software System"); assertTrue(system.hasTag("Element"), "hasTag returns true for Element"); } @Test - void test_getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { + void getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags("tag1", "tag2", "tag3"); assertEquals("Element,Software System,tag1,tag2,tag3", element.getTags()); } @Test - void test_setTags_DoesNotDoAnything_WhenPassedNull() { + void setTags_DoesNotDoAnything_WhenPassedNull() { Element element = model.addSoftwareSystem("Name", "Description"); element.setTags(null); assertEquals("Element,Software System", element.getTags()); } @Test - void test_addTags_DoesNotDoAnything_WhenPassedNull() { + void addTags_DoesNotDoAnything_WhenPassedNull() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags((String) null); assertEquals("Element,Software System", element.getTags()); @@ -55,21 +55,21 @@ void test_addTags_DoesNotDoAnything_WhenPassedNull() { } @Test - void test_addTags_AddsTags_WhenPassedSomeTags() { + void addTags_AddsTags_WhenPassedSomeTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags(null, "tag1", null, "tag2"); assertEquals("Element,Software System,tag1,tag2", element.getTags()); } @Test - void test_addTags_AddsTags_WhenPassedSomeTagsAndThereAreDuplicateTags() { + void addTags_AddsTags_WhenPassedSomeTagsAndThereAreDuplicateTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags(null, "tag1", null, "tag2", "tag2"); assertEquals("Element,Software System,tag1,tag2", element.getTags()); } @Test - void test_removeTags() { + void removeTags() { Element element = model.addSoftwareSystem("Name", "Description"); element.addTags("tag1", "tag2"); assertTrue(element.removeTag("tag1"), "Remove an existing tag returns true"); @@ -80,13 +80,13 @@ void test_removeTags() { } @Test - void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + void getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { Element element = model.addSoftwareSystem("Name", "Description"); assertEquals(0, element.getProperties().size()); } @Test - void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { + void addProperty_ThrowsAnException_WhenTheNameIsNull() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty(null, "value"); @@ -97,7 +97,7 @@ void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { } @Test - void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + void addProperty_ThrowsAnException_WhenTheNameIsEmpty() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty(" ", "value"); @@ -108,7 +108,7 @@ void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { + void addProperty_ThrowsAnException_WhenTheValueIsNull() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty("name", null); @@ -119,7 +119,7 @@ void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { } @Test - void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + void addProperty_ThrowsAnException_WhenTheValueIsEmpty() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty("name", " "); @@ -130,21 +130,21 @@ void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { } @Test - void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + void addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.addProperty("AWS region", "us-east-1"); assertEquals("us-east-1", element.getProperties().get("AWS region")); } @Test - void test_setProperties_DoesNothing_WhenNullIsSpecified() { + void setProperties_DoesNothing_WhenNullIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); element.setProperties(null); assertEquals(0, element.getProperties().size()); } @Test - void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { Element element = model.addSoftwareSystem("Name", "Description"); Map properties = new HashMap<>(); properties.put("name", "value"); @@ -154,7 +154,7 @@ void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { } @Test - void test_addPerspective_ThrowsAnException_WhenANameIsNotSpecified() { + void addPerspective_ThrowsAnException_WhenANameIsNotSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective(null, null); @@ -165,7 +165,7 @@ void test_addPerspective_ThrowsAnException_WhenANameIsNotSpecified() { } @Test - void test_addPerspective_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void addPerspective_ThrowsAnException_WhenAnEmptyNameIsSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective(" ", null); @@ -176,7 +176,7 @@ void test_addPerspective_ThrowsAnException_WhenAnEmptyNameIsSpecified() { } @Test - void test_addPerspective_ThrowsAnException_WhenADescriptionIsNotSpecified() { + void addPerspective_ThrowsAnException_WhenADescriptionIsNotSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective("Security", null); @@ -187,7 +187,7 @@ void test_addPerspective_ThrowsAnException_WhenADescriptionIsNotSpecified() { } @Test - void test_addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecified() { + void addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecified() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective("Security", " "); @@ -198,7 +198,7 @@ void test_addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecified() { } @Test - void test_addPerspective_AddsAPerspective() { + void addPerspective_AddsAPerspective() { Element element = model.addSoftwareSystem("Name", "Description"); Perspective perspective = element.addPerspective("Security", "Data is encrypted at rest."); assertEquals("Security", perspective.getName()); @@ -207,7 +207,7 @@ void test_addPerspective_AddsAPerspective() { } @Test - void test_addPerspective_ThrowsAnException_WhenTheNamedPerspectiveAlreadyExists() { + void addPerspective_ThrowsAnException_WhenTheNamedPerspectiveAlreadyExists() { try { Element element = model.addSoftwareSystem("Name", "Description"); element.addPerspective("Security", "Data is encrypted at rest."); diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java index 3d429a746..3407988e6 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java @@ -10,35 +10,35 @@ public class ModelTests extends AbstractWorkspaceTestBase { @Test - void test_addSoftwareSystem_ThrowsAnException_WhenANullNameIsSpecified() { + void addSoftwareSystem_ThrowsAnException_WhenANullNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { model.addSoftwareSystem(null, ""); }); } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void addSoftwareSystem_ThrowsAnException_WhenAnEmptyNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { model.addSoftwareSystem(" ", ""); }); } @Test - void test_addPerson_ThrowsAnException_WhenANullNameIsSpecified() { + void addPerson_ThrowsAnException_WhenANullNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { model.addPerson(null, ""); }); } @Test - void test_addPerson_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void addPerson_ThrowsAnException_WhenAnEmptyNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { model.addPerson(" ", ""); }); } @Test - void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { + void addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { assertTrue(model.getSoftwareSystems().isEmpty()); SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); @@ -51,7 +51,7 @@ void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExis } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSameName() { + void addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSameName() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); @@ -64,7 +64,7 @@ void test_addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSa } @Test - void test_addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { + void addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { assertTrue(model.getSoftwareSystems().isEmpty()); SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); @@ -77,7 +77,7 @@ void test_addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenA } @Test - void test_addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { + void addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { assertTrue(model.getPeople().isEmpty()); Person person = model.addPerson(Location.Internal, "Some internal user", "Some description"); assertEquals(1, model.getPeople().size()); @@ -90,7 +90,7 @@ void test_addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { } @Test - void test_addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { + void addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { Person person = model.addPerson(Location.Internal, "Admin User", "Description"); assertEquals(1, model.getPeople().size()); @@ -103,7 +103,7 @@ void test_addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { } @Test - void test_addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNotExistWithTheSameName() { + void addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNotExistWithTheSameName() { assertTrue(model.getPeople().isEmpty()); Person person = model.addPerson("Some internal user", "Some description"); assertEquals(1, model.getPeople().size()); @@ -116,42 +116,42 @@ void test_addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNot } @Test - void test_getElement_ReturnsNull_WhenAnElementWithTheSpecifiedIdDoesNotExist() { + void getElement_ReturnsNull_WhenAnElementWithTheSpecifiedIdDoesNotExist() { assertNull(model.getElement("100")); } @Test - void test_getElement_ReturnsAnElement_WhenAnElementWithTheSpecifiedIdDoesExist() { + void getElement_ReturnsAnElement_WhenAnElementWithTheSpecifiedIdDoesExist() { Person person = model.addPerson(Location.Internal, "Name", "Description"); assertSame(person, model.getElement(person.getId())); } @Test - void test_contains_ReturnsFalse_WhenTheSpecifiedElementIsNotInTheModel() { + void contains_ReturnsFalse_WhenTheSpecifiedElementIsNotInTheModel() { Model newModel = new Model(); SoftwareSystem softwareSystem = newModel.addSoftwareSystem(Location.Unspecified, "Name", "Description"); assertFalse(model.contains(softwareSystem)); } @Test - void test_contains_ReturnsTrue_WhenTheSpecifiedElementIsInTheModel() { + void contains_ReturnsTrue_WhenTheSpecifiedElementIsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Unspecified, "Name", "Description"); assertTrue(model.contains(softwareSystem)); } @Test - void test_getSoftwareSystemWithName_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedNameDoesNotExist() { + void getSoftwareSystemWithName_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedNameDoesNotExist() { assertNull(model.getSoftwareSystemWithName("System X")); } @Test - void test_getSoftwareSystemWithName_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedNameExists() { + void getSoftwareSystemWithName_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedNameExists() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Description"); assertSame(softwareSystem, model.getSoftwareSystemWithName("System A")); } @Test - void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedANullId() { + void getSoftwareSystemWithId_ThrowsAnException_WhenPassedANullId() { try { model.getSoftwareSystemWithId(null); fail(); @@ -161,7 +161,7 @@ void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedANullId() { } @Test - void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedAnEmptyId() { + void getSoftwareSystemWithId_ThrowsAnException_WhenPassedAnEmptyId() { try { model.getSoftwareSystemWithId(" "); fail(); @@ -171,29 +171,29 @@ void test_getSoftwareSystemWithId_ThrowsAnException_WhenPassedAnEmptyId() { } @Test - void test_getSoftwareSystemWithId_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedIdDoesNotExist() { + void getSoftwareSystemWithId_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedIdDoesNotExist() { assertNull(model.getSoftwareSystemWithId("100")); } @Test - void test_getSoftwareSystemWithId_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedIdDoesExist() { + void getSoftwareSystemWithId_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedIdDoesExist() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Description"); assertSame(softwareSystem, model.getSoftwareSystemWithId(softwareSystem.getId())); } @Test - void test_getPersonWithName_ReturnsNull_WhenAPersonWithTheSpecifiedNameDoesNotExist() { + void getPersonWithName_ReturnsNull_WhenAPersonWithTheSpecifiedNameDoesNotExist() { assertNull(model.getPersonWithName("Admin User")); } @Test - void test_getPersonWithName_ReturnsAPerson_WhenAPersonWithTheSpecifiedNameExists() { + void getPersonWithName_ReturnsAPerson_WhenAPersonWithTheSpecifiedNameExists() { Person person = model.addPerson(Location.External, "Admin User", "Description"); assertSame(person, model.getPersonWithName("Admin User")); } @Test - void test_getRelationship_ThrowsAnException_WhenPassedANullId() { + void getRelationship_ThrowsAnException_WhenPassedANullId() { try { model.getRelationship(null); fail(); @@ -203,7 +203,7 @@ void test_getRelationship_ThrowsAnException_WhenPassedANullId() { } @Test - void test_getRelationship_ThrowsAnException_WhenPassedAnEmptyId() { + void getRelationship_ThrowsAnException_WhenPassedAnEmptyId() { try { model.getRelationship(" "); fail(); @@ -213,7 +213,7 @@ void test_getRelationship_ThrowsAnException_WhenPassedAnEmptyId() { } @Test - void test_addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnologyAndInteractionStyle() { + void addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnologyAndInteractionStyle() { SoftwareSystem a = model.addSoftwareSystem("A", ""); SoftwareSystem b = model.addSoftwareSystem("B", ""); Relationship relationship = model.addRelationship(a, b, "Uses", "HTTPS", InteractionStyle.Asynchronous); @@ -228,7 +228,7 @@ void test_addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnol } @Test - void test_addRelationship_DisallowsTheSameRelationshipToBeAddedMoreThanOnce() { + void addRelationship_DisallowsTheSameRelationshipToBeAddedMoreThanOnce() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship1 = element1.uses(element2, "Uses", ""); @@ -239,7 +239,7 @@ void test_addRelationship_DisallowsTheSameRelationshipToBeAddedMoreThanOnce() { } @Test - void test_addRelationship_AllowsMultipleRelationshipsBetweenElements() { + void addRelationship_AllowsMultipleRelationshipsBetweenElements() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship1 = element1.uses(element2, "Uses in some way", ""); @@ -250,7 +250,7 @@ void test_addRelationship_AllowsMultipleRelationshipsBetweenElements() { } @Test - void test_addRelationship_ThrowsAnException_WhenTheDestinationIsAChildOfTheSource() { + void addRelationship_ThrowsAnException_WhenTheDestinationIsAChildOfTheSource() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); Component component = container.addComponent("Component", "", ""); @@ -281,7 +281,7 @@ void test_addRelationship_ThrowsAnException_WhenTheDestinationIsAChildOfTheSourc } @Test - void test_addRelationship_ThrowsAnException_WhenTheSourceIsAChildOfTheDestination() { + void addRelationship_ThrowsAnException_WhenTheSourceIsAChildOfTheDestination() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); Component component = container.addComponent("Component", "", ""); @@ -312,7 +312,7 @@ void test_addRelationship_ThrowsAnException_WhenTheSourceIsAChildOfTheDestinatio } @Test - void test_modifyRelationship_ThrowsAnException_WhenARelationshipIsNotSpecified() { + void modifyRelationship_ThrowsAnException_WhenARelationshipIsNotSpecified() { try { model.modifyRelationship(null, "Uses", "Technology"); fail(); @@ -322,7 +322,7 @@ void test_modifyRelationship_ThrowsAnException_WhenARelationshipIsNotSpecified() } @Test - void test_modifyRelationship_ModifiesAnExistingRelationship_WhenThatRelationshipDoesNotAlreadyExist() { + void modifyRelationship_ModifiesAnExistingRelationship_WhenThatRelationshipDoesNotAlreadyExist() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship = element1.uses(element2, "", ""); @@ -333,7 +333,7 @@ void test_modifyRelationship_ModifiesAnExistingRelationship_WhenThatRelationship } @Test - void test_modifyRelationship_ThrowsAnException_WhenThatRelationshipDoesAlreadyExist() { + void modifyRelationship_ThrowsAnException_WhenThatRelationshipDoesAlreadyExist() { SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); Relationship relationship = element1.uses(element2, "Uses", "Technology"); @@ -347,7 +347,7 @@ void test_modifyRelationship_ThrowsAnException_WhenThatRelationshipDoesAlreadyEx } @Test - void test_addSoftwareSystemInstance_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + void addSoftwareSystemInstance_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); deploymentNode.add((SoftwareSystem) null); @@ -358,7 +358,7 @@ void test_addSoftwareSystemInstance_ThrowsAnException_WhenANullSoftwareSystemIsS } @Test - void test_addContainerInstance_ThrowsAnException_WhenANullContainerIsSpecified() { + void addContainerInstance_ThrowsAnException_WhenANullContainerIsSpecified() { try { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); deploymentNode.add((Container) null); @@ -369,7 +369,7 @@ void test_addContainerInstance_ThrowsAnException_WhenANullContainerIsSpecified() } @Test - void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsNotSpecified() { + void addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsNotSpecified() { DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); assertEquals("Deployment Node", deploymentNode.getName()); @@ -379,7 +379,7 @@ void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsNotS } @Test - void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsSpecified() { + void addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsSpecified() { DeploymentNode deploymentNode = model.addDeploymentNode("Development", "Deployment Node", "Description", "Technology"); assertEquals("Deployment Node", deploymentNode.getName()); @@ -389,7 +389,7 @@ void test_addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsSpec } @Test - void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironment() { + void addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironment() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); Container container1 = softwareSystem1.addContainer("Container 1", "Description", "Technology"); @@ -447,7 +447,7 @@ void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithi } @Test - void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndDefaultGroup() { + void addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndDefaultGroup() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System"); Container api = softwareSystem1.addContainer("API"); Container database = softwareSystem1.addContainer("Database"); @@ -481,7 +481,7 @@ void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithi } @Test - void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroup() { + void addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroup() { // in this test, container instances are added to two deployment groups: "Instance 1" and "Instance 2" // relationships are not replicated between element instances in other groups @@ -510,7 +510,7 @@ void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithi } @Test - void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroups() { + void addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndSpecifiedGroups() { // in this test: // - API container instances are added to "Instance 1", "Instance 2" and "Shared" // - database container instances are added to "Instance 1" and "Instance 2" @@ -550,7 +550,7 @@ void test_addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithi } @Test - void test_getElement_ThrowsAnException_WhenANullIdIsSpecified() { + void getElement_ThrowsAnException_WhenANullIdIsSpecified() { try { model.getElement(null); } catch (IllegalArgumentException iae) { @@ -559,7 +559,7 @@ void test_getElement_ThrowsAnException_WhenANullIdIsSpecified() { } @Test - void test_getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { + void getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { try { model.getElement(" "); } catch (IllegalArgumentException iae) { @@ -568,7 +568,7 @@ void test_getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { } @Test - void test_getElementWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIsSpecified() { + void getElementWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIsSpecified() { try { model.getElementWithCanonicalName(null); } catch (IllegalArgumentException iae) { @@ -577,7 +577,7 @@ void test_getElementWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIs } @Test - void test_getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpecified() { + void getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpecified() { try { model.getElementWithCanonicalName(" "); } catch (IllegalArgumentException iae) { @@ -586,12 +586,12 @@ void test_getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalName } @Test - void test_getElementWithCanonicalName_ReturnsNull_WhenAnElementWithTheSpecifiedCanonicalNameDoesNotExist() { + void getElementWithCanonicalName_ReturnsNull_WhenAnElementWithTheSpecifiedCanonicalNameDoesNotExist() { assertNull(model.getElementWithCanonicalName("Software System")); } @Test - void test_getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpecifiedCanonicalNameExists() { + void getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpecifiedCanonicalNameExists() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Web Application", "Description", "Technology"); @@ -600,7 +600,7 @@ void test_getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpec } @Test - void test_addDeploymentNode_ThrowsAnException_WhenADeploymentNodeWithTheSameNameAlreadyExists() { + void addDeploymentNode_ThrowsAnException_WhenADeploymentNodeWithTheSameNameAlreadyExists() { model.addDeploymentNode("Amazon AWS", "Description", "Technology"); try { model.addDeploymentNode("Amazon AWS", "Description", "Technology"); @@ -611,7 +611,7 @@ void test_addDeploymentNode_ThrowsAnException_WhenADeploymentNodeWithTheSameName } @Test - void test_addDeploymentNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { + void addDeploymentNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addDeploymentNode("AWS Region"); try { @@ -623,7 +623,7 @@ void test_addDeploymentNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSam } @Test - void test_addDeploymentNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { + void addDeploymentNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addInfrastructureNode("Node"); try { @@ -635,7 +635,7 @@ void test_addDeploymentNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTh } @Test - void test_addInfrastructureNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { + void addInfrastructureNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addDeploymentNode("Node"); try { @@ -647,7 +647,7 @@ void test_addInfrastructureNode_ThrowsAnException_WhenAChildDeploymentNodeWithTh } @Test - void test_addInfrastructureNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { + void addInfrastructureNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); deploymentNode.addInfrastructureNode("Node"); try { @@ -659,7 +659,7 @@ void test_addInfrastructureNode_ThrowsAnException_WhenAChildInfrastructureNodeWi } @Test - void test_setIdGenerator_ThrowsAnException_WhenANullIdGeneratorIsSpecified() { + void setIdGenerator_ThrowsAnException_WhenANullIdGeneratorIsSpecified() { try { model.setIdGenerator(null); fail(); @@ -669,7 +669,7 @@ void test_setIdGenerator_ThrowsAnException_WhenANullIdGeneratorIsSpecified() { } @Test - void test_hydrate() { + void hydrate() { Person person = new Person(); person.setId("1"); person.setName("Person"); @@ -749,7 +749,7 @@ void test_hydrate() { } @Test - void test_impliedRelationshipStrategy() { + void impliedRelationshipStrategy() { // default strategy initially assertTrue(model.getImpliedRelationshipsStrategy() instanceof DefaultImpliedRelationshipsStrategy); @@ -758,7 +758,7 @@ void test_impliedRelationshipStrategy() { } @Test - void test_setImpliedRelationshipStrategy_ResetsToTheDefaultStrategy_WhenPassedNull() { + void setImpliedRelationshipStrategy_ResetsToTheDefaultStrategy_WhenPassedNull() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); model.setImpliedRelationshipsStrategy(null); @@ -766,7 +766,7 @@ void test_setImpliedRelationshipStrategy_ResetsToTheDefaultStrategy_WhenPassedNu } @Test - void test_addSoftwareSystemInstance_AllocatesInstanceIdsPerDeploymentNode() { + void addSoftwareSystemInstance_AllocatesInstanceIdsPerDeploymentNode() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); DeploymentNode deploymentNodeA = model.addDeploymentNode("Deployment Node A", "", ""); DeploymentNode deploymentNodeB = model.addDeploymentNode("Deployment Node B", "", ""); @@ -782,7 +782,7 @@ void test_addSoftwareSystemInstance_AllocatesInstanceIdsPerDeploymentNode() { } @Test - void test_addContainerInstance_AllocatesInstanceIdsPerDeploymentNode() { + void addContainerInstance_AllocatesInstanceIdsPerDeploymentNode() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "", ""); DeploymentNode deploymentNodeA = model.addDeploymentNode("Deployment Node A", "", ""); diff --git a/structurizr-core/test/unit/com/structurizr/model/PersonTests.java b/structurizr-core/test/unit/com/structurizr/model/PersonTests.java index 6334a04f9..4a7460364 100644 --- a/structurizr-core/test/unit/com/structurizr/model/PersonTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/PersonTests.java @@ -8,26 +8,26 @@ public class PersonTests extends AbstractWorkspaceTestBase { @Test - void test_getCanonicalName() { + void getCanonicalName() { Person person = model.addPerson("Person", "Description"); assertEquals("Person://Person", person.getCanonicalName()); } @Test - void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { Person person = model.addPerson("Person", "Description"); person.setName("Name1/.Name2"); assertEquals("Person://Name1Name2", person.getCanonicalName()); } @Test - void test_getParent_ReturnsNull() { + void getParent_ReturnsNull() { Person person = model.addPerson("Person", "Description"); assertNull(person.getParent()); } @Test - void test_removeTags_DoesNotRemoveRequiredTags() { + void removeTags_DoesNotRemoveRequiredTags() { Person person = model.addPerson("Person", "Description"); assertTrue(person.getTags().contains(Tags.ELEMENT)); assertTrue(person.getTags().contains(Tags.PERSON)); @@ -40,7 +40,7 @@ void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - void test_interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() { + void interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() { Person person1 = model.addPerson("Person 1", "Description"); Person person2 = model.addPerson("Person 2", "Description"); @@ -56,7 +56,7 @@ void test_interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() { } @Test - void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { + void interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { Person person1 = model.addPerson("Person 1", "Description"); Person person2 = model.addPerson("Person 2", "Description"); @@ -72,7 +72,7 @@ void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpeci } @Test - void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { + void interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { Person person1 = model.addPerson("Person 1", "Description"); Person person2 = model.addPerson("Person 2", "Description"); @@ -88,7 +88,7 @@ void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInter } @Test - void test_setLocation_SetsTheLocationToUnspecified_WhenNullIsPassed() { + void setLocation_SetsTheLocationToUnspecified_WhenNullIsPassed() { Person person = model.addPerson("Person", "Description"); person.setLocation(null); assertEquals(Location.Unspecified, person.getLocation()); diff --git a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java index c854ff1cb..f8edd18ec 100644 --- a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java @@ -17,26 +17,26 @@ public void setUp() { } @Test - void test_getDescription_NeverReturnsNull() { + void getDescription_NeverReturnsNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, null); assertEquals("", relationship.getDescription()); } @Test - void test_getTags_WhenThereAreNoTags() { + void getTags_WhenThereAreNoTags() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); assertEquals("Relationship", relationship.getTags()); } @Test - void test_getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { + void getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); relationship.addTags("tag1", "tag2", "tag3"); assertEquals("Relationship,tag1,tag2,tag3", relationship.getTags()); } @Test - void test_setTags_ClearsTheTags_WhenPassedNull() { + void setTags_ClearsTheTags_WhenPassedNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); relationship.addTags("Tag 1", "Tag 2"); assertEquals("Relationship,Tag 1,Tag 2", relationship.getTags()); @@ -45,7 +45,7 @@ void test_setTags_ClearsTheTags_WhenPassedNull() { } @Test - void test_addTags_DoesNotDoAnything_WhenPassedNull() { + void addTags_DoesNotDoAnything_WhenPassedNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); relationship.addTags((String) null); assertEquals("Relationship", relationship.getTags()); @@ -55,20 +55,20 @@ void test_addTags_DoesNotDoAnything_WhenPassedNull() { } @Test - void test_addTags_AddsTags_WhenPassedSomeTags() { + void addTags_AddsTags_WhenPassedSomeTags() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); relationship.addTags(null, "tag1", null, "tag2"); assertEquals("Relationship,tag1,tag2", relationship.getTags()); } @Test - void test_getInteractionStyle_ReturnsNull_WhenNotExplicitlySet() { + void getInteractionStyle_ReturnsNull_WhenNotExplicitlySet() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); assertNull(relationship.getInteractionStyle()); } @Test - void test_getTags_IncludesTheInteractionStyleWhenSpecified() { + void getTags_IncludesTheInteractionStyleWhenSpecified() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); assertFalse(relationship.getTags().contains(Tags.SYNCHRONOUS)); assertFalse(relationship.getTags().contains(Tags.ASYNCHRONOUS)); @@ -83,14 +83,14 @@ void test_getTags_IncludesTheInteractionStyleWhenSpecified() { } @Test - void test_setUrl() { + void setUrl() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); relationship.setUrl("https://structurizr.com"); assertEquals("https://structurizr.com", relationship.getUrl()); } @Test - void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + void setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); relationship.setUrl("htt://blah"); @@ -98,7 +98,7 @@ void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() } @Test - void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { + void setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); relationship.setUrl("https://structurizr.com"); relationship.setUrl(null); @@ -106,7 +106,7 @@ void test_setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { } @Test - void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { + void setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); relationship.setUrl("https://structurizr.com"); relationship.setUrl(" "); @@ -114,7 +114,7 @@ void test_setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { } @Test - void test_interactionStyle_CanBeSetToNull() { + void interactionStyle_CanBeSetToNull() { Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology", null); assertNull(relationship.getInteractionStyle()); diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java index 0e7c352ec..e7019e231 100644 --- a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java @@ -11,7 +11,7 @@ public class SoftwareSystemInstanceTests extends AbstractWorkspaceTestBase { private DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @Test - void test_construction() { + void construction() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertSame(softwareSystem, instance.getSoftwareSystem()); @@ -20,7 +20,7 @@ void test_construction() { } @Test - void test_getSoftwareSystemId() { + void getSoftwareSystemId() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals(softwareSystem.getId(), instance.getSoftwareSystemId()); @@ -30,7 +30,7 @@ void test_getSoftwareSystemId() { } @Test - void test_getName_CannotBeChanged() { + void getName_CannotBeChanged() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals("System", instance.getName()); @@ -40,28 +40,28 @@ void test_getName_CannotBeChanged() { } @Test - void test_getCanonicalName() { + void getCanonicalName() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals("SoftwareSystemInstance://Default/Deployment Node/System[1]", instance.getCanonicalName()); } @Test - void test_getParent_ReturnsTheParentDeploymentNode() { + void getParent_ReturnsTheParentDeploymentNode() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals(deploymentNode, instance.getParent()); } @Test - void test_getRequiredTags() { + void getRequiredTags() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertTrue(instance.getDefaultTags().isEmpty()); } @Test - void test_getTags() { + void getTags() { softwareSystem.addTags("Tag 1"); SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); instance.addTags("Primary Instance"); @@ -70,7 +70,7 @@ void test_getTags() { } @Test - void test_removeTags_DoesNotRemoveAnyTags() { + void removeTags_DoesNotRemoveAnyTags() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertTrue(instance.getTags().contains(Tags.SOFTWARE_SYSTEM_INSTANCE)); @@ -81,7 +81,7 @@ void test_removeTags_DoesNotRemoveAnyTags() { } @Test - void test_addHealthCheck() { + void addHealthCheck() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertTrue(instance.getHealthChecks().isEmpty()); @@ -94,7 +94,7 @@ void test_addHealthCheck() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { + void addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -106,7 +106,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { + void addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -118,7 +118,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { + void addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -130,7 +130,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { + void addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -142,7 +142,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { + void addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -154,7 +154,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { + void addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -166,7 +166,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { } @Test - void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { + void addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); try { @@ -178,7 +178,7 @@ void test_addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { } @Test - void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { + void getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); assertEquals(1, instance.getDeploymentGroups().size()); @@ -186,7 +186,7 @@ void test_getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { } @Test - void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { + void getDeploymentGroups_WhenOneGroupHasBeenSpecified() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem, "Group 1"); assertEquals(1, instance.getDeploymentGroups().size()); @@ -194,7 +194,7 @@ void test_getDeploymentGroups_WhenOneGroupHasBeenSpecified() { } @Test - void test_getDeploymentGroups_WhenMultipleGroupsAreSpecified() { + void getDeploymentGroups_WhenMultipleGroupsAreSpecified() { SoftwareSystemInstance instance = deploymentNode.add(softwareSystem, "Group 1", "Group 2"); assertEquals(2, instance.getDeploymentGroups().size()); diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java index 415172f98..03f0e9c2d 100644 --- a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java @@ -12,21 +12,21 @@ public class SoftwareSystemTests extends AbstractWorkspaceTestBase { private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "Name", "Description"); @Test - void test_addContainer_ThrowsAnException_WhenANullNameIsSpecified() { + void addContainer_ThrowsAnException_WhenANullNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { softwareSystem.addContainer(null, "", ""); }); } @Test - void test_addContainer_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void addContainer_ThrowsAnException_WhenAnEmptyNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { softwareSystem.addContainer(" ", "", ""); }); } @Test - void test_addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNotExist() { + void addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNotExist() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertEquals("Web Application", container.getName()); assertEquals("Description", container.getDescription()); @@ -37,7 +37,7 @@ void test_addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNotExist( } @Test - void test_addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlreadyExists() { + void addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlreadyExists() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertEquals(1, softwareSystem.getContainers().size()); @@ -50,29 +50,29 @@ void test_addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlreadyExi } @Test - void test_getContainerWithName_ReturnsNull_WhenAContainerWithTheSpecifiedNameDoesNotExist() { + void getContainerWithName_ReturnsNull_WhenAContainerWithTheSpecifiedNameDoesNotExist() { assertNull(softwareSystem.getContainerWithName("Web Application")); } @Test - void test_GetContainerWithName_ReturnsAContainer_WhenAContainerWithTheSpecifiedNameDoesExist() { + void GetContainerWithName_ReturnsAContainer_WhenAContainerWithTheSpecifiedNameDoesExist() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertSame(container, softwareSystem.getContainerWithName("Web Application")); } @Test - void test_getContainerWithId_ReturnsNull_WhenAContainerWithTheSpecifiedIdDoesNotExist() { + void getContainerWithId_ReturnsNull_WhenAContainerWithTheSpecifiedIdDoesNotExist() { assertNull(softwareSystem.getContainerWithId("100")); } @Test - void test_GetContainerWithId_ReturnsAContainer_WhenAContainerWithTheSpecifiedIdDoesExist() { + void GetContainerWithId_ReturnsAContainer_WhenAContainerWithTheSpecifiedIdDoesExist() { Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); assertSame(container, softwareSystem.getContainerWithId(container.getId())); } @Test - void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { + void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); systemA.uses(systemB, "Gets some data from"); @@ -86,7 +86,7 @@ void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { } @Test - void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADifferentRelationshipAlreadyExists() { + void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADifferentRelationshipAlreadyExists() { SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); systemA.uses(systemB, "Gets data using the REST API"); @@ -106,7 +106,7 @@ void test_uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADif } @Test - void test_uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenTheSameRelationshipAlreadyExists() { + void uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenTheSameRelationshipAlreadyExists() { SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); systemA.uses(systemB, "Gets data using the REST API"); @@ -116,7 +116,7 @@ void test_uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_Wh } @Test - void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson() { + void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); Person person = model.addPerson(Location.Internal, "User", "Description"); system.delivers(person, "E-mails results to"); @@ -130,7 +130,7 @@ void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPers } @Test - void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenADifferentRelationshipAlreadyExists() { + void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenADifferentRelationshipAlreadyExists() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); Person person = model.addPerson(Location.Internal, "User", "Description"); system.delivers(person, "E-mails results to"); @@ -151,7 +151,7 @@ void test_delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPers } @Test - void test_delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenTheSameRelationshipAlreadyExists() { + void delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenTheSameRelationshipAlreadyExists() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); Person person = model.addPerson(Location.Internal, "User", "Description"); system.delivers(person, "E-mails results to"); @@ -161,30 +161,30 @@ void test_delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAn } @Test - void test_getTags_IncludesSoftwareSystemByDefault() { + void getTags_IncludesSoftwareSystemByDefault() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); assertEquals("Element,Software System", system.getTags()); } @Test - void test_getCanonicalName() { + void getCanonicalName() { SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); assertEquals("SoftwareSystem://System", system.getCanonicalName()); } @Test - void test_getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name1/.Name2", "Description"); assertEquals("SoftwareSystem://Name1Name2", softwareSystem.getCanonicalName()); } @Test - void test_getParent_ReturnsNull() { + void getParent_ReturnsNull() { assertNull(softwareSystem.getParent()); } @Test - void test_removeTags_DoesNotRemoveRequiredTags() { + void removeTags_DoesNotRemoveRequiredTags() { assertTrue(softwareSystem.getTags().contains(Tags.ELEMENT)); assertTrue(softwareSystem.getTags().contains(Tags.SOFTWARE_SYSTEM)); @@ -196,7 +196,7 @@ void test_removeTags_DoesNotRemoveRequiredTags() { } @Test - void test_getContainerWithName_ThrowsAnException_WhenANullNameIsSpecified() { + void getContainerWithName_ThrowsAnException_WhenANullNameIsSpecified() { try { softwareSystem.getContainerWithName(null); fail(); @@ -206,7 +206,7 @@ void test_getContainerWithName_ThrowsAnException_WhenANullNameIsSpecified() { } @Test - void test_getContainerWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + void getContainerWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { try { softwareSystem.getContainerWithName(" "); fail(); @@ -216,7 +216,7 @@ void test_getContainerWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { } @Test - void test_getContainerWithId_ThrowsAnException_WhenANullIdIsSpecified() { + void getContainerWithId_ThrowsAnException_WhenANullIdIsSpecified() { try { softwareSystem.getContainerWithId(null); fail(); @@ -226,7 +226,7 @@ void test_getContainerWithId_ThrowsAnException_WhenANullIdIsSpecified() { } @Test - void test_getContainerWithId_ThrowsAnException_WhenAnEmptyIdIsSpecified() { + void getContainerWithId_ThrowsAnException_WhenAnEmptyIdIsSpecified() { try { softwareSystem.getContainerWithId(" "); fail(); diff --git a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java index 2c6c753e6..7d9a25af6 100644 --- a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java @@ -9,7 +9,7 @@ public class ImageUtilsTests { @Test - void test_getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + void getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { ImageUtils.getContentType(null); fail(); @@ -19,7 +19,7 @@ void test_getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exc } @Test - void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + void getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { try { ImageUtils.getContentType(new File("../structurizr-core")); fail(); @@ -30,7 +30,7 @@ void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() } @Test - void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + void getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { try { ImageUtils.getContentType(new File("../build.gradle")); fail(); @@ -41,7 +41,7 @@ void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage } @Test - void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + void getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { try { ImageUtils.getContentType(new File("./foo.xml")); fail(); @@ -51,13 +51,13 @@ void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist } @Test - void test_getContentType_ReturnsTheContentType_WhenAFileIsSpecified() throws Exception { + void getContentType_ReturnsTheContentType_WhenAFileIsSpecified() throws Exception { String contentType = ImageUtils.getContentType(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); assertEquals("image/png", contentType); } @Test - void test_getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + void getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { ImageUtils.getImageAsBase64(null); fail(); @@ -67,7 +67,7 @@ void test_getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() throws E } @Test - void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + void getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { try { ImageUtils.getImageAsBase64(new File("../structurizr-core")); fail(); @@ -78,7 +78,7 @@ void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile } @Test - void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + void getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { try { ImageUtils.getImageAsBase64(new File("../build.gradle")); fail(); @@ -89,7 +89,7 @@ void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnIma } @Test - void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + void getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { try { ImageUtils.getImageAsBase64(new File("./foo.xml")); fail(); @@ -99,13 +99,13 @@ void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExi } @Test - void test_getImageAsBase64_ReturnsTheImageAsABase64EncodedString_WhenAFileIsSpecified() throws Exception { + void getImageAsBase64_ReturnsTheImageAsABase64EncodedString_WhenAFileIsSpecified() throws Exception { String imageAsBase64 = ImageUtils.getImageAsBase64(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); assertTrue(imageAsBase64.startsWith("iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADApo5rAAA")); // the actual base64 encoded string varies between Java 8 and 9 } @Test - void test_getImageAsDataUri_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + void getImageAsDataUri_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { ImageUtils.getImageAsDataUri(null); fail(); @@ -115,7 +115,7 @@ void test_getImageAsDataUri_ThrowsAnException_WhenANullFileIsSpecified() throws } @Test - void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + void getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { try { ImageUtils.getImageAsDataUri(new File("../structurizr-core")); fail(); @@ -126,7 +126,7 @@ void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFil } @Test - void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + void getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { try { ImageUtils.getImageAsDataUri(new File("../build.gradle")); fail(); @@ -137,7 +137,7 @@ void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnIm } @Test - void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + void getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { try { ImageUtils.getImageAsDataUri(new File("./foo.xml")); fail(); @@ -147,14 +147,14 @@ void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotEx } @Test - void test_getImageAsDataUri_ReturnsTheImageAsADataUri_WhenAFileIsSpecified() throws Exception { + void getImageAsDataUri_ReturnsTheImageAsADataUri_WhenAFileIsSpecified() throws Exception { String imageAsDataUri = ImageUtils.getImageAsDataUri(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); System.out.println(imageAsDataUri); assertTrue(imageAsDataUri.startsWith("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADA")); // the actual base64 encoded string varies between Java 8 and 9 } @Test - void test_validateImage() { + void validateImage() { // allowed ImageUtils.validateImage("https://structurizr.com/image.png"); ImageUtils.validateImage("data:image/png;base64,iVBORw0KGg"); @@ -170,7 +170,7 @@ void test_validateImage() { } @Test - void test_isSupportedDataUri() { + void isSupportedDataUri() { assertTrue(ImageUtils.isSupportedDataUri("data:image/png;base64,iVBORw0KGg")); assertTrue(ImageUtils.isSupportedDataUri("data:image/jpeg;base64,iVBORw0KGg")); assertFalse(ImageUtils.isSupportedDataUri("data:image/svg+xml;base64,iVBORw0KGg")); diff --git a/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java index 98bf0a7f6..dda79bdee 100644 --- a/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java @@ -8,18 +8,18 @@ public class StringUtilsTests { @Test - void test_isNullOrEmpty_ReturnsTrue_WhenPassedNull() { + void isNullOrEmpty_ReturnsTrue_WhenPassedNull() { assertTrue(StringUtils.isNullOrEmpty(null)); } @Test - void test_isNullOrEmpty_ReturnsTrue_WhenPassedAnEmptyString() { + void isNullOrEmpty_ReturnsTrue_WhenPassedAnEmptyString() { assertTrue(StringUtils.isNullOrEmpty("")); assertTrue(StringUtils.isNullOrEmpty(" ")); } @Test - void test_isNullOrEmpty_ReturnsFalse_WhenPassedANonEmptyString() { + void isNullOrEmpty_ReturnsFalse_WhenPassedANonEmptyString() { assertFalse(StringUtils.isNullOrEmpty("Hello World!")); } diff --git a/structurizr-core/test/unit/com/structurizr/util/UrlTests.java b/structurizr-core/test/unit/com/structurizr/util/UrlTests.java index 70ba4f6db..c9e179095 100644 --- a/structurizr-core/test/unit/com/structurizr/util/UrlTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/UrlTests.java @@ -8,23 +8,23 @@ public class UrlTests { @Test - void test_isUrl_ReturnsFalse_WhenPassedNull() { + void isUrl_ReturnsFalse_WhenPassedNull() { assertFalse(Url.isUrl(null)); } @Test - void test_isUrl_ReturnsFalse_WhenPassedAnEmptyString() { + void isUrl_ReturnsFalse_WhenPassedAnEmptyString() { assertFalse(Url.isUrl("")); assertFalse(Url.isUrl(" ")); } @Test - void test_isUrl_ReturnsFalse_WhenPassedAnInvalidUrl() { + void isUrl_ReturnsFalse_WhenPassedAnInvalidUrl() { assertFalse(Url.isUrl("www.google.com")); } @Test - void test_isUrl_ReturnsTrue_WhenPassedAValidUrl() { + void isUrl_ReturnsTrue_WhenPassedAValidUrl() { assertTrue(Url.isUrl("https://www.google.com")); } diff --git a/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java b/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java index cb2efd64c..3ffd208d9 100644 --- a/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java @@ -7,7 +7,7 @@ public class AutomaticLayoutTests { @Test - void test_setAutomaticLayout() { + void setAutomaticLayout() { AutomaticLayout automaticLayout = new AutomaticLayout(AutomaticLayout.Implementation.Dagre, AutomaticLayout.RankDirection.LeftRight, 100, 200, 300, true); assertEquals(AutomaticLayout.RankDirection.LeftRight, automaticLayout.getRankDirection()); @@ -18,7 +18,7 @@ void test_setAutomaticLayout() { } @Test - void test_setRankDirection_ThrowsAnException_WhenNullIsSpecified() { + void setRankDirection_ThrowsAnException_WhenNullIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setRankDirection(null); @@ -29,7 +29,7 @@ void test_setRankDirection_ThrowsAnException_WhenNullIsSpecified() { } @Test - void test_setRankSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void setRankSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setRankSeparation(-100); @@ -40,7 +40,7 @@ void test_setRankSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() } @Test - void test_setNodeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void setNodeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setNodeSeparation(-100); @@ -51,7 +51,7 @@ void test_setNodeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() } @Test - void test_setEdgeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void setEdgeSeparation_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { AutomaticLayout automaticLayout = new AutomaticLayout(); automaticLayout.setEdgeSeparation(-100); diff --git a/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java b/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java index dc01bf8b4..47c383281 100644 --- a/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java @@ -15,38 +15,38 @@ public void setUp() { } @Test - void test_setLogo_WithAUrl() { + void setLogo_WithAUrl() { branding.setLogo("https://structurizr.com/static/img/structurizr-logo.png"); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", branding.getLogo()); } @Test - void test_setLogo_WithAUrlThatHasATrailingSpace() { + void setLogo_WithAUrlThatHasATrailingSpace() { branding.setLogo("https://structurizr.com/static/img/structurizr-logo.png "); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", branding.getLogo()); } @Test - void test_setLogo_WithADataUri() { + void setLogo_WithADataUri() { branding.setLogo("data:image/png;base64,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"); assertEquals("data:image/png;base64,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", branding.getLogo()); } @Test - void test_setLogo_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + void setLogo_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { branding.setLogo("htt://blah"); }); } @Test - void test_setLogo_DoesNothing_WhenANullUrlIsSpecified() { + void setLogo_DoesNothing_WhenANullUrlIsSpecified() { branding.setLogo(null); assertNull(branding.getLogo()); } @Test - void test_setLogo_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void setLogo_DoesNothing_WhenAnEmptyUrlIsSpecified() { branding.setLogo(" "); assertNull(branding.getLogo()); } diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java index f85ec7794..215f1c36b 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java @@ -8,21 +8,21 @@ public class ColorPairTests { @Test - void test_construction() { + void construction() { ColorPair colorPair = new ColorPair("#ffffff", "#000000"); assertEquals("#ffffff", colorPair.getBackground()); assertEquals("#000000", colorPair.getForeground()); } @Test - void test_setBackground_WithAValidHtmlColorCode() { + void setBackground_WithAValidHtmlColorCode() { ColorPair colorPair = new ColorPair(); colorPair.setBackground("#ffffff"); assertEquals("#ffffff", colorPair.getBackground()); } @Test - void test_setBackground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { + void setBackground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setBackground(null); @@ -33,7 +33,7 @@ void test_setBackground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { } @Test - void test_setBackground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { + void setBackground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setBackground(""); @@ -44,7 +44,7 @@ void test_setBackground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() } @Test - void test_setBackground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { + void setBackground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setBackground("ffffff"); @@ -55,14 +55,14 @@ void test_setBackground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified( } @Test - void test_setForeground_WithAValidHtmlColorCode() { + void setForeground_WithAValidHtmlColorCode() { ColorPair colorPair = new ColorPair(); colorPair.setForeground("#000000"); assertEquals("#000000", colorPair.getForeground()); } @Test - void test_setForeground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { + void setForeground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setForeground(null); @@ -73,7 +73,7 @@ void test_setForeground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { } @Test - void test_setForeground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { + void setForeground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setForeground(""); @@ -84,7 +84,7 @@ void test_setForeground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() } @Test - void test_setForeground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { + void setForeground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { try { ColorPair colorPair = new ColorPair(); colorPair.setForeground("000000"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java index c76263ce2..ad295c6db 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java @@ -8,24 +8,24 @@ public class ColorTests { @Test - void test_isHexColorCode_ReturnsFalse_WhenPassedNull() { + void isHexColorCode_ReturnsFalse_WhenPassedNull() { assertFalse(Color.isHexColorCode(null)); } @Test - void test_isHexColorCode_ReturnsFalse_WhenPassedAnEmptyString() { + void isHexColorCode_ReturnsFalse_WhenPassedAnEmptyString() { assertFalse(Color.isHexColorCode("")); } @Test - void test_isHexColorCode_ReturnsFalse_WhenPassedAnInvalidString() { + void isHexColorCode_ReturnsFalse_WhenPassedAnInvalidString() { assertFalse(Color.isHexColorCode("ffffff")); assertFalse(Color.isHexColorCode("#fffff")); assertFalse(Color.isHexColorCode("#gggggg")); } @Test - void test_isHexColorCode_ReturnsTrue_WhenPassedAnValidString() { + void isHexColorCode_ReturnsTrue_WhenPassedAnValidString() { assertTrue(Color.isHexColorCode("#abcdef")); assertTrue(Color.isHexColorCode("#ABCDEF")); assertTrue(Color.isHexColorCode("#123456")); diff --git a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java index eaeeb4300..ddf7daf9f 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java @@ -24,7 +24,7 @@ public void setUp() { } @Test - void test_construction() { + void construction() { assertEquals("The System - Web Application - Components", view.getName()); assertEquals("Some description", view.getDescription()); assertEquals(0, view.getElements().size()); @@ -35,14 +35,14 @@ void test_construction() { } @Test - void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { assertEquals(0, view.getElements().size()); view.addAllSoftwareSystems(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -54,14 +54,14 @@ void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareS } @Test - void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { assertEquals(0, view.getElements().size()); view.addAllPeople(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson(Location.External, "User A", "Description"); Person userB = model.addPerson(Location.External, "User B", "Description"); @@ -73,14 +73,14 @@ void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { assertEquals(0, view.getElements().size()); view.addAllElements(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndComponents_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersAndComponentsInTheModel() { + void addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndComponents_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersAndComponentsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); Person userA = model.addPerson(Location.External, "User A", "Description"); @@ -102,14 +102,14 @@ void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndComponen } @Test - void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { + void addAllContainers_DoesNothing_WhenThereAreNoContainers() { assertEquals(0, view.getElements().size()); view.addAllContainers(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { + void addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); @@ -121,14 +121,14 @@ void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { } @Test - void test_addAllComponents_DoesNothing_WhenThereAreNoComponents() { + void addAllComponents_DoesNothing_WhenThereAreNoComponents() { assertEquals(0, view.getElements().size()); view.addAllComponents(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() { + void addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); @@ -140,7 +140,7 @@ void test_addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() { } @Test - void test_add_ThrowsAnException_WhenANullContainerIsSpecified() { + void add_ThrowsAnException_WhenANullContainerIsSpecified() { assertEquals(0, view.getElements().size()); try { @@ -152,7 +152,7 @@ void test_add_ThrowsAnException_WhenANullContainerIsSpecified() { } @Test - void test_add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { + void add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); assertEquals(0, view.getElements().size()); @@ -162,7 +162,7 @@ void test_add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { } @Test - void test_add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { + void add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); view.add(database); assertEquals(1, view.getElements().size()); @@ -173,7 +173,7 @@ void test_add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { } @Test - void test_remove_ThrowsAndException_WhenANullContainerIsPassed() { + void remove_ThrowsAndException_WhenANullContainerIsPassed() { try { view.remove((Container) null); fail(); @@ -183,7 +183,7 @@ void test_remove_ThrowsAndException_WhenANullContainerIsPassed() { } @Test - void test_remove_RemovesTheContainer_WhenTheContainerIsInTheView() { + void remove_RemovesTheContainer_WhenTheContainerIsInTheView() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); view.add(database); assertEquals(1, view.getElements().size()); @@ -194,7 +194,7 @@ void test_remove_RemovesTheContainer_WhenTheContainerIsInTheView() { } @Test - void test_remove_DoesNothing_WhenTheContainerIsNotInTheView() { + void remove_DoesNothing_WhenTheContainerIsNotInTheView() { Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); @@ -208,14 +208,14 @@ void test_remove_DoesNothing_WhenTheContainerIsNotInTheView() { } @Test - void test_add_DoesNothing_WhenANullComponentIsSpecified() { + void add_DoesNothing_WhenANullComponentIsSpecified() { assertEquals(0, view.getElements().size()); view.add((Component) null); assertEquals(0, view.getElements().size()); } @Test - void test_add_AddsTheComponent_WhenTheComponentIsNotInTheViewAlready() { + void add_AddsTheComponent_WhenTheComponentIsNotInTheViewAlready() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); assertEquals(0, view.getElements().size()); @@ -225,7 +225,7 @@ void test_add_AddsTheComponent_WhenTheComponentIsNotInTheViewAlready() { } @Test - void test_add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { + void add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); view.add(componentA); assertEquals(1, view.getElements().size()); @@ -236,7 +236,7 @@ void test_add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { } @Test - void test_add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentContainer() { + void add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentContainer() { try { SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); @@ -253,7 +253,7 @@ void test_add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentContainer } @Test - void test_add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { + void add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { try { view.add(webApplication); fail(); @@ -263,7 +263,7 @@ void test_add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { } @Test - void test_add_DoesNothing_WhenTheContainerOfTheViewIsAddedViaDependency() { + void add_DoesNothing_WhenTheContainerOfTheViewIsAddedViaDependency() { final SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "Some other system", "external system that uses our web application"); final Relationship relationshipFromExternalSystem = softwareSystem.uses(webApplication, ""); @@ -274,7 +274,7 @@ void test_add_DoesNothing_WhenTheContainerOfTheViewIsAddedViaDependency() { } @Test - void test_remove_DoesNothing_WhenANullComponentIsPassed() { + void remove_DoesNothing_WhenANullComponentIsPassed() { try { view.remove((Component) null); fail(); @@ -284,7 +284,7 @@ void test_remove_DoesNothing_WhenANullComponentIsPassed() { } @Test - void test_remove_RemovesTheComponent_WhenTheComponentIsInTheView() { + void remove_RemovesTheComponent_WhenTheComponentIsInTheView() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); view.add(componentA); assertEquals(1, view.getElements().size()); @@ -295,7 +295,7 @@ void test_remove_RemovesTheComponent_WhenTheComponentIsInTheView() { } @Test - void test_remove_RemovesTheComponentAndRelationships_WhenTheComponentIsInTheViewAndHasArelationshipToAnotherElement() { + void remove_RemovesTheComponentAndRelationships_WhenTheComponentIsInTheViewAndHasArelationshipToAnotherElement() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); componentA.uses(componentB, "uses"); @@ -311,7 +311,7 @@ void test_remove_RemovesTheComponentAndRelationships_WhenTheComponentIsInTheView } @Test - void test_remove_DoesNothing_WhenTheComponentIsNotInTheView() { + void remove_DoesNothing_WhenTheComponentIsNotInTheView() { Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); @@ -325,14 +325,14 @@ void test_remove_DoesNothing_WhenTheComponentIsNotInTheView() { } @Test - void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { + void addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { view.addNearestNeighbours(null); assertEquals(0, view.getElements().size()); } @Test - void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { Component component = webApplication.addComponent("Component", "", ""); view.add(component); assertEquals(1, view.getElements().size()); @@ -342,7 +342,7 @@ void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { } @Test - void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); Person userA = model.addPerson("User A", "Description"); @@ -400,7 +400,7 @@ void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeig } @Test - void test_addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDirectRelationshipsWithAComponent() { + void addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDirectRelationshipsWithAComponent() { SoftwareSystem source = model.addSoftwareSystem("Source", ""); SoftwareSystem destination = model.addSoftwareSystem("Destination", ""); @@ -424,7 +424,7 @@ void test_addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDirectRelat } @Test - void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipToAContainerInTheSameSoftwareSystem() { + void addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipToAContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -441,7 +441,7 @@ void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshi } @Test - void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipFromAContainerInTheSameSoftwareSystem() { + void addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipFromAContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -458,7 +458,7 @@ void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshi } @Test - void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipToAComponentInADifferentContainerInTheSameSoftwareSystem() { + void addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipToAComponentInADifferentContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -476,7 +476,7 @@ void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelat } @Test - void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipFromAComponentInADifferentContainerInTheSameSoftwareSystem() { + void addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipFromAComponentInADifferentContainerInTheSameSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -494,7 +494,7 @@ void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelat } @Test - void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAContainerInAnotherSoftwareSystem() { + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAContainerInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -512,7 +512,7 @@ void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasA } @Test - void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAContainerInAnotherSoftwareSystem() { + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAContainerInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -530,7 +530,7 @@ void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasA } @Test - void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAComponentInAnotherSoftwareSystem() { + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAComponentInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -549,7 +549,7 @@ void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasA } @Test - void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAComponentInAnotherSoftwareSystem() { + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAComponentInAnotherSoftwareSystem() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); Container containerA = softwareSystemA.addContainer("Container A", "", ""); Component componentA = containerA.addComponent("Component A", "", ""); @@ -568,7 +568,7 @@ void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasA } @Test - void test_addDefaultElements() { + void addDefaultElements() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); CustomElement element = model.addCustomElement("Custom"); @@ -615,7 +615,7 @@ void test_addDefaultElements() { } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { + void addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); @@ -629,7 +629,7 @@ void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfT } @Test - void test_addContainer_ThrowsAnException_WhenTheContainerIsTheScopeOfTheView() { + void addContainer_ThrowsAnException_WhenTheContainerIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); @@ -643,7 +643,7 @@ void test_addContainer_ThrowsAnException_WhenTheContainerIsTheScopeOfTheView() { } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { + void addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -665,7 +665,7 @@ void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded( } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { + void addSoftwareSystem_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -687,7 +687,7 @@ void test_addSoftwareSystem_ThrowsAnException_WhenAChildComponentIsAlreadyAdded( } @Test - void test_addContainer_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { + void addContainer_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -709,7 +709,7 @@ void test_addContainer_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { } @Test - void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + void addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -731,7 +731,7 @@ void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { } @Test - void test_addComponent_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + void addComponent_ThrowsAnException_WhenTheParentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java index ac004de8e..ff3490c42 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java @@ -8,14 +8,14 @@ public class ConfigurationTests extends AbstractWorkspaceTestBase { @Test - void test_defaultView_DoesNothing_WhenPassedNull() { + void defaultView_DoesNothing_WhenPassedNull() { Configuration configuration = new Configuration(); configuration.setDefaultView((View) null); assertNull(configuration.getDefaultView()); } @Test - void test_defaultView() { + void defaultView() { SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); Configuration configuration = new Configuration(); configuration.setDefaultView(view); @@ -23,7 +23,7 @@ void test_defaultView() { } @Test - void test_copyConfigurationFrom() { + void copyConfigurationFrom() { Configuration source = new Configuration(); source.setLastSavedView("someKey"); @@ -33,21 +33,21 @@ void test_copyConfigurationFrom() { } @Test - void test_setTheme_WithAUrl() { + void setTheme_WithAUrl() { Configuration configuration = new Configuration(); configuration.setTheme("https://example.com/theme.json"); assertEquals("https://example.com/theme.json", configuration.getTheme()); } @Test - void test_setTheme_WithAUrlThatHasATrailingSpace() { + void setTheme_WithAUrlThatHasATrailingSpace() { Configuration configuration = new Configuration(); configuration.setTheme("https://example.com/theme.json "); assertEquals("https://example.com/theme.json", configuration.getTheme()); } @Test - void test_setTheme_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + void setTheme_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { Configuration configuration = new Configuration(); configuration.setTheme("htt://blah"); @@ -55,14 +55,14 @@ void test_setTheme_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified( } @Test - void test_setTheme_DoesNothing_WhenANullUrlIsSpecified() { + void setTheme_DoesNothing_WhenANullUrlIsSpecified() { Configuration configuration = new Configuration(); configuration.setTheme(null); assertNull(configuration.getTheme()); } @Test - void test_setTheme_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void setTheme_DoesNothing_WhenAnEmptyUrlIsSpecified() { Configuration configuration = new Configuration(); configuration.setTheme(" "); assertNull(configuration.getTheme()); diff --git a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java index 2c0c52b9a..fb97520f8 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java @@ -19,7 +19,7 @@ public void setUp() { } @Test - void test_construction() { + void construction() { assertEquals("The System - Containers", view.getName()); assertEquals("Description", view.getDescription()); assertEquals(0, view.getElements().size()); @@ -29,14 +29,14 @@ void test_construction() { } @Test - void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { assertEquals(0, view.getElements().size()); view.addAllSoftwareSystems(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -48,14 +48,14 @@ void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareS } @Test - void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { assertEquals(0, view.getElements().size()); view.addAllPeople(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson(Location.External, "User A", "Description"); Person userB = model.addPerson(Location.External, "User B", "Description"); @@ -67,14 +67,14 @@ void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { assertEquals(0, view.getElements().size()); view.addAllElements(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersInTheModel() { + void addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); Person userA = model.addPerson(Location.External, "User A", "Description"); @@ -94,14 +94,14 @@ void test_addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_WhenThereA } @Test - void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { + void addAllContainers_DoesNothing_WhenThereAreNoContainers() { assertEquals(0, view.getElements().size()); view.addAllContainers(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { + void addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { Container webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); Container database = softwareSystem.addContainer("Database", "Does something", "MySQL"); @@ -113,21 +113,21 @@ void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { } @Test - void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { + void addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { view.addNearestNeighbours(null); assertEquals(0, view.getElements().size()); } @Test - void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { view.addNearestNeighbours(softwareSystem); assertEquals(0, view.getElements().size()); } @Test - void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); Person userA = model.addPerson("User A", "Description"); @@ -186,7 +186,7 @@ void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeig } @Test - void test_remove_RemovesContainer() { + void remove_RemovesContainer() { Container webApplication = softwareSystem.addContainer("Web Application", "", ""); Container database = softwareSystem.addContainer("Database", "", ""); @@ -199,7 +199,7 @@ void test_remove_RemovesContainer() { } @Test - void test_remove_ElementsWithTag() { + void remove_ElementsWithTag() { final String TAG = "myTag"; Container webApplication = softwareSystem.addContainer("Web Application", "", ""); Container database = softwareSystem.addContainer("Database", "", ""); @@ -214,7 +214,7 @@ void test_remove_ElementsWithTag() { } @Test - void test_remove_RelationshipWithTag() { + void remove_RelationshipWithTag() { final String TAG = "myTag"; Container webApplication = softwareSystem.addContainer("Web Application", "", ""); Container database = softwareSystem.addContainer("Database", "", ""); @@ -230,7 +230,7 @@ void test_remove_RelationshipWithTag() { } @Test - void test_addDependentSoftwareSystem() { + void addDependentSoftwareSystem() { assertEquals(0, view.getElements().size()); assertEquals(0, view.getRelationships().size()); @@ -249,7 +249,7 @@ void test_addDependentSoftwareSystem() { } @Test - void test_addDependentSoftwareSystem2() { + void addDependentSoftwareSystem2() { Container container1a = softwareSystem.addContainer("Container 1A", "", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem(Location.External, "SoftwareSystem 2", ""); @@ -266,7 +266,7 @@ void test_addDependentSoftwareSystem2() { } @Test - void test_addDefaultElements() { + void addDefaultElements() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); CustomElement element = model.addCustomElement("Custom"); @@ -307,7 +307,7 @@ void test_addDefaultElements() { } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { + void addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); view = new ContainerView(softwareSystem, "containers", "Description"); @@ -320,7 +320,7 @@ void test_addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfT } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { + void addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -341,7 +341,7 @@ void test_addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded( } @Test - void test_addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + void addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { try { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); diff --git a/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java b/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java index 26e329dfd..18744e7d7 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java @@ -10,7 +10,7 @@ public class DefaultLayoutMergeStrategyTests { @Test - void test_copyLayoutInformation_WhenCanonicalNamesHaveNotChanged() { + void copyLayoutInformation_WhenCanonicalNamesHaveNotChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "", ""); @@ -33,7 +33,7 @@ void test_copyLayoutInformation_WhenCanonicalNamesHaveNotChanged() { } @Test - void test_copyLayoutInformation_WhenAParentElementNameHasChanged() { + void copyLayoutInformation_WhenAParentElementNameHasChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "", ""); @@ -56,7 +56,7 @@ void test_copyLayoutInformation_WhenAParentElementNameHasChanged() { } @Test - void test_copyLayoutInformation_WhenAnElementNameHasChangedButTheDescriptionHasNotChanged() { + void copyLayoutInformation_WhenAnElementNameHasChangedButTheDescriptionHasNotChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); @@ -79,7 +79,7 @@ void test_copyLayoutInformation_WhenAnElementNameHasChangedButTheDescriptionHasN } @Test - void test_copyLayoutInformation_WhenAnElementNameAndDescriptionHaveChangedButTheIdHasNotChanged() { + void copyLayoutInformation_WhenAnElementNameAndDescriptionHaveChangedButTheIdHasNotChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); @@ -102,7 +102,7 @@ void test_copyLayoutInformation_WhenAnElementNameAndDescriptionHaveChangedButThe } @Test - void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChanged() { + void copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChanged() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); @@ -126,7 +126,7 @@ void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChanged( } @Test - void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChangedAndDescriptionWasNull() { + void copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChangedAndDescriptionWasNull() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); Container container1 = softwareSystem1.addContainer("Container"); @@ -151,7 +151,7 @@ void test_copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChangedA } @Test - void test_copyLayoutInformation_DoesNotThrowAnExceptionWhenAddingAnElementToAView() { + void copyLayoutInformation_DoesNotThrowAnExceptionWhenAddingAnElementToAView() { Workspace workspace1 = new Workspace("1", ""); SoftwareSystem softwareSystem1A = workspace1.getModel().addSoftwareSystem("Software System A"); SoftwareSystem softwareSystem1B = workspace1.getModel().addSoftwareSystem("Software System B"); diff --git a/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java b/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java index 36394293b..dd4e3c5e1 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java @@ -16,27 +16,27 @@ public void setup() { } @Test - void test_getName_WithNoSoftwareSystemAndNoEnvironment() { + void getName_WithNoSoftwareSystemAndNoEnvironment() { deploymentView = views.createDeploymentView("deployment", "Description"); assertEquals("Deployment - Default", deploymentView.getName()); } @Test - void test_getName_WithNoSoftwareSystemAndAnEnvironment() { + void getName_WithNoSoftwareSystemAndAnEnvironment() { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.setEnvironment("Live"); assertEquals("Deployment - Live", deploymentView.getName()); } @Test - void test_getName_WithASoftwareSystemAndNoEnvironment() { + void getName_WithASoftwareSystemAndNoEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); assertEquals("Software System - Deployment - Default", deploymentView.getName()); } @Test - void test_getName_WithASoftwareSystemAndAnEnvironment() { + void getName_WithASoftwareSystemAndAnEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); deploymentView.setEnvironment("Live"); @@ -44,7 +44,7 @@ void test_getName_WithASoftwareSystemAndAnEnvironment() { } @Test - void test_addDeploymentNode_ThrowsAnException_WhenPassedNull() { + void addDeploymentNode_ThrowsAnException_WhenPassedNull() { try { deploymentView = views.createDeploymentView("key", "Description"); deploymentView.add((DeploymentNode) null); @@ -55,7 +55,7 @@ void test_addDeploymentNode_ThrowsAnException_WhenPassedNull() { } @Test - void test_addRelationship_ThrowsAnException_WhenPassedNull() { + void addRelationship_ThrowsAnException_WhenPassedNull() { try { deploymentView = views.createDeploymentView("key", "Description"); deploymentView.add((Relationship) null); @@ -66,7 +66,7 @@ void test_addRelationship_ThrowsAnException_WhenPassedNull() { } @Test - void test_addAllDeploymentNodes_DoesNothing_WhenThereAreNoTopLevelDeploymentNodes() { + void addAllDeploymentNodes_DoesNothing_WhenThereAreNoTopLevelDeploymentNodes() { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.addAllDeploymentNodes(); @@ -74,7 +74,7 @@ void test_addAllDeploymentNodes_DoesNothing_WhenThereAreNoTopLevelDeploymentNode } @Test - void test_addAllDeploymentNodes_DoesNothing_WhenThereAreTopLevelDeploymentNodesButNoContainerInstances() { + void addAllDeploymentNodes_DoesNothing_WhenThereAreTopLevelDeploymentNodesButNoContainerInstances() { deploymentView = views.createDeploymentView("deployment", "Description"); model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -83,7 +83,7 @@ void test_addAllDeploymentNodes_DoesNothing_WhenThereAreTopLevelDeploymentNodesB } @Test - void test_addAllDeploymentNodes_DoesNothing_WhenThereNoDeploymentNodesForTheDeploymentEnvironment() { + void addAllDeploymentNodes_DoesNothing_WhenThereNoDeploymentNodesForTheDeploymentEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -96,7 +96,7 @@ void test_addAllDeploymentNodes_DoesNothing_WhenThereNoDeploymentNodesForTheDepl } @Test - void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreTopLevelDeploymentNodesWithContainerInstances() { + void addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreTopLevelDeploymentNodesWithContainerInstances() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -110,7 +110,7 @@ void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThe } @Test - void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreChildDeploymentNodesWithContainerInstances() { + void addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThereAreChildDeploymentNodesWithContainerInstances() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -126,7 +126,7 @@ void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstances_WhenThe } @Test - void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstancesOnlyForTheSoftwareSystemInScope() { + void addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstancesOnlyForTheSoftwareSystemInScope() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", ""); Container container1 = softwareSystem1.addContainer("Container 1", "Description", "Technology"); DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); @@ -149,7 +149,7 @@ void test_addAllDeploymentNodes_AddsDeploymentNodesAndContainerInstancesOnlyForT } @Test - void test_addDeploymentNode_AddsTheParentToo() { + void addDeploymentNode_AddsTheParentToo() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -165,7 +165,7 @@ void test_addDeploymentNode_AddsTheParentToo() { } @Test - void test_addDeploymentNode_ThrowsAnException_WhenAddingADeploymentNodeFromAnotherDeploymentEnvironment() { + void addDeploymentNode_ThrowsAnException_WhenAddingADeploymentNodeFromAnotherDeploymentEnvironment() { DeploymentNode devDeploymentNode = model.addDeploymentNode("Dev", "Deployment Node", "Description", "Technology"); devDeploymentNode.addInfrastructureNode("Load Balancer"); DeploymentNode liveDeploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); @@ -184,7 +184,7 @@ void test_addDeploymentNode_ThrowsAnException_WhenAddingADeploymentNodeFromAnoth } @Test - void test_addSoftwareSystemInstance_ThrowsAnException_WhenTheSoftwareSystemInstanceIsTheSoftwareSystemInScope() { + void addSoftwareSystemInstance_ThrowsAnException_WhenTheSoftwareSystemInstanceIsTheSoftwareSystemInScope() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); @@ -198,7 +198,7 @@ void test_addSoftwareSystemInstance_ThrowsAnException_WhenTheSoftwareSystemInsta } @Test - void test_addSoftwareSystemInstance_DoesNotAddTheSoftwareSystemInstance_WhenAChildContainerInstanceHasAlreadyBeenAdded() { + void addSoftwareSystemInstance_DoesNotAddTheSoftwareSystemInstance_WhenAChildContainerInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); @@ -217,7 +217,7 @@ void test_addSoftwareSystemInstance_DoesNotAddTheSoftwareSystemInstance_WhenAChi } @Test - void test_addContainerInstance_DoesNotAddTheContainerInstance_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { + void addContainerInstance_DoesNotAddTheContainerInstance_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); @@ -236,7 +236,7 @@ void test_addContainerInstance_DoesNotAddTheContainerInstance_WhenTheParentSoftw } @Test - void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesAreSpecified() { + void addAnimationStep_ThrowsAnException_WhenNoElementInstancesAreSpecified() { try { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.addAnimation((ContainerInstance[]) null); @@ -247,7 +247,7 @@ void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesAreSpecified( } @Test - void test_addAnimationStep_ThrowsAnException_WhenNoInfrastructureNodesAreSpecified() { + void addAnimationStep_ThrowsAnException_WhenNoInfrastructureNodesAreSpecified() { try { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.addAnimation((InfrastructureNode[]) null); @@ -258,7 +258,7 @@ void test_addAnimationStep_ThrowsAnException_WhenNoInfrastructureNodesAreSpecifi } @Test - void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesOrInfrastructureNodesAreSpecified() { + void addAnimationStep_ThrowsAnException_WhenNoElementInstancesOrInfrastructureNodesAreSpecified() { try { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.addAnimation(null, null); @@ -269,7 +269,7 @@ void test_addAnimationStep_ThrowsAnException_WhenNoElementInstancesOrInfrastruct } @Test - void test_addAnimationStep() { + void addAnimationStep() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); Container database = softwareSystem.addContainer("Database", "Description", "Technology"); @@ -303,7 +303,7 @@ void test_addAnimationStep() { } @Test - void test_addAnimationStep_IgnoresContainerInstancesThatDoNotExistInTheView() { + void addAnimationStep_IgnoresContainerInstancesThatDoNotExistInTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); Container database = softwareSystem.addContainer("Database", "Description", "Technology"); @@ -329,7 +329,7 @@ void test_addAnimationStep_IgnoresContainerInstancesThatDoNotExistInTheView() { } @Test - void test_addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpecifiedButNoneOfThemExistInTheView() { + void addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpecifiedButNoneOfThemExistInTheView() { try { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); @@ -352,7 +352,7 @@ void test_addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpecifiedB } @Test - void test_remove_RemovesTheInfrastructureNode() { + void remove_RemovesTheInfrastructureNode() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -372,7 +372,7 @@ void test_remove_RemovesTheInfrastructureNode() { } @Test - void test_remove_RemovesTheSoftwareSystemInstance() { + void remove_RemovesTheSoftwareSystemInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -391,7 +391,7 @@ void test_remove_RemovesTheSoftwareSystemInstance() { } @Test - void test_remove_RemovesTheContainerInstance() { + void remove_RemovesTheContainerInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -411,7 +411,7 @@ void test_remove_RemovesTheContainerInstance() { } @Test - void test_remove_RemovesTheDeploymentNodeAndChildren() { + void remove_RemovesTheDeploymentNodeAndChildren() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -429,7 +429,7 @@ void test_remove_RemovesTheDeploymentNodeAndChildren() { } @Test - void test_remove_RemovesTheChildDeploymentNodeAndChildren() { + void remove_RemovesTheChildDeploymentNodeAndChildren() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -446,7 +446,7 @@ void test_remove_RemovesTheChildDeploymentNodeAndChildren() { } @Test - void test_add_AddsTheInfrastructureNode() { + void add_AddsTheInfrastructureNode() { DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); InfrastructureNode infrastructureNode1 = deploymentNodeChild.addInfrastructureNode("Infrastructure Node 1"); @@ -462,7 +462,7 @@ void test_add_AddsTheInfrastructureNode() { } @Test - void test_add_AddsTheSoftwareSystemInstance() { + void add_AddsTheSoftwareSystemInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -479,7 +479,7 @@ void test_add_AddsTheSoftwareSystemInstance() { } @Test - void test_addSoftwareSystemInstance_ThrowsAnException_WhenAChildContainerInstanceHasAlreadyBeenAdded() { + void addSoftwareSystemInstance_ThrowsAnException_WhenAChildContainerInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -499,7 +499,7 @@ void test_addSoftwareSystemInstance_ThrowsAnException_WhenAChildContainerInstanc } @Test - void test_add_AddsTheContainerInstance() { + void add_AddsTheContainerInstance() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @@ -517,7 +517,7 @@ void test_add_AddsTheContainerInstance() { } @Test - void test_addContainerInstance_ThrowsAnException_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { + void addContainerInstance_ThrowsAnException_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); diff --git a/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java b/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java index 33758c0f2..da073721b 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java @@ -8,7 +8,7 @@ public class DimensionsTests { @Test - void test_construction() { + void construction() { Dimensions dimensions = new Dimensions(123, 456); assertEquals(123, dimensions.getWidth()); @@ -16,7 +16,7 @@ void test_construction() { } @Test - void test_setWidth_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void setWidth_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { Dimensions dimensions = new Dimensions(); dimensions.setWidth(-100); @@ -27,7 +27,7 @@ void test_setWidth_ThrowsAnException_WhenANegativeIntegerIsSpecified() { } @Test - void test_setHeight_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + void setHeight_ThrowsAnException_WhenANegativeIntegerIsSpecified() { try { Dimensions dimensions = new Dimensions(); dimensions.setHeight(-100); diff --git a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java index 81c98cbeb..27621a210 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java @@ -44,7 +44,7 @@ public void setup() { } @Test - void test_add_ThrowsAnException_WhenPassedANullSourceElement() { + void add_ThrowsAnException_WhenPassedANullSourceElement() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(null, softwareSystemA); @@ -55,7 +55,7 @@ void test_add_ThrowsAnException_WhenPassedANullSourceElement() { } @Test - void test_add_ThrowsAnException_WhenPassedANullDestinationElement() { + void add_ThrowsAnException_WhenPassedANullDestinationElement() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(person, null); @@ -66,7 +66,7 @@ void test_add_ThrowsAnException_WhenPassedANullDestinationElement() { } @Test - void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAContainerIsAdded() { + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAContainerIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(containerA1, containerA1); @@ -77,7 +77,7 @@ void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButACo } @Test - void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAComponentIsAdded() { + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAComponentIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); dynamicView.add(componentA1, componentA1); @@ -88,7 +88,7 @@ void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButACo } @Test - void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemButAComponentIsAdded() { + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemButAComponentIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); dynamicView.add(componentA1, containerA1); @@ -99,7 +99,7 @@ void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemBut } @Test - void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemAndTheSameSoftwareSystemIsAdded() { + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemAndTheSameSoftwareSystemIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); dynamicView.add(softwareSystemA, containerA1); @@ -110,7 +110,7 @@ void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemAnd } @Test - void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheSameContainerIsAdded() { + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheSameContainerIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); dynamicView.add(containerA1, containerA2); @@ -121,7 +121,7 @@ void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheSa } @Test - void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheParentSoftwareSystemIsAdded() { + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheParentSoftwareSystemIsAdded() { try { DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); dynamicView.add(softwareSystemA, containerA2); @@ -132,7 +132,7 @@ void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndThePa } @Test - void test_add_ThrowsAnException_WhenTheParentOfAnElementHasAlreadyBeenAdded() { + void add_ThrowsAnException_WhenTheParentOfAnElementHasAlreadyBeenAdded() { try { SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); Container container1 = softwareSystem.addContainer("Container 1", "", ""); @@ -154,7 +154,7 @@ void test_add_ThrowsAnException_WhenTheParentOfAnElementHasAlreadyBeenAdded() { } @Test - void test_add_ThrowsAnException_WhenTheChildOfAnElementHasAlreadyBeenAdded() { + void add_ThrowsAnException_WhenTheChildOfAnElementHasAlreadyBeenAdded() { try { SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); Container container1 = softwareSystem.addContainer("Container 1", "", ""); @@ -176,7 +176,7 @@ void test_add_ThrowsAnException_WhenTheChildOfAnElementHasAlreadyBeenAdded() { } @Test - void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsDoesNotExist() { + void add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsDoesNotExist() { try { DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); SoftwareSystem ss1 = workspace.getModel().addSoftwareSystem("Software System 1", ""); @@ -189,7 +189,7 @@ void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationE } @Test - void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsWithTheSpecifiedTechnologyDoesNotExist() { + void add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsWithTheSpecifiedTechnologyDoesNotExist() { try { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -209,7 +209,7 @@ void test_add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationE } @Test - void test_addRelationshipWithOriginalDescription() { + void addRelationshipWithOriginalDescription() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -226,7 +226,7 @@ void test_addRelationshipWithOriginalDescription() { } @Test - void test_addRelationshipWithOveriddenDescription() { + void addRelationshipWithOveriddenDescription() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -243,14 +243,14 @@ void test_addRelationshipWithOveriddenDescription() { } @Test - void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExists() { + void add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExists() { final DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); dynamicView.add(containerA1, containerA2); assertEquals(2, dynamicView.getElements().size()); } @Test - void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExistsAndTheDestinationIsAnExternalSoftwareSystem() { + void add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExistsAndTheDestinationIsAnExternalSoftwareSystem() { DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); containerA2.uses(softwareSystemB, "", ""); dynamicView.add(containerA2, softwareSystemB); @@ -258,7 +258,7 @@ void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemEx } @Test - void test_normalSequence() { + void normalSequence() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -280,7 +280,7 @@ void test_normalSequence() { } @Test - void test_normalSequence_WhenThereAreMultipleDescriptions() { + void normalSequence_WhenThereAreMultipleDescriptions() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -300,7 +300,7 @@ void test_normalSequence_WhenThereAreMultipleDescriptions() { } @Test - void test_normalSequence_WhenThereAreMultipleTechnologies() { + void normalSequence_WhenThereAreMultipleTechnologies() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); @@ -320,7 +320,7 @@ void test_normalSequence_WhenThereAreMultipleTechnologies() { } @Test - void test_parallelSequence() { + void parallelSequence() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", ""); @@ -359,7 +359,7 @@ void test_parallelSequence() { } @Test - void test_getRelationships_WhenTheOrderPropertyIsAnInteger() { + void getRelationships_WhenTheOrderPropertyIsAnInteger() { containerA1.uses(containerA2, "uses"); DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); for (int i = 0; i < 10; i++) { @@ -380,7 +380,7 @@ void test_getRelationships_WhenTheOrderPropertyIsAnInteger() { } @Test - void test_getRelationships_WhenTheOrderPropertyIsADecimal() { + void getRelationships_WhenTheOrderPropertyIsADecimal() { containerA1.uses(containerA2, "uses"); DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); for (int i = 0; i < 10; i++) { @@ -402,7 +402,7 @@ void test_getRelationships_WhenTheOrderPropertyIsADecimal() { } @Test - void test_getRelationships_WhenTheOrderPropertyIsAString() { + void getRelationships_WhenTheOrderPropertyIsAString() { String characters = "abcdefghij"; containerA1.uses(containerA2, "uses"); DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); @@ -425,7 +425,7 @@ void test_getRelationships_WhenTheOrderPropertyIsAString() { } @Test - void test_response() { + void response() { workspace = new Workspace("Name", "Description"); model = workspace.getModel(); diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java index 2b2f0692b..779c0b984 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java @@ -10,7 +10,7 @@ public class ElementStyleTests { @Test - void test_setOpacity() { + void setOpacity() { ElementStyle style = new ElementStyle(); assertNull(style.getOpacity()); @@ -31,7 +31,7 @@ void test_setOpacity() { } @Test - void test_opacity() { + void opacity() { ElementStyle style = new ElementStyle(); assertNull(style.getOpacity()); @@ -52,7 +52,7 @@ void test_opacity() { } @Test - void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.setColor("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -65,7 +65,7 @@ void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { } @Test - void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.color("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -78,7 +78,7 @@ void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { } @Test - void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); style.setColor("white"); @@ -86,7 +86,7 @@ void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { } @Test - void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); style.color("white"); @@ -94,7 +94,7 @@ void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { } @Test - void test_setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { + void setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.setBackground("#ffffff"); assertEquals("#ffffff", style.getBackground()); @@ -107,7 +107,7 @@ void test_setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecif } @Test - void test_background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { + void background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.background("#ffffff"); assertEquals("#ffffff", style.getBackground()); @@ -120,7 +120,7 @@ void test_background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified } @Test - void test_setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); style.setBackground("white"); @@ -128,7 +128,7 @@ void test_setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() } @Test - void test_background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); style.background("white"); @@ -136,28 +136,28 @@ void test_background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { } @Test - void test_setIcon_WithAUrl() { + void setIcon_WithAUrl() { ElementStyle style = new ElementStyle(); style.setIcon("https://structurizr.com/static/img/structurizr-logo.png"); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", style.getIcon()); } @Test - void test_setIcon_WithAUrlThatHasATrailingSpaceCharacter() { + void setIcon_WithAUrlThatHasATrailingSpaceCharacter() { ElementStyle style = new ElementStyle(); style.setIcon("https://structurizr.com/static/img/structurizr-logo.png "); assertEquals("https://structurizr.com/static/img/structurizr-logo.png", style.getIcon()); } @Test - void test_setIcon_WithADataUri() { + void setIcon_WithADataUri() { ElementStyle style = new ElementStyle(); style.setIcon("data:image/png;base64,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"); assertEquals("data:image/png;base64,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", style.getIcon()); } @Test - void test_setIcon_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + void setIcon_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); style.setIcon("htt://blah"); @@ -165,21 +165,21 @@ void test_setIcon_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() } @Test - void test_setIcon_DoesNothing_WhenANullUrlIsSpecified() { + void setIcon_DoesNothing_WhenANullUrlIsSpecified() { ElementStyle style = new ElementStyle(); style.setIcon(null); assertNull(style.getIcon()); } @Test - void test_setIcon_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void setIcon_DoesNothing_WhenAnEmptyUrlIsSpecified() { ElementStyle style = new ElementStyle(); style.setIcon(" "); assertNull(style.getIcon()); } @Test - void test_setStroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { + void setStroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.setStroke("#ffffff"); assertEquals("#ffffff", style.getStroke()); @@ -192,7 +192,7 @@ void test_setStroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { } @Test - void test_Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { + void Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { ElementStyle style = new ElementStyle(); style.stroke("#ffffff"); assertEquals("#ffffff", style.getStroke()); @@ -205,7 +205,7 @@ void test_Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { } @Test - void test_setStroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void setStroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); style.setStroke("white"); @@ -213,7 +213,7 @@ void test_setStroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { } @Test - void test_Stroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void Stroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); style.stroke("white"); @@ -221,13 +221,13 @@ void test_Stroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { } @Test - void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + void getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { ElementStyle style = new ElementStyle(); assertEquals(0, style.getProperties().size()); } @Test - void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { + void addProperty_ThrowsAnException_WhenTheNameIsNull() { try { ElementStyle style = new ElementStyle(); style.addProperty(null, "value"); @@ -238,7 +238,7 @@ void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { } @Test - void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + void addProperty_ThrowsAnException_WhenTheNameIsEmpty() { try { ElementStyle style = new ElementStyle(); style.addProperty(" ", "value"); @@ -249,7 +249,7 @@ void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { + void addProperty_ThrowsAnException_WhenTheValueIsNull() { try { ElementStyle style = new ElementStyle(); style.addProperty("name", null); @@ -260,7 +260,7 @@ void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { } @Test - void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + void addProperty_ThrowsAnException_WhenTheValueIsEmpty() { try { ElementStyle style = new ElementStyle(); style.addProperty("name", " "); @@ -271,21 +271,21 @@ void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { } @Test - void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + void addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { ElementStyle style = new ElementStyle(); style.addProperty("name", "value"); assertEquals("value", style.getProperties().get("name")); } @Test - void test_setProperties_DoesNothing_WhenNullIsSpecified() { + void setProperties_DoesNothing_WhenNullIsSpecified() { ElementStyle style = new ElementStyle(); style.setProperties(null); assertEquals(0, style.getProperties().size()); } @Test - void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { ElementStyle style = new ElementStyle(); Map properties = new HashMap<>(); properties.put("name", "value"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java index a768f19f1..497131c33 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java @@ -10,14 +10,14 @@ public class ElementViewTests extends AbstractWorkspaceTestBase { @Test - void test_copyLayoutInformationFrom_DoesNothing_WhenNullIsPassed() { + void copyLayoutInformationFrom_DoesNothing_WhenNullIsPassed() { Element element = model.addSoftwareSystem(Location.External, "SystemA", ""); ElementView elementView = new ElementView(element); elementView.copyLayoutInformationFrom(null); } @Test - void test_copyLayoutInformationFrom_CopiesXAndY_WhenANonNullElementViewIsPassed() { + void copyLayoutInformationFrom_CopiesXAndY_WhenANonNullElementViewIsPassed() { Element element = model.addSoftwareSystem(Location.External, "SystemA", ""); ElementView elementView1 = new ElementView(element); assertEquals(0, elementView1.getX()); diff --git a/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java b/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java index 47837ceb9..a21074f97 100644 --- a/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java @@ -9,7 +9,7 @@ public class FilteredViewTests extends AbstractWorkspaceTestBase { @Test - void test_construction() { + void construction() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); SystemContextView systemContextView = views.createSystemContextView(softwareSystem, "SystemContext", "Description"); FilteredView filteredView = views.createFilteredView( diff --git a/structurizr-core/test/unit/com/structurizr/view/FontTests.java b/structurizr-core/test/unit/com/structurizr/view/FontTests.java index 4839fc961..b5cee1446 100644 --- a/structurizr-core/test/unit/com/structurizr/view/FontTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/FontTests.java @@ -28,26 +28,26 @@ void construction_WithANameAndUrl() { } @Test - void test_setUrl_WithAUrl() { + void setUrl_WithAUrl() { font.setUrl("https://fonts.googleapis.com/css?family=Open+Sans:400,700"); assertEquals("https://fonts.googleapis.com/css?family=Open+Sans:400,700", font.getUrl()); } @Test - void test_setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + void setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { font.setUrl("htt://blah"); }); } @Test - void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { + void setUrl_DoesNothing_WhenANullUrlIsSpecified() { font.setUrl(null); assertNull(font.getUrl()); } @Test - void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { font.setUrl(" "); assertNull(font.getUrl()); } diff --git a/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java b/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java index 52cd80477..bf4af3932 100644 --- a/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java @@ -9,7 +9,7 @@ public class PaperSizeTests { @Test - void test_getOrderedPaperSizes_WhenOrientationIsLandscape() { + void getOrderedPaperSizes_WhenOrientationIsLandscape() { List paperSizes = PaperSize.getOrderedPaperSizes(PaperSize.Orientation.Landscape); assertEquals(12, paperSizes.size()); @@ -29,7 +29,7 @@ void test_getOrderedPaperSizes_WhenOrientationIsLandscape() { } @Test - void test_getOrderedPaperSizes_WhenOrientationIsPortrait() { + void getOrderedPaperSizes_WhenOrientationIsPortrait() { List paperSizes = PaperSize.getOrderedPaperSizes(PaperSize.Orientation.Portrait); assertEquals(9, paperSizes.size()); @@ -46,7 +46,7 @@ void test_getOrderedPaperSizes_WhenOrientationIsPortrait() { } @Test - void test_getOrderedPaperSizes() { + void getOrderedPaperSizes() { List paperSizes = PaperSize.getOrderedPaperSizes(); assertEquals(21, paperSizes.size()); diff --git a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java index bc95ba423..46e559ad3 100644 --- a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java @@ -12,25 +12,25 @@ public class RelationshipStyleTests { private RelationshipStyle relationshipStyle = new RelationshipStyle("tag"); @Test - void test_setPosition_SetsPositionToNull_WhenNullIsSpecified() { + void setPosition_SetsPositionToNull_WhenNullIsSpecified() { relationshipStyle.setPosition(null); assertNull(relationshipStyle.getPosition()); } @Test - void test_setPosition_SetsPositionToZero_WhenANegativeNumberIsSpecified() { + void setPosition_SetsPositionToZero_WhenANegativeNumberIsSpecified() { relationshipStyle.setPosition(-1); assertEquals(Integer.valueOf(0), relationshipStyle.getPosition()); } @Test - void test_setPosition_SetsPositionToOneHundred_WhenANumberGreaterThanOneHundredIsSpecified() { + void setPosition_SetsPositionToOneHundred_WhenANumberGreaterThanOneHundredIsSpecified() { relationshipStyle.setPosition(101); assertEquals(Integer.valueOf(100), relationshipStyle.getPosition()); } @Test - void test_setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsSpecified() { + void setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsSpecified() { relationshipStyle.setPosition(0); assertEquals(Integer.valueOf(0), relationshipStyle.getPosition()); @@ -49,7 +49,7 @@ void test_setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsSpecifie } @Test - void test_setOpacity() { + void setOpacity() { RelationshipStyle style = new RelationshipStyle(); assertNull(style.getOpacity()); @@ -70,7 +70,7 @@ void test_setOpacity() { } @Test - void test_opacity() { + void opacity() { RelationshipStyle style = new RelationshipStyle(); assertNull(style.getOpacity()); @@ -91,7 +91,7 @@ void test_opacity() { } @Test - void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { RelationshipStyle style = new RelationshipStyle(); style.setColor("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -104,7 +104,7 @@ void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { } @Test - void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + void color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { RelationshipStyle style = new RelationshipStyle(); style.color("#ffffff"); assertEquals("#ffffff", style.getColor()); @@ -117,7 +117,7 @@ void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { } @Test - void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { RelationshipStyle style = new RelationshipStyle(); style.setColor("white"); @@ -125,7 +125,7 @@ void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { } @Test - void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { RelationshipStyle style = new RelationshipStyle(); style.color("white"); @@ -133,13 +133,13 @@ void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { } @Test - void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + void getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { RelationshipStyle style = new RelationshipStyle(); assertEquals(0, style.getProperties().size()); } @Test - void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { + void addProperty_ThrowsAnException_WhenTheNameIsNull() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty(null, "value"); @@ -150,7 +150,7 @@ void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { } @Test - void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + void addProperty_ThrowsAnException_WhenTheNameIsEmpty() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty(" ", "value"); @@ -161,7 +161,7 @@ void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { } @Test - void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { + void addProperty_ThrowsAnException_WhenTheValueIsNull() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty("name", null); @@ -172,7 +172,7 @@ void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { } @Test - void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + void addProperty_ThrowsAnException_WhenTheValueIsEmpty() { try { RelationshipStyle style = new RelationshipStyle(); style.addProperty("name", " "); @@ -183,21 +183,21 @@ void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { } @Test - void test_addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + void addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { RelationshipStyle style = new RelationshipStyle(); style.addProperty("name", "value"); assertEquals("value", style.getProperties().get("name")); } @Test - void test_setProperties_DoesNothing_WhenNullIsSpecified() { + void setProperties_DoesNothing_WhenNullIsSpecified() { RelationshipStyle style = new RelationshipStyle(); style.setProperties(null); assertEquals(0, style.getProperties().size()); } @Test - void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { RelationshipStyle style = new RelationshipStyle(); Map properties = new HashMap<>(); properties.put("name", "value"); diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java b/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java index 1b77117a8..3b8242421 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java @@ -7,7 +7,7 @@ public class SequenceCounterTests { @Test - void test_increment_IncrementsTheCounter_WhenThereIsNoParent() { + void increment_IncrementsTheCounter_WhenThereIsNoParent() { SequenceCounter counter = new SequenceCounter(); assertEquals("0", counter.toString()); diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java b/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java index bb5404cc2..ed1eff62f 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java @@ -7,14 +7,14 @@ public class SequenceNumberTests { @Test - void test_increment() { + void increment() { SequenceNumber sequenceNumber = new SequenceNumber(); assertEquals("1", sequenceNumber.getNext()); assertEquals("2", sequenceNumber.getNext()); } @Test - void test_parallelSequences() { + void parallelSequences() { SequenceNumber sequenceNumber = new SequenceNumber(); assertEquals("1", sequenceNumber.getNext()); diff --git a/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java b/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java index c23526029..722bd5c39 100644 --- a/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java @@ -10,7 +10,7 @@ public class StaticViewTests extends AbstractWorkspaceTestBase { @Test - void test_addAnimationStep_ThrowsAnException_WhenNoElementsAreSpecified() { + void addAnimationStep_ThrowsAnException_WhenNoElementsAreSpecified() { try { SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); view.addAnimation(); @@ -21,7 +21,7 @@ void test_addAnimationStep_ThrowsAnException_WhenNoElementsAreSpecified() { } @Test - void test_addAnimationStep() { + void addAnimationStep() { SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); SoftwareSystem element2 = model.addSoftwareSystem("Software System 2", ""); SoftwareSystem element3 = model.addSoftwareSystem("Software System 3", ""); @@ -55,7 +55,7 @@ void test_addAnimationStep() { } @Test - void test_addAnimationStep_IgnoresElementsThatDoNotExistInTheView() { + void addAnimationStep_IgnoresElementsThatDoNotExistInTheView() { SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); SoftwareSystem element2 = model.addSoftwareSystem("Software System 2", ""); @@ -69,7 +69,7 @@ void test_addAnimationStep_IgnoresElementsThatDoNotExistInTheView() { } @Test - void test_addAnimationStep_ThrowsAnException_WhenElementsAreSpecifiedButNoneOfThemExistInTheView() { + void addAnimationStep_ThrowsAnException_WhenElementsAreSpecifiedButNoneOfThemExistInTheView() { try { SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); diff --git a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java index 99182c7a3..0a663da2a 100644 --- a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java @@ -11,7 +11,7 @@ public class StylesTests extends AbstractWorkspaceTestBase { private Styles styles = new Styles(); @Test - void test_findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { + void findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { ElementStyle style = styles.findElementStyle((Element) null); assertEquals(Integer.valueOf(450), style.getWidth()); assertEquals(Integer.valueOf(300), style.getHeight()); @@ -28,7 +28,7 @@ void test_findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { } @Test - void test_findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { + void findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); ElementStyle style = styles.findElementStyle(element); assertEquals(Integer.valueOf(450), style.getWidth()); @@ -46,7 +46,7 @@ void test_findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { } @Test - void test_findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { + void findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); element.addTags("Some Tag"); @@ -69,7 +69,7 @@ void test_findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { } @Test - void test_findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDefined() { + void findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDefined() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name"); softwareSystem.addTags("Some Tag"); @@ -95,7 +95,7 @@ void test_findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStyles } @Test - void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABox() { + void findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABox() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); element.addTags("Some Tag"); @@ -109,7 +109,7 @@ void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABox() { } @Test - void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsAPerson() { + void findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsAPerson() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); element.addTags("Some Tag"); @@ -123,7 +123,7 @@ void test_findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsAPerson() } @Test - void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { + void findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { RelationshipStyle style = styles.findRelationshipStyle((Relationship) null); assertEquals(Integer.valueOf(2), style.getThickness()); assertEquals("#707070", style.getColor()); @@ -136,7 +136,7 @@ void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { } @Test - void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { + void findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); Relationship relationship = element.uses(element, "Uses"); RelationshipStyle style = styles.findRelationshipStyle(relationship); @@ -151,7 +151,7 @@ void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() } @Test - void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { + void findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); Relationship relationship = element.uses(element, "Uses"); relationship.addTags("Some Tag"); @@ -171,7 +171,7 @@ void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { } @Test - void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationship() { + void findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationship() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); Container container1 = softwareSystem.addContainer("Container 1", "Description", "Technology"); Container container2 = softwareSystem.addContainer("Container 2", "Description", "Technology"); @@ -194,7 +194,7 @@ void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelatio } @Test - void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationshipBasedUponAnImpliedRelationship() { + void findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationshipBasedUponAnImpliedRelationship() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); Container container1 = softwareSystem.addContainer("Container 1"); @@ -220,7 +220,7 @@ void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelatio } @Test - void test_addElementStyle_ThrowsAnException_WhenATagIsNotSpecified() { + void addElementStyle_ThrowsAnException_WhenATagIsNotSpecified() { try { styles.addElementStyle(""); fail(); @@ -244,7 +244,7 @@ void test_addElementStyle_ThrowsAnException_WhenATagIsNotSpecified() { } @Test - void test_addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); @@ -256,7 +256,7 @@ void test_addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsA } @Test - void test_addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { ElementStyle style = styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); styles.add(style); @@ -268,7 +268,7 @@ void test_addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlread } @Test - void test_addRelationshipStyle_ThrowsAnException_WhenATagIsNotSpecified() { + void addRelationshipStyle_ThrowsAnException_WhenATagIsNotSpecified() { try { styles.addRelationshipStyle(""); fail(); @@ -292,7 +292,7 @@ void test_addRelationshipStyle_ThrowsAnException_WhenATagIsNotSpecified() { } @Test - void test_addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); @@ -304,7 +304,7 @@ void test_addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagEx } @Test - void test_addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + void addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { RelationshipStyle style = styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); styles.add(style); @@ -316,7 +316,7 @@ void test_addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsA } @Test - void test_clearElementStyles_RemovesAllElementStyles() { + void clearElementStyles_RemovesAllElementStyles() { styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); assertEquals(1, styles.getElements().size()); @@ -325,7 +325,7 @@ void test_clearElementStyles_RemovesAllElementStyles() { } @Test - void test_clearRelationshipStyles_RemovesAllRelationshipStyles() { + void clearRelationshipStyles_RemovesAllRelationshipStyles() { styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); assertEquals(1, styles.getRelationships().size()); diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java index dc37a0cf1..2c68a8e72 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java @@ -19,7 +19,7 @@ public void setUp() { } @Test - void test_construction() { + void construction() { assertEquals("The System - System Context", view.getName()); assertEquals(1, view.getElements().size()); assertSame(view.getElements().iterator().next().getElement(), softwareSystem); @@ -29,14 +29,14 @@ void test_construction() { } @Test - void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { assertEquals(1, view.getElements().size()); view.addAllSoftwareSystems(); assertEquals(1, view.getElements().size()); } @Test - void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -49,14 +49,14 @@ void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareS } @Test - void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { assertEquals(1, view.getElements().size()); view.addAllPeople(); assertEquals(1, view.getElements().size()); } @Test - void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson(Location.External, "User A", "Description"); Person userB = model.addPerson(Location.External, "User B", "Description"); @@ -69,14 +69,14 @@ void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { assertEquals(1, view.getElements().size()); view.addAllElements(); assertEquals(1, view.getElements().size()); } @Test - void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { + void addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); Person userA = model.addPerson(Location.External, "User A", "Description"); @@ -93,7 +93,7 @@ void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwar } @Test - void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { + void addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { try { view.addNearestNeighbours(null); fail(); @@ -103,7 +103,7 @@ void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { } @Test - void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { + void addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { Container container = softwareSystem.addContainer("Container", "Description", "Technology"); try { view.addNearestNeighbours(container); @@ -114,14 +114,14 @@ void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOr } @Test - void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { view.addNearestNeighbours(softwareSystem); assertEquals(1, view.getElements().size()); } @Test - void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", "Description"); Person userA = model.addPerson("User A", "Description"); @@ -170,7 +170,7 @@ void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeig } @Test - void test_removeSoftwareSystem_ThrowsAnException_WhenPassedNull() { + void removeSoftwareSystem_ThrowsAnException_WhenPassedNull() { try { view.remove((SoftwareSystem) null); fail(); @@ -180,7 +180,7 @@ void test_removeSoftwareSystem_ThrowsAnException_WhenPassedNull() { } @Test - void test_removeSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTheView() { + void removeSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTheView() { SoftwareSystem anotherSoftwareSystem = model.addSoftwareSystem("Another software system", ""); assertEquals(1, view.getElements().size()); @@ -189,7 +189,7 @@ void test_removeSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTheView() } @Test - void test_removeSoftwareSystem_DoesNotRemoveTheSoftwareSystemInFocus() { + void removeSoftwareSystem_DoesNotRemoveTheSoftwareSystemInFocus() { try { view.remove(softwareSystem); fail(); @@ -199,7 +199,7 @@ void test_removeSoftwareSystem_DoesNotRemoveTheSoftwareSystemInFocus() { } @Test - void test_removeSoftwareSystem_RemovesTheSoftwareSystemAndRelationshipsFromTheView() { + void removeSoftwareSystem_RemovesTheSoftwareSystemAndRelationshipsFromTheView() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software system 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software system 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -215,7 +215,7 @@ void test_removeSoftwareSystem_RemovesTheSoftwareSystemAndRelationshipsFromTheVi } @Test - void test_removePerson_ThrowsAnException_WhenPassedNull() { + void removePerson_ThrowsAnException_WhenPassedNull() { try { view.remove((Person) null); fail(); @@ -225,7 +225,7 @@ void test_removePerson_ThrowsAnException_WhenPassedNull() { } @Test - void test_removePerson_DoesNothing_WhenThePersonIsNotInTheView() { + void removePerson_DoesNothing_WhenThePersonIsNotInTheView() { Person person = model.addPerson("Person", ""); assertEquals(1, view.getElements().size()); @@ -234,7 +234,7 @@ void test_removePerson_DoesNothing_WhenThePersonIsNotInTheView() { } @Test - void test_removePerson_RemovesThePersonAndRelationshipsFromTheView() { + void removePerson_RemovesThePersonAndRelationshipsFromTheView() { Person person = model.addPerson("Person", ""); person.uses(softwareSystem, "uses"); softwareSystem.delivers(person, "delivers something to"); @@ -249,7 +249,7 @@ void test_removePerson_RemovesThePersonAndRelationshipsFromTheView() { } @Test - void test_addSoftwareSystemWithoutRelationships_DoesNotAddRelationships() { + void addSoftwareSystemWithoutRelationships_DoesNotAddRelationships() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software system 1", ""); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software system 2", ""); softwareSystem1.uses(softwareSystem2, "uses"); @@ -261,7 +261,7 @@ void test_addSoftwareSystemWithoutRelationships_DoesNotAddRelationships() { } @Test - void test_addPersonWithoutRelationships_DoesNotAddRelationships() { + void addPersonWithoutRelationships_DoesNotAddRelationships() { Person user = model.addPerson("User", ""); SoftwareSystem softwareSystem = model.addSoftwareSystem("Software system 2", ""); user.uses(softwareSystem, "uses"); @@ -273,7 +273,7 @@ void test_addPersonWithoutRelationships_DoesNotAddRelationships() { } @Test - void test_isEnterpriseBoundaryVisible() { + void isEnterpriseBoundaryVisible() { assertTrue(view.isEnterpriseBoundaryVisible()); // default is true view.setEnterpriseBoundaryVisible(false); @@ -281,7 +281,7 @@ void test_isEnterpriseBoundaryVisible() { } @Test - void test_addDefaultElements() { + void addDefaultElements() { CustomElement element = model.addCustomElement("Custom"); Person user1 = model.addPerson("User 1"); Person user2 = model.addPerson("User 2"); diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java b/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java index 9e0838836..69b922957 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java @@ -17,45 +17,45 @@ public void setUp() { } @Test - void test_construction() { + void construction() { assertEquals("System Landscape", view.getName()); assertEquals(0, view.getElements().size()); assertSame(model, view.getModel()); } @Test - void test_getName_WhenNoEnterpriseIsSpecified() { + void getName_WhenNoEnterpriseIsSpecified() { assertEquals("System Landscape", view.getName()); } @Test - void test_getName_WhenAnEnterpriseIsSpecified() { + void getName_WhenAnEnterpriseIsSpecified() { model.setEnterprise(new Enterprise("Widgets Limited")); assertEquals("System Landscape for Widgets Limited", view.getName()); } @Test - void test_getName_WhenAnEmptyEnterpriseNameIsSpecified() { + void getName_WhenAnEmptyEnterpriseNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { model.setEnterprise(new Enterprise("")); }); } @Test - void test_getName_WhenANullEnterpriseNameIsSpecified() { + void getName_WhenANullEnterpriseNameIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { model.setEnterprise(new Enterprise(null)); }); } @Test - void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { view.addAllSoftwareSystems(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); @@ -67,13 +67,13 @@ void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareS } @Test - void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { view.addAllPeople(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { Person userA = model.addPerson("User A", "Description"); Person userB = model.addPerson("User B", "Description"); @@ -85,13 +85,13 @@ void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { } @Test - void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { view.addAllElements(); assertEquals(0, view.getElements().size()); } @Test - void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { + void addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Person person = model.addPerson("Person", "Description"); @@ -103,7 +103,7 @@ void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwar } @Test - void test_isEnterpriseBoundaryVisible() { + void isEnterpriseBoundaryVisible() { assertTrue(view.isEnterpriseBoundaryVisible()); // default is true view.setEnterpriseBoundaryVisible(false); @@ -111,7 +111,7 @@ void test_isEnterpriseBoundaryVisible() { } @Test - void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { + void addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { try { view.addNearestNeighbours(null); fail(); @@ -121,7 +121,7 @@ void test_addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { } @Test - void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { + void addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); try { @@ -133,7 +133,7 @@ void test_addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOr } @Test - void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); view.addNearestNeighbours(softwareSystem); @@ -141,7 +141,7 @@ void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { } @Test - void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + void addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); @@ -191,7 +191,7 @@ void test_addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeig } @Test - void test_addDefaultElements() { + void addDefaultElements() { CustomElement element = model.addCustomElement("Custom"); Person user = model.addPerson("User"); SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); diff --git a/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java b/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java index 13398c7f8..7f19c8959 100644 --- a/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java @@ -9,7 +9,7 @@ public class TerminologyTests { @Test - void test_findTerminology() { + void findTerminology() { Workspace workspace = new Workspace("Name", "Description"); Terminology terminology = workspace.getViews().getConfiguration().getTerminology(); Person person = workspace.getModel().addPerson("Name"); diff --git a/structurizr-core/test/unit/com/structurizr/view/VertexTests.java b/structurizr-core/test/unit/com/structurizr/view/VertexTests.java index 94e81fec0..f9377d309 100644 --- a/structurizr-core/test/unit/com/structurizr/view/VertexTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/VertexTests.java @@ -8,7 +8,7 @@ public class VertexTests { @Test - void test_equals() { + void equals() { Vertex vertex1 = new Vertex(123, 456); Vertex vertex2 = new Vertex(123, 456); Vertex vertex3 = new Vertex(456, 123); diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java b/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java index b3741b231..d490ae9fe 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java @@ -28,7 +28,7 @@ private Workspace createWorkspace() { } @Test - void test_createCustomView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createCustomView_ThrowsAnException_WhenANullKeyIsSpecified() { try { new Workspace("", "").getViews().createCustomView(null, "Title", "Description"); fail(); @@ -38,7 +38,7 @@ void test_createCustomView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_createCustomView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createCustomView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { new Workspace("", "").getViews().createCustomView(" ", "Title", "Description"); fail(); @@ -48,7 +48,7 @@ void test_createCustomView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { } @Test - void test_createCustomView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void createCustomView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createCustomView("key", "Title", "Description"); @@ -60,14 +60,14 @@ void test_createCustomView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { } @Test - void test_createCustomView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { + void createCustomView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createCustomView("key1", "Title", "Description"); workspace.getViews().createCustomView("key2", "Title", "Description"); } @Test - void test_createCustomView() { + void createCustomView() { Workspace workspace = new Workspace("Name", "Description"); CustomView customView = workspace.getViews().createCustomView("key", "Title", "Description"); assertEquals("key", customView.getKey()); @@ -78,7 +78,7 @@ void test_createCustomView() { } @Test - void test_createSystemLandscapeView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createSystemLandscapeView_ThrowsAnException_WhenANullKeyIsSpecified() { try { new Workspace("", "").getViews().createSystemLandscapeView(null, "Description"); fail(); @@ -88,7 +88,7 @@ void test_createSystemLandscapeView_ThrowsAnException_WhenANullKeyIsSpecified() } @Test - void test_createSystemLandscapeView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createSystemLandscapeView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { new Workspace("", "").getViews().createSystemLandscapeView(" ", "Description"); fail(); @@ -98,7 +98,7 @@ void test_createSystemLandscapeView_ThrowsAnException_WhenAnEmptyKeyIsSpecified( } @Test - void test_createSystemLandscapeView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void createSystemLandscapeView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key", "Description"); @@ -110,14 +110,14 @@ void test_createSystemLandscapeView_ThrowsAnException_WhenADuplicateKeyIsSpecifi } @Test - void test_createSystemLandscapeView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { + void createSystemLandscapeView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createSystemLandscapeView("key1", "Description"); workspace.getViews().createSystemLandscapeView("key2", "Description"); } @Test - void test_createSystemLandscapeView() { + void createSystemLandscapeView() { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView systemLandscapeView = workspace.getViews().createSystemLandscapeView("key", "Description"); assertEquals("key", systemLandscapeView.getKey()); @@ -125,7 +125,7 @@ void test_createSystemLandscapeView() { } @Test - void test_createSystemContextView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + void createSystemContextView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createSystemContextView(null, null, "Description"); fail(); @@ -135,7 +135,7 @@ void test_createSystemContextView_ThrowsAnException_WhenASoftwareSystemIsSpecifi } @Test - void test_createSystemContextView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createSystemContextView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -147,7 +147,7 @@ void test_createSystemContextView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_createSystemContextView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createSystemContextView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -159,7 +159,7 @@ void test_createSystemContextView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() } @Test - void test_createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -172,7 +172,7 @@ void test_createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsSpecified } @Test - void test_createSystemContextView() { + void createSystemContextView() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "key", "Description"); @@ -182,7 +182,7 @@ void test_createSystemContextView() { } @Test - void test_createContainerView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + void createContainerView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createContainerView(null, null, "Description"); fail(); @@ -192,7 +192,7 @@ void test_createContainerView_ThrowsAnException_WhenASoftwareSystemIsSpecified() } @Test - void test_createContainerView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createContainerView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -204,7 +204,7 @@ void test_createContainerView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_createContainerView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createContainerView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -216,7 +216,7 @@ void test_createContainerView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { } @Test - void test_createContainerView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void createContainerView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -229,7 +229,7 @@ void test_createContainerView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { } @Test - void test_createContainerView() { + void createContainerView() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); ContainerView containerView = workspace.getViews().createContainerView(softwareSystem, "key", "Description"); @@ -239,7 +239,7 @@ void test_createContainerView() { } @Test - void test_createComponentView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + void createComponentView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createComponentView(null, null, "Description"); fail(); @@ -249,7 +249,7 @@ void test_createComponentView_ThrowsAnException_WhenASoftwareSystemIsSpecified() } @Test - void test_createComponentView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createComponentView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -262,7 +262,7 @@ void test_createComponentView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_createComponentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createComponentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -275,7 +275,7 @@ void test_createComponentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { } @Test - void test_createComponentView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + void createComponentView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -289,7 +289,7 @@ void test_createComponentView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { } @Test - void test_createComponentView() { + void createComponentView() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); @@ -301,7 +301,7 @@ void test_createComponentView() { } @Test - void test_createDynamicView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createDynamicView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDynamicView(null, "Description"); @@ -312,7 +312,7 @@ void test_createDynamicView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_createDynamicView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createDynamicView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDynamicView(" ", "Description"); @@ -323,7 +323,7 @@ void test_createDynamicView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { } @Test - void test_createDynamicView() { + void createDynamicView() { Workspace workspace = new Workspace("Name", "Description"); DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); @@ -334,7 +334,7 @@ void test_createDynamicView() { } @Test - void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + void createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createDynamicView((SoftwareSystem) null, "key", "Description"); fail(); @@ -344,7 +344,7 @@ void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwar } @Test - void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { + void createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -356,7 +356,7 @@ void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSp } @Test - void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createDynamicViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -368,7 +368,7 @@ void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIs } @Test - void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void createDynamicViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -382,7 +382,7 @@ void test_createDynamicViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKe } @Test - void test_createDynamicViewForSoftwareSystem() { + void createDynamicViewForSoftwareSystem() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -394,7 +394,7 @@ void test_createDynamicViewForSoftwareSystem() { } @Test - void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullContainerIsSpecified() { + void createDynamicViewForAContainer_ThrowsAnException_WhenANullContainerIsSpecified() { try { new Workspace("", "").getViews().createDynamicView((Container) null, "key", "Description"); fail(); @@ -404,7 +404,7 @@ void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullContainerIsS } @Test - void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullKeyIsSpecified() { + void createDynamicViewForAContainer_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -417,7 +417,7 @@ void test_createDynamicViewForAContainer_ThrowsAnException_WhenANullKeyIsSpecifi } @Test - void test_createDynamicViewForAContainer_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createDynamicViewForAContainer_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -430,7 +430,7 @@ void test_createDynamicViewForAContainer_ThrowsAnException_WhenAnEmptyKeyIsSpeci } @Test - void test_createDynamicViewForContainer() { + void createDynamicViewForContainer() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); @@ -443,7 +443,7 @@ void test_createDynamicViewForContainer() { } @Test - void test_createDeploymentView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createDeploymentView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDeploymentView(null, "Description"); @@ -454,7 +454,7 @@ void test_createDeploymentView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_createDeploymentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createDeploymentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createDeploymentView(" ", "Description"); @@ -465,7 +465,7 @@ void test_createDeploymentView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { } @Test - void test_createDeploymentView_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void createDeploymentView_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -479,7 +479,7 @@ void test_createDeploymentView_ThrowsAnException_WhenADuplicateKeyIsUsed() { } @Test - void test_createDeploymentView() { + void createDeploymentView() { Workspace workspace = new Workspace("Name", "Description"); DeploymentView deploymentView = workspace.getViews().createDeploymentView("key", "Description"); @@ -489,7 +489,7 @@ void test_createDeploymentView() { } @Test - void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + void createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { try { new Workspace("", "").getViews().createDeploymentView((SoftwareSystem) null, "key", "Description"); fail(); @@ -499,7 +499,7 @@ void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullSoft } @Test - void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { + void createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -511,7 +511,7 @@ void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullKeyI } @Test - void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -523,7 +523,7 @@ void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenAnEmptyKe } @Test - void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -537,7 +537,7 @@ void test_createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenADuplicat } @Test - void test_createDeploymentViewForSoftwareSystem() { + void createDeploymentViewForSoftwareSystem() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); @@ -548,7 +548,7 @@ void test_createDeploymentViewForSoftwareSystem() { } @Test - void test_createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() { + void createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); workspace.getViews().createFilteredView(null, "key", "Description", FilterMode.Include, "tag1", "tag2"); @@ -559,7 +559,7 @@ void test_createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() { } @Test - void test_createFilteredView_ThrowsAnException_WhenANullKeyIsSpecified() { + void createFilteredView_ThrowsAnException_WhenANullKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); @@ -571,7 +571,7 @@ void test_createFilteredView_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_createFilteredView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void createFilteredView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); @@ -583,7 +583,7 @@ void test_createFilteredView_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { } @Test - void test_createFilteredView_ThrowsAnException_WhenADuplicateKeyIsUsed() { + void createFilteredView_ThrowsAnException_WhenADuplicateKeyIsUsed() { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); workspace.getViews().createFilteredView(view, "filtered", "Description", FilterMode.Include, "tag1", "tag2"); @@ -596,7 +596,7 @@ void test_createFilteredView_ThrowsAnException_WhenADuplicateKeyIsUsed() { } @Test - void test_createFilteredView() { + void createFilteredView() { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); FilteredView filteredView = workspace.getViews().createFilteredView(view, "key", "Description", FilterMode.Include, "tag1", "tag2"); @@ -610,7 +610,7 @@ void test_createFilteredView() { } @Test - void test_copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesMatch() { + void copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -631,7 +631,7 @@ void test_copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesMatch() } @Test - void test_copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { + void copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -651,7 +651,7 @@ void test_copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { } @Test - void test_copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeIsSet() { + void copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeIsSet() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -667,7 +667,7 @@ void test_copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeIsSet() } @Test - void test_copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIsNotSet() { + void copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIsNotSet() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -682,7 +682,7 @@ void test_copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIsNotSet( } @Test - void test_copyLayoutInformationFrom_WhenTheSystemLandscapeViewKeysMatch() { + void copyLayoutInformationFrom_WhenTheSystemLandscapeViewKeysMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("landscape", "Description"); @@ -701,7 +701,7 @@ void test_copyLayoutInformationFrom_WhenTheSystemLandscapeViewKeysMatch() { } @Test - void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemLandscapeViewToCopyInformationFrom() { + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemLandscapeViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -715,7 +715,7 @@ void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemLandscapeV } @Test - void test_copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { + void copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); @@ -734,7 +734,7 @@ void test_copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { } @Test - void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemContextViewToCopyInformationFrom() { + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemContextViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -748,7 +748,7 @@ void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemContextVie } @Test - void test_copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { + void copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "containers", "Description"); @@ -767,7 +767,7 @@ void test_copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { } @Test - void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainerViewToCopyInformationFrom() { + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainerViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -781,7 +781,7 @@ void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainerViewToC } @Test - void test_copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { + void copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { Workspace workspace1 = createWorkspace(); Container container1 = workspace1.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); ComponentView view1 = workspace1.getViews().createComponentView(container1, "containers", "Description"); @@ -800,7 +800,7 @@ void test_copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { } @Test - void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponentViewToCopyInformationFrom() { + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponentViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -814,7 +814,7 @@ void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponentViewToC } @Test - void test_copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { + void copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { Workspace workspace1 = createWorkspace(); Person person1 = workspace1.getModel().getPersonWithName("Person"); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); @@ -835,7 +835,7 @@ void test_copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { } @Test - void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicViewToCopyInformationFrom() { + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -850,7 +850,7 @@ void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicViewToCop } @Test - void test_copyLayoutInformationFrom_WhenTheDeploymentViewKeysMatch() { + void copyLayoutInformationFrom_WhenTheDeploymentViewKeysMatch() { Workspace workspace1 = createWorkspace(); DeploymentNode deploymentNode1 = workspace1.getModel().getDeploymentNodeWithName("Deployment Node"); DeploymentView view1 = workspace1.getViews().createDeploymentView("key", "Description"); @@ -871,7 +871,7 @@ void test_copyLayoutInformationFrom_WhenTheDeploymentViewKeysMatch() { } @Test - void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDeploymentViewToCopyInformationFrom() { + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDeploymentViewToCopyInformationFrom() { Workspace workspace1 = createWorkspace(); Workspace workspace2 = createWorkspace(); @@ -910,7 +910,7 @@ private HashSet relationshipViewsFor(Relationship... relations } @Test - void test_hydrate() { + void hydrate() { Workspace workspace = new Workspace("Name", "Description"); Model model = workspace.getModel(); ViewSet views = workspace.getViews(); @@ -1004,7 +1004,7 @@ void test_hydrate() { } @Test - void test_setEnterpriseContextViews_IsSupportedForOlderWorkspaces() { + void setEnterpriseContextViews_IsSupportedForOlderWorkspaces() { ViewSet views = new Workspace("", "").getViews(); SystemLandscapeView systemLandscapeView = views.createSystemLandscapeView("key", "Description"); views.setEnterpriseContextViews(Collections.singleton(systemLandscapeView)); @@ -1013,7 +1013,7 @@ void test_setEnterpriseContextViews_IsSupportedForOlderWorkspaces() { } @Test - void test_createDefaultViews() { + void createDefaultViews() { Workspace workspace = new Workspace("Name", "Description"); Model model = workspace.getModel(); ViewSet views = workspace.getViews(); @@ -1075,7 +1075,7 @@ void test_createDefaultViews() { } @Test - void test_copyLayoutInformationFrom_DoesNothing_WhenMergeFromRemoteIsSetToFalse() { + void copyLayoutInformationFrom_DoesNothing_WhenMergeFromRemoteIsSetToFalse() { Workspace workspace1 = createWorkspace(); SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("landscape", "Description"); @@ -1095,7 +1095,7 @@ void test_copyLayoutInformationFrom_DoesNothing_WhenMergeFromRemoteIsSetToFalse( } @Test - void test_view_ordering() { + void view_ordering() { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); Container container = softwareSystem.addContainer("Container"); diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ViewTests.java index 8b2fde869..8017e46db 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ViewTests.java @@ -13,7 +13,7 @@ public class ViewTests extends AbstractWorkspaceTestBase { @Test - void test_construction() { + void construction() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "key", "Description"); assertEquals("key", view.getKey()); @@ -22,14 +22,14 @@ void test_construction() { } @Test - void test_construction_WhenTheViewKeyContainsAForwardSlashCharacter() { + void construction_WhenTheViewKeyContainsAForwardSlashCharacter() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); StaticView view = new SystemContextView(softwareSystem, "key/1", "Description"); assertEquals("key_1", view.getKey()); } @Test - void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsInTheModel() { + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); assertEquals(1, view.getElements().size()); @@ -38,7 +38,7 @@ void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsIn } @Test - void test_addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSystemsInTheModel() { + void addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSystemsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.Unspecified, "System B", "Description"); @@ -56,7 +56,7 @@ void test_addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSy } @Test - void test_addSoftwareSystem_ThrowsAnException_WhenGivenNull() { + void addSoftwareSystem_ThrowsAnException_WhenGivenNull() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); @@ -69,7 +69,7 @@ void test_addSoftwareSystem_ThrowsAnException_WhenGivenNull() { } @Test - void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() { + void addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); @@ -82,7 +82,7 @@ void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheMo } @Test - void test_addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { + void addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); @@ -93,7 +93,7 @@ void test_addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { } @Test - void test_addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { + void addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); Person person2 = model.addPerson(Location.Unspecified, "Person 2", "Description"); @@ -111,7 +111,7 @@ void test_addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { } @Test - void test_addPerson_ThrowsAnException_WhenGivenNull() { + void addPerson_ThrowsAnException_WhenGivenNull() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); try { @@ -123,7 +123,7 @@ void test_addPerson_ThrowsAnException_WhenGivenNull() { } @Test - void test_addPerson_AddsThePerson_WhenThPersonIsInTheModel() { + void addPerson_AddsThePerson_WhenThPersonIsInTheModel() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); @@ -137,7 +137,7 @@ void test_addPerson_AddsThePerson_WhenThPersonIsInTheModel() { } @Test - void test_removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelationshipsBetweenElements() { + void removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelationshipsBetweenElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "Software System", "Description"); Person person = model.addPerson(Location.Unspecified, "Person", "Description"); @@ -150,7 +150,7 @@ void test_removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoR } @Test - void test_removeElementsWithNoRelationships_RemovesOnlyThoseElementsWithoutRelationships_WhenTheViewContainsSomeUnlinkedElements() { + void removeElementsWithNoRelationships_RemovesOnlyThoseElementsWithoutRelationships_WhenTheViewContainsSomeUnlinkedElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.Unspecified, "System B", "Description"); @@ -170,7 +170,7 @@ void test_removeElementsWithNoRelationships_RemovesOnlyThoseElementsWithoutRelat } @Test - void test_copyLayoutInformationFrom() { + void copyLayoutInformationFrom() { Workspace workspace1 = new Workspace("", ""); Model model1 = workspace1.getModel(); SoftwareSystem softwareSystem1A = model1.addSoftwareSystem("System A", "Description"); @@ -256,21 +256,21 @@ void test_copyLayoutInformationFrom() { } @Test - void test_getName() { + void getName() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); SystemContextView systemContextView = new SystemContextView(softwareSystem, "context", "Description"); assertEquals("The System - System Context", systemContextView.getName()); } @Test - void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenANullElementIsSpecified() { + void removeElementsThatAreUnreachableFrom_DoesNothing_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); view.removeElementsThatAreUnreachableFrom(null); } @Test - void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenAllElementsCanBeReached() { + void removeElementsThatAreUnreachableFrom_DoesNothing_WhenAllElementsCanBeReached() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); @@ -287,7 +287,7 @@ void test_removeElementsThatAreUnreachableFrom_DoesNothing_WhenAllElementsCanBeR } @Test - void test_removeElementsThatAreUnreachableFrom_RemovesOrphanedElements_WhenThereAreSomeOrphanedElements() { + void removeElementsThatAreUnreachableFrom_RemovesOrphanedElements_WhenThereAreSomeOrphanedElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); @@ -306,7 +306,7 @@ void test_removeElementsThatAreUnreachableFrom_RemovesOrphanedElements_WhenThere } @Test - void test_removeElementsThatAreUnreachableFrom_RemovesUnreachableElements_WhenThereAreSomeUnreachableElements() { + void removeElementsThatAreUnreachableFrom_RemovesUnreachableElements_WhenThereAreSomeUnreachableElements() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); @@ -324,7 +324,7 @@ void test_removeElementsThatAreUnreachableFrom_RemovesUnreachableElements_WhenTh } @Test - void test_removeElementsThatAreUnreachableFrom_DoesntIncludeAllElements_WhenThereIsACyclicGraph() { + void removeElementsThatAreUnreachableFrom_DoesntIncludeAllElements_WhenThereIsACyclicGraph() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); Person user = model.addPerson("User", ""); @@ -345,7 +345,7 @@ void test_removeElementsThatAreUnreachableFrom_DoesntIncludeAllElements_WhenTher } @Test - void test_removeRelationship_DoesNothing_WhenNullIsSpecified() { + void removeRelationship_DoesNothing_WhenNullIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); SoftwareSystem softwareSystem3 = model.addSoftwareSystem("Software System 3", "Description"); @@ -362,7 +362,7 @@ void test_removeRelationship_DoesNothing_WhenNullIsSpecified() { } @Test - void test_removeRelationship_RemovesARelationship_WhenAValidRelationshipIsSpecified() { + void removeRelationship_RemovesARelationship_WhenAValidRelationshipIsSpecified() { SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); SoftwareSystem softwareSystem3 = model.addSoftwareSystem("Software System 3", "Description"); @@ -383,7 +383,7 @@ void test_removeRelationship_RemovesARelationship_WhenAValidRelationshipIsSpecif } @Test - void test_setKey_ThrowsAnException_WhenANullKeyIsSpecified() { + void setKey_ThrowsAnException_WhenANullKeyIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); new SystemContextView(softwareSystem, null, "Description"); @@ -391,7 +391,7 @@ void test_setKey_ThrowsAnException_WhenANullKeyIsSpecified() { } @Test - void test_setKey_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + void setKey_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); new SystemContextView(softwareSystem, " ", "Description"); @@ -399,7 +399,7 @@ void test_setKey_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { } @Test - void test_addElement_ThrowsAnException_WhenTheSpecifiedElementDoesNotExistInTheModel() { + void addElement_ThrowsAnException_WhenTheSpecifiedElementDoesNotExistInTheModel() { try { Workspace workspace = new Workspace("1", ""); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); @@ -413,7 +413,7 @@ void test_addElement_ThrowsAnException_WhenTheSpecifiedElementDoesNotExistInTheM } @Test - void test_enableAutomaticLayout_EnablesAutoLayoutWithSomeDefaultValues_WhenTrueIsSpecified() { + void enableAutomaticLayout_EnablesAutoLayoutWithSomeDefaultValues_WhenTrueIsSpecified() { SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); view.enableAutomaticLayout(); @@ -426,7 +426,7 @@ void test_enableAutomaticLayout_EnablesAutoLayoutWithSomeDefaultValues_WhenTrueI } @Test - void test_enableAutomaticLayout_DisablesAutoLayout_WhenFalseIsSpecified() { + void enableAutomaticLayout_DisablesAutoLayout_WhenFalseIsSpecified() { SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); view.enableAutomaticLayout(); assertNotNull(view.getAutomaticLayout()); @@ -436,7 +436,7 @@ void test_enableAutomaticLayout_DisablesAutoLayout_WhenFalseIsSpecified() { } @Test - void test_enableAutomaticLayout() { + void enableAutomaticLayout() { SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); view.enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 100, 200, 300, true); @@ -449,7 +449,7 @@ void test_enableAutomaticLayout() { } @Test - void test_addCustomElement_AddsTheCustomElementToTheView() { + void addCustomElement_AddsTheCustomElementToTheView() { Workspace workspace = new Workspace("", ""); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); @@ -467,7 +467,7 @@ void test_addCustomElement_AddsTheCustomElementToTheView() { } @Test - void test_addCustomElementWithoutRelationships_AddsTheCustomElementToTheView() { + void addCustomElementWithoutRelationships_AddsTheCustomElementToTheView() { Workspace workspace = new Workspace("", ""); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); @@ -485,7 +485,7 @@ void test_addCustomElementWithoutRelationships_AddsTheCustomElementToTheView() { } @Test - void test_removeCustomElement_RemovesTheCustomElementFromTheView() { + void removeCustomElement_RemovesTheCustomElementFromTheView() { Workspace workspace = new Workspace("", ""); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); From 531975ed433db1b5d60a6c672b38a0742f510ec6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 12:53:35 +0100 Subject: [PATCH 017/418] Change GitHub action to run on Java 11 and 17. --- .github/workflows/gradle.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9432fa591..ed7c421ef 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -13,14 +13,17 @@ jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + java: [ '11', '17' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: - java-version: '11' - distribution: 'adopt' + java-version: ${{ matrix.java }} + distribution: 'temurin' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle From 9cc1373b43a3200698edf3fb8394334b0a496373 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 13:40:37 +0100 Subject: [PATCH 018/418] Tweak action. --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ed7c421ef..5b5e99dbc 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up Java uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} From 2d635a98a149725428c4804c21417d560ac27b5d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 18:06:28 +0100 Subject: [PATCH 019/418] Fixes #171. --- README.md | 25 +++------ docs/client-side-encryption.md | 2 +- docs/component-diagram.md | 2 +- docs/container-diagram.md | 2 +- docs/corporate-branding.md | 17 ------ docs/decisions.md | 7 --- docs/deployment-diagram.md | 2 +- docs/documentation-arc42.md | 32 ----------- docs/documentation-automatic.md | 22 -------- docs/documentation-structurizr.md | 32 ----------- ...cumentation-viewpoints-and-perspectives.md | 27 ---------- docs/documentation.md | 53 ------------------- docs/dynamic-diagram.md | 2 +- docs/filtered-views.md | 2 +- docs/getting-started.md | 8 +-- docs/health-checks.md | 7 --- docs/styling-elements.md | 4 +- docs/styling-relationships.md | 5 +- docs/system-context-diagram.md | 2 +- docs/system-landscape-diagram.md | 2 +- 20 files changed, 22 insertions(+), 233 deletions(-) delete mode 100644 docs/corporate-branding.md delete mode 100644 docs/decisions.md delete mode 100644 docs/documentation-arc42.md delete mode 100644 docs/documentation-automatic.md delete mode 100644 docs/documentation-structurizr.md delete mode 100644 docs/documentation-viewpoints-and-perspectives.md delete mode 100644 docs/documentation.md delete mode 100644 docs/health-checks.md diff --git a/README.md b/README.md index bb3c09cce..49a6872b3 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,15 @@ public static void main(String[] args) throws Exception { } ``` -The view can then be exported to be visualised using the [Structurizr cloud service/on-premises installation](https://structurizr.com), or other formats including PlantUML and WebSequenceDiagrams via the [Structurizr for Java extensions](https://github.com/structurizr/java-extensions). +The view can then be exported to be visualised using the [Structurizr cloud service/on-premises installation](https://structurizr.com), +or other formats including PlantUML, Mermaid, and WebSequenceDiagrams via the [structurizr-export library](https://github.com/structurizr/export). -![Views can be exported and visualised in many ways; e.g. PlantUML, Structurizr and Graphviz](docs/images/readme-1.png) +![Views can be exported and visualised via a number of tools](docs/images/readme-1.png) ## Table of contents * Introduction * [Getting started](docs/getting-started.md) - * [About Structurizr and how it compares to other tooling](https://structurizr.com/help/about) - * [Why use code?](https://structurizr.com/help/code) * [Basic concepts](https://structurizr.com/help/concepts) (workspaces, models, views and documentation) * [C4 model](https://c4model.com) * [Examples](https://github.com/structurizr/examples) @@ -57,26 +56,16 @@ The view can then be exported to be visualised using the [Structurizr cloud serv * [Styling relationships](docs/styling-relationships.md) * [Filtered views](docs/filtered-views.md) * [Graphviz automatic layout](https://github.com/structurizr/java-extensions/blob/master/structurizr-graphviz) -* Documentation - * [Documentation overview](docs/documentation.md) - * [Structurizr](docs/documentation-structurizr.md) - * [arc42](docs/documentation-arc42.md) - * [Viewpoints and Perspectives](docs/documentation-viewpoints-and-perspectives.md) - * [Automatic template](docs/documentation-automatic.md) - * [Architecture decision records](docs/decisions.md) * Other - * [HTTP-based health checks](docs/health-checks.md) * [Client-side encryption](docs/client-side-encryption.md) - * [Corporate branding](docs/corporate-branding.md) * Related projects + * [structurizr-dsl](https://github.com/structurizr/dsl): A text-based DSL for authoring Structurizr workspaces. * [java-quickstart](https://github.com/structurizr/java-quickstart): A simple starting point for using Structurizr for Java - * [java-extensions](https://github.com/structurizr/java-extensions): A collection of Structurizr for Java extensions; including the ability to extract software architecture information from code, export views to PlantUML, etc. - * [arch-as-code](https://github.com/nahknarmi/arch-as-code): A tool to store software architecture diagrams/documentation as YAML, and publish it to Structurizr. + * [structurizr-export](https://github.com/structurizr/export): Export model and views to external formats (e.g. PlantUML, Mermaid, etc). + * [structurizr-documentation](https://github.com/structurizr/documentation): Import Markdown/AsciiDoc documentation and ADRs into a Structurizr workspace. + * [java-extensions](https://github.com/structurizr/java-extensions): A collection of Structurizr for Java extensions; including the ability to extract software architecture information from code. * [structurizr-kotlin](https://github.com/Catalysts/structurizr-extensions/tree/master/structurizr-kotlin): An extension for Structurizr that lets you create your models in a fluent way. * [structurizr-spring-boot](https://github.com/Catalysts/structurizr-extensions/tree/master/structurizr-spring-boot): A way to apply dependency management to help modularise Structurizr code. * [structurizr-groovy](https://github.com/tidyjava/structurizr-groovy): An initial version of a Groovy wrapper around Structurizr for Java. - * [structurizr-dotnet](https://github.com/structurizr/dotnet): Structurizr for .NET * [changelog](docs/changelog.md) -[![Build Status](https://travis-ci.org/structurizr/java.svg?branch=master)](https://travis-ci.org/structurizr/java) - diff --git a/docs/client-side-encryption.md b/docs/client-side-encryption.md index 644c48052..cce9397e6 100644 --- a/docs/client-side-encryption.md +++ b/docs/client-side-encryption.md @@ -20,4 +20,4 @@ The default key size is 128 bits and the default iteration count is 1000. An alt In addition, a random salt and initialization vector are generated automatically for you, using Java's ```SecureRandom``` class. -See [ClientSideEncryption.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/ClientSideEncryption.java) for a full example, and [https://structurizr.com/share/41](https://structurizr.com/share/41) to access the workspace. \ No newline at end of file +See [ClientSideEncryption.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/ClientSideEncryption.java) for a full example, and [https://structurizr.com/share/41](https://structurizr.com/share/41) to access the workspace. \ No newline at end of file diff --git a/docs/component-diagram.md b/docs/component-diagram.md index 61d2b99c6..39c3dcd64 100644 --- a/docs/component-diagram.md +++ b/docs/component-diagram.md @@ -10,7 +10,7 @@ This is an example Component diagram for a fictional Internet Banking System, sh ![An example Component diagram](images/component-diagram-1.png) -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the code, and [https://structurizr.com/share/36141#Components](https://structurizr.com/share/36141#Components) for the diagram. +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#Components](https://structurizr.com/share/36141#Components) for the diagram. ### Extracting components automatically diff --git a/docs/container-diagram.md b/docs/container-diagram.md index 437c648a2..34691c74b 100644 --- a/docs/container-diagram.md +++ b/docs/container-diagram.md @@ -12,4 +12,4 @@ Both the Single-Page Application and Mobile App use a JSON/HTTPS API, which is p ![An example Container diagram](images/container-diagram-1.png) -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the code, and [https://structurizr.com/share/36141#Containers](https://structurizr.com/share/36141#Containers) for the diagram. \ No newline at end of file +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#Containers](https://structurizr.com/share/36141#Containers) for the diagram. \ No newline at end of file diff --git a/docs/corporate-branding.md b/docs/corporate-branding.md deleted file mode 100644 index d5bfb8874..000000000 --- a/docs/corporate-branding.md +++ /dev/null @@ -1,17 +0,0 @@ -# Corporate branding - -> Note: this page describes a feature that is not available to use with Structurizr's Free Plan. - -In addition to [styling diagram elements](styling-elements.md) and [relationships](styling-relationships.md), some corporate branding can be added to diagrams and documentation. This includes: - -- A font (font name and optional web font stylesheet URL). -- A logo (a URL to an image file or a data URI). - -You can add branding to an existing workspace, as follows: - -```java -Branding branding = views.getConfiguration().getBranding(); -branding.setLogo(ImageUtils.getImageAsDataUri(new File("./docs/images/structurizr-logo.png"))); -``` - -See [Help - Corporate Branding](https://structurizr.com/help/corporate-branding) for more details, [CorporateBranding.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/CorporateBranding.java) for a full example, and [https://structurizr.com/share/35031](https://structurizr.com/share/35031) to access the workspace. diff --git a/docs/decisions.md b/docs/decisions.md deleted file mode 100644 index bff92c907..000000000 --- a/docs/decisions.md +++ /dev/null @@ -1,7 +0,0 @@ -# Decisions - -Although architecture decisions can be included in supplementary documentation, Structurizr also provides support for publishing architecture decision records, [as described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). - -Decision records can either be created manually using the API on the [Documentation class](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/documentation/Documentation.java), or using the [AdrToolsImporter](https://github.com/structurizr/java-extensions/blob/master/structurizr-adr-tools/src/com/structurizr/documentation/AdrToolsImporter.java) to import ADRs from Nat Pryce's popular [adr-tools](https://github.com/npryce/adr-tools) tooling. Here is [an example](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/AdrTools.java). - -See [Structurizr - Decision Log](https://structurizr.com/help/decision-log) for more details. diff --git a/docs/deployment-diagram.md b/docs/deployment-diagram.md index 5abda112f..972b51363 100644 --- a/docs/deployment-diagram.md +++ b/docs/deployment-diagram.md @@ -8,4 +8,4 @@ As an example, a Deployment diagram for the live environment of a simplified, fi ![An example Deployment diagram](images/deployment-diagram-1.png) -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the code, and [https://structurizr.com/share/36141#LiveDeployment](https://structurizr.com/share/36141#LiveDeployment) for the diagram. \ No newline at end of file +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#LiveDeployment](https://structurizr.com/share/36141#LiveDeployment) for the diagram. \ No newline at end of file diff --git a/docs/documentation-arc42.md b/docs/documentation-arc42.md deleted file mode 100644 index 99f2c1e0d..000000000 --- a/docs/documentation-arc42.md +++ /dev/null @@ -1,32 +0,0 @@ -# arc42 documentation template - -Structurizr for Java includes an implementation of the [arc42 documentation template](http://arc42.org), which can be used to document your software architecture. - -## Example - -To use this template, create an instance of the [Arc42DocumentationTemplate](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/documentation/Arc42DocumentationTemplate.java) class. -You can then add documentation sections as needed, each associated with a software system in your software architecture model, using Markdown or AsciiDoc. For example: - -```java -Arc42DocumentationTemplate template = new Arc42DocumentationTemplate(workspace); - -File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown"); -template.addIntroductionAndGoalsSection(softwareSystem, new File(documentationRoot, "01-introduction-and-goals.md")); -template.addConstraintsSection(softwareSystem, new File(documentationRoot, "02-architecture-constraints.md")); -template.addContextAndScopeSection(softwareSystem, new File(documentationRoot, "03-system-scope-and-context.md")); -template.addSolutionStrategySection(softwareSystem, new File(documentationRoot, "04-solution-strategy.md")); -template.addBuildingBlockViewSection(softwareSystem, new File(documentationRoot, "05-building-block-view.md")); -template.addRuntimeViewSection(softwareSystem, new File(documentationRoot, "06-runtime-view.md")); -template.addDeploymentViewSection(softwareSystem, new File(documentationRoot, "07-deployment-view.md")); -template.addCrosscuttingConceptsSection(softwareSystem, new File(documentationRoot, "08-crosscutting-concepts.md")); -template.addArchitecturalDecisionsSection(softwareSystem, new File(documentationRoot, "09-architecture-decisions.md")); -template.addRisksAndTechnicalDebtSection(softwareSystem, new File(documentationRoot, "10-quality-requirements.md")); -template.addQualityRequirementsSection(softwareSystem, new File(documentationRoot, "11-risks-and-technical-debt.md")); -template.addGlossarySection(softwareSystem, new File(documentationRoot, "12-glossary.md")); -``` - -Structurizr will create navigation controls based upon the the sections in the documentation, and the software systems they have been associated with. See [Arc42DocumentationExample.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/Arc42DocumentationExample.java) for the full code, and [https://structurizr.com/share/27791/documentation](https://structurizr.com/share/27791/documentation) to see the rendered documentation. - -## More information - -See [Help - Documentation](https://structurizr.com/help/documentation) for more information about how headings are rendered, and how to embed diagrams from you workspace into the documentation. \ No newline at end of file diff --git a/docs/documentation-automatic.md b/docs/documentation-automatic.md deleted file mode 100644 index 7424fc5e9..000000000 --- a/docs/documentation-automatic.md +++ /dev/null @@ -1,22 +0,0 @@ -# Automatic documentation template - -Structurizr for Java includes an automatic documentation template, which will scan a given directory and automatically add all Markdown or AsciiDoc -files in that directory. Each file must represent a separate section, and the second level heading ("## Section Title" in Markdown and "== Section Title" in AsciiDoc) will be used as the section name. - -## Example - -To use this template, create an instance of the [AutomaticDocumentationTemplate](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/documentation/AutomaticDocumentationTemplate.java) class. -You can then add documentation sections as needed, each associated with a software system in your software architecture model, using Markdown or AsciiDoc. For example: - -```java -File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/automatic"); - -AutomaticDocumentationTemplate template = new AutomaticDocumentationTemplate(workspace); -template.addSections(softwareSystem, documentationRoot); -``` - -Structurizr will create navigation controls based upon the the sections in the documentation, and the software systems they have been associated with. See [AutomaticDocumentationTemplateExample.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/AutomaticDocumentationTemplateExample.java) for the full code, and [https://structurizr.com/share/35971/documentation](https://structurizr.com/share/35971/documentation) to see the rendered documentation. - -## More information - -See [Help - Documentation](https://structurizr.com/help/documentation) for more information about how headings are rendered, and how to embed diagrams from you workspace into the documentation. \ No newline at end of file diff --git a/docs/documentation-structurizr.md b/docs/documentation-structurizr.md deleted file mode 100644 index d6397403b..000000000 --- a/docs/documentation-structurizr.md +++ /dev/null @@ -1,32 +0,0 @@ -# Structurizr documentation template - -Structurizr for Java includes an implementation of the "software guidebook" from Simon Brown's [Software Architecture for Developers](https://leanpub.com/visualising-software-architecture) book, which can be used to document your software architecture. - -## Example - -To use this template, create an instance of the [StructurizrDocumentationTemplate](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/documentation/StructurizrDocumentationTemplate.java) class. -You can then add documentation sections as needed, each associated with a software system in your software architecture model, using Markdown or AsciiDoc. For example: - -```java -StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - -File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown"); -template.addContextSection(softwareSystem, new File(documentationRoot, "01-context.md")); -template.addFunctionalOverviewSection(softwareSystem, new File(documentationRoot, "02-functional-overview.md")); -template.addQualityAttributesSection(softwareSystem, new File(documentationRoot, "03-quality-attributes.md")); -template.addConstraintsSection(softwareSystem, new File(documentationRoot, "04-constraints.md")); -template.addPrinciplesSection(softwareSystem, new File(documentationRoot, "05-principles.md")); -template.addSoftwareArchitectureSection(softwareSystem, new File(documentationRoot, "06-software-architecture.md")); -template.addDataSection(softwareSystem, new File(documentationRoot, "07-data.md")); -template.addInfrastructureArchitectureSection(softwareSystem, new File(documentationRoot, "08-infrastructure-architecture.md")); -template.addDeploymentSection(softwareSystem, new File(documentationRoot, "09-deployment.md")); -template.addDevelopmentEnvironmentSection(softwareSystem, new File(documentationRoot, "10-development-environment.md")); -template.addOperationAndSupportSection(softwareSystem, new File(documentationRoot, "11-operation-and-support.md")); -template.addDecisionLogSection(softwareSystem, new File(documentationRoot, "12-decision-log.md")); -``` - -Structurizr will create navigation controls based upon the the sections in the documentation, and the software systems they have been associated with. See [StructurizrDocumentationExample.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/StructurizrDocumentationExample.java) for the full code, and [https://structurizr.com/share/14181/documentation](https://structurizr.com/share/14181/documentation) to see the rendered documentation. - -## More information - -See [Help - Documentation](https://structurizr.com/help/documentation) for more information about how headings are rendered, and how to embed diagrams from you workspace into the documentation. \ No newline at end of file diff --git a/docs/documentation-viewpoints-and-perspectives.md b/docs/documentation-viewpoints-and-perspectives.md deleted file mode 100644 index ab8c39b90..000000000 --- a/docs/documentation-viewpoints-and-perspectives.md +++ /dev/null @@ -1,27 +0,0 @@ -# Viewpoints and Perspectives documentation template - -Structurizr for Java includes an implementation of the [Viewpoints and Perspectives documentation template](http://www.viewpoints-and-perspectives.info), which can be used to document your software architecture. - -## Example - -To use this template, create an instance of the [ViewpointsAndPerspectivesDocumentationTemplate](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/documentation/ViewpointsAndPerspectivesDocumentationTemplate.java) class. -You can then add documentation sections as needed, each associated with a software system in your software architecture model, using Markdown or AsciiDoc. For example: - -```java -ViewpointsAndPerspectivesDocumentationTemplate template = new ViewpointsAndPerspectivesDocumentationTemplate(workspace); - -File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown"); -template.addIntroductionSection(softwareSystem, new File(documentationRoot, "01-introduction.md")); -template.addGlossarySection(softwareSystem, new File(documentationRoot, "02-glossary.md")); -template.addSystemStakeholdersAndRequirementsSection(softwareSystem, new File(documentationRoot, "03-system-stakeholders-and-requirements.md")); -template.addArchitecturalForcesSection(softwareSystem, new File(documentationRoot, "04-architectural-forces.md")); -template.addArchitecturalViewsSection(softwareSystem, new File(documentationRoot, "05-architectural-views")); -template.addSystemQualitiesSection(softwareSystem, new File(documentationRoot, "06-system-qualities.md")); -template.addAppendicesSection(softwareSystem, new File(documentationRoot, "07-appendices.md")); -``` - -Structurizr will create navigation controls based upon the the sections in the documentation, and the software systems they have been associated with. See [ViewpointsAndPerspectivesDocumentationExample.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/ViewpointsAndPerspectivesDocumentationExample.java) for the full code, and [https://structurizr.com/share/36371/documentation](https://structurizr.com/share/36371/documentation) to see the rendered documentation. - -## More information - -See [Help - Documentation](https://structurizr.com/help/documentation) for more information about how headings are rendered, and how to embed diagrams from you workspace into the documentation. \ No newline at end of file diff --git a/docs/documentation.md b/docs/documentation.md deleted file mode 100644 index 74869e488..000000000 --- a/docs/documentation.md +++ /dev/null @@ -1,53 +0,0 @@ -# Documentation - -In addition to diagrams, Structurizr lets you create supplementary documentation using the Markdown or AsciiDoc formats. - -![Example documentation](images/documentation-1.png) - -See [https://structurizr.com/share/31/documentation](https://structurizr.com/share/31/documentation) for an example. - -## Documentation templates - -The documentation is broken up into a number of sections, as defined by the template you are using, the following of which are included: - -- [Structurizr](documentation-structurizr.md) -- [arc42](documentation-arc42.md) -- [Viewpoints and Perspectives](documentation-viewpoints-and-perspectives.md) -- [Automatic template](documentation-automatic.md) - -## Custom sections - -You can add custom sections using the ```addSection``` method on the [DocumentationTemplate](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/documentation/DocumentationTemplate.java) class: - -```java -template.addSection(softwareSystem, "My custom section", Format.Markdown, ...); -``` - -## Images - -Images can be included using the regular Markdown/AsciiDoc syntax. - -![Including images](images/documentation-2.png) - -For this to work, the image files must be hosted externally (e.g. on your own web server, ideally accessible via HTTPS) or uploaded with your workspace using the ```addImages()``` or ```addImage()``` methods on the [DocumentationTemplate](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/documentation/DocumentationTemplate.java) class. - -```java -template.addImages(new File("...")); -``` - -See [functional-overview.md](https://raw.githubusercontent.com/structurizr/java/master/structurizr-examples/src/com/structurizr/example/financialrisksystem/functional-overview.md) and [FinancialRiskSystem](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/FinancialRiskSystem.java) for an example. - -## Embedding diagrams - -Software architecture diagrams from your workspace can be embedded within the documentation sections using an additional special syntax. - -![Embedding diagrams](images/documentation-3.png) - -The syntax is similar to that used for including images, for example: - -``` -Markdown - ![](embed:DiagramKey) -AsciiDoc - image::embed:DiagramKey[] -``` - -See [context.md](https://raw.githubusercontent.com/structurizr/java/master/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.md), [context.adoc](https://raw.githubusercontent.com/structurizr/java/master/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.adoc) and [FinancialRiskSystem](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/FinancialRiskSystem.java) for an example. \ No newline at end of file diff --git a/docs/dynamic-diagram.md b/docs/dynamic-diagram.md index 88374e8ea..673b6a700 100644 --- a/docs/dynamic-diagram.md +++ b/docs/dynamic-diagram.md @@ -8,7 +8,7 @@ As an example, a Dynamic diagram describing the customer sign in process for a s ![An example Dynamic diagram](images/dynamic-diagram-1.png) -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the code, and [https://structurizr.com/share/36141#SignIn](https://structurizr.com/share/36141#SignIn) for the diagram. +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#SignIn](https://structurizr.com/share/36141#SignIn) for the diagram. ### Adding relationships diff --git a/docs/filtered-views.md b/docs/filtered-views.md index 6e1f104dc..7b749e566 100644 --- a/docs/filtered-views.md +++ b/docs/filtered-views.md @@ -36,4 +36,4 @@ views.createFilteredView(systemLandscapeView, "FutureState", "The future state s In summary, you create a view with all of the elements and relationships that you want to show, and then create one or more filtered views on top, specifying the tags that you'd like to include or exclude. -See [FilteredViews.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/FilteredViews.java) for the full code, and [https://structurizr.com/share/19911](https://structurizr.com/share/19911) for the diagram. \ No newline at end of file +See [FilteredViews.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/FilteredViews.java) for the full code, and [https://structurizr.com/share/19911](https://structurizr.com/share/19911) for the diagram. \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md index 79b918070..5e11b5f7f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,14 +1,16 @@ # Getting started -Here is a quick overview of how to get started with Structurizr for Java so that you can create a software architecture model as code. You can find the code at [GettingStarted.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/GettingStarted.java) and the live example workspace at [https://structurizr.com/share/25441](https://structurizr.com/share/25441). +Here is a quick overview of how to get started with Structurizr for Java so that you can create a software architecture model as code. +You can find the code at [GettingStarted.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/GettingStarted.java) +and the live example workspace at [https://structurizr.com/share/25441](https://structurizr.com/share/25441). > See the [java-quickstart project](https://github.com/structurizr/java-quickstart) for a quick and simple way to get started with Structurizr for Java. -For more examples, please see [structurizr-examples](https://github.com/structurizr/java/tree/master/structurizr-examples/src/com/structurizr/example). +For more examples, please see [structurizr-examples](https://github.com/structurizr/examples/tree/main/java/src/main/java/com/structurizr/example). ## 1. Dependencies -The Structurizr for Java binaries are hosted on [Maven Central](https://repo1.maven.org/maven2/com/structurizr/) and the dependencies for use with Maven, Ivy, Gradle, etc are as follows. +The Structurizr for Java binaries are hosted on [Maven Central](https://repo1.maven.org/maven2/com/structurizr/structurizr-client/) and the dependencies for use with Maven, Ivy, Gradle, etc are as follows. Name | Description ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- diff --git a/docs/health-checks.md b/docs/health-checks.md deleted file mode 100644 index 6cf5f1b5a..000000000 --- a/docs/health-checks.md +++ /dev/null @@ -1,7 +0,0 @@ -# HTTP-based health checks - -Structurizr's health checks feature allows you to supplement your deployment models with HTTP-based health checks to get an "at a glance" view of the health of your software systems. See [Structurizr - Health Checks](https://structurizr.com/help/health-checks) for more details. - -When defining your software architecture model using the client library, HTTP-based health checks can be added to the Container Instances in your deployment model. Each health check is defined by a name, an endpoint URL, a polling interval (e.g. 60 seconds), a timeout (e.g. 1000 milliseconds), and optionally one or more HTTP headers. - -[HttpHealthChecks.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/HttpHealthChecks.java) shows an example of how to setup the health checks, and the result can be see online at [https://structurizr.com/share/39441/health](https://structurizr.com/share/39441/health). \ No newline at end of file diff --git a/docs/styling-elements.md b/docs/styling-elements.md index d8e7674f3..c0ae0967f 100644 --- a/docs/styling-elements.md +++ b/docs/styling-elements.md @@ -75,6 +75,4 @@ The set of available shapes is as follows: Structurizr will automatically add all element styles to a diagram key, showing you which styles are associated with which tags. -![The diagram key](images/styling-elements-6.png) - -You can find the code for this example at [StylingElements.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/StylingElements.java) and the live example workspace at [https://structurizr.com/share/36111](https://structurizr.com/share/36111). \ No newline at end of file +![The diagram key](images/styling-elements-6.png) \ No newline at end of file diff --git a/docs/styling-relationships.md b/docs/styling-relationships.md index 942a1c179..ae696bd34 100644 --- a/docs/styling-relationships.md +++ b/docs/styling-relationships.md @@ -45,7 +45,4 @@ styles.addRelationshipStyle("JDBC").color("#0000ff"); Structurizr will automatically add all relationship styles to a diagram key. -![The diagram key](images/styling-relationships-4.png) - - -You can find the code for this example at [StylingRelationships.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/StylingRelationships.java) and the live example workspace at [https://structurizr.com/share/36131](https://structurizr.com/share/36131). \ No newline at end of file +![The diagram key](images/styling-relationships-4.png) \ No newline at end of file diff --git a/docs/system-context-diagram.md b/docs/system-context-diagram.md index 25331aade..1c265dc0d 100644 --- a/docs/system-context-diagram.md +++ b/docs/system-context-diagram.md @@ -10,4 +10,4 @@ This is an example System Context diagram for a fictional Internet Banking Syste ![An example System Context diagram](images/system-context-diagram-1.png) -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the code, and [https://structurizr.com/share/36141#SystemContext](https://structurizr.com/share/36141#SystemContext) for the diagram. \ No newline at end of file +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#SystemContext](https://structurizr.com/share/36141#SystemContext) for the diagram. \ No newline at end of file diff --git a/docs/system-landscape-diagram.md b/docs/system-landscape-diagram.md index aed32f0a9..ad1caeee5 100644 --- a/docs/system-landscape-diagram.md +++ b/docs/system-landscape-diagram.md @@ -10,4 +10,4 @@ As an example, a System Landscape diagram for a simplified, fictional bank might ![An example System Landscape diagram](images/system-landscape-diagram-1.png) -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the code, and [https://structurizr.com/share/36141#SystemLandscape](https://structurizr.com/share/36141#SystemLandscape) for the diagram. \ No newline at end of file +See [SystemLandscape.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/SystemLandscape.java) for the code, and [https://structurizr.com/share/36141#SystemLandscape](https://structurizr.com/share/36141#SystemLandscape) for the diagram. \ No newline at end of file From aa79b7c0d830e549ff6784e886d09a9d843ef986 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 18:09:22 +0100 Subject: [PATCH 020/418] Fixes link. --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9099392bb..0a3dcc9c2 100644 --- a/build.gradle +++ b/build.gradle @@ -73,8 +73,8 @@ subprojects { proj -> url = 'https://github.com/structurizr/java' scm { - connection = 'scm:git:git://github.com/structurizr/structurizr-java.git' - developerConnection = 'scm:git:git@github.com:structurizr/structurizr-java.git' + connection = 'scm:git:git://github.com/structurizr/java.git' + developerConnection = 'scm:git:git@github.com:structurizr/java.git' url = 'https://github.com/structurizr/java' } From 83f874b2a9f345b401afce4183959f953057891a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 18:09:35 +0100 Subject: [PATCH 021/418] Fixes link. --- docs/system-landscape-diagram.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/system-landscape-diagram.md b/docs/system-landscape-diagram.md index ad1caeee5..7e7b0d599 100644 --- a/docs/system-landscape-diagram.md +++ b/docs/system-landscape-diagram.md @@ -10,4 +10,4 @@ As an example, a System Landscape diagram for a simplified, fictional bank might ![An example System Landscape diagram](images/system-landscape-diagram-1.png) -See [SystemLandscape.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/SystemLandscape.java) for the code, and [https://structurizr.com/share/36141#SystemLandscape](https://structurizr.com/share/36141#SystemLandscape) for the diagram. \ No newline at end of file +See [SystemLandscape.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/SystemLandscape.java) for the code, and [https://structurizr.com/share/28201/diagrams#SystemLandscape](https://structurizr.com/share/28201/diagrams#SystemLandscape) for the diagram. \ No newline at end of file From 94228f918688f9fa865f6168ded73b768781c36c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 18:11:42 +0100 Subject: [PATCH 022/418] Fixes diagram links. --- docs/component-diagram.md | 2 +- docs/container-diagram.md | 2 +- docs/dynamic-diagram.md | 2 +- docs/filtered-views.md | 2 +- docs/system-context-diagram.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/component-diagram.md b/docs/component-diagram.md index 39c3dcd64..6b7f6a2d3 100644 --- a/docs/component-diagram.md +++ b/docs/component-diagram.md @@ -10,7 +10,7 @@ This is an example Component diagram for a fictional Internet Banking System, sh ![An example Component diagram](images/component-diagram-1.png) -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#Components](https://structurizr.com/share/36141#Components) for the diagram. +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#Components](https://structurizr.com/share/36141/diagrams#Components) for the diagram. ### Extracting components automatically diff --git a/docs/container-diagram.md b/docs/container-diagram.md index 34691c74b..1592be0a2 100644 --- a/docs/container-diagram.md +++ b/docs/container-diagram.md @@ -12,4 +12,4 @@ Both the Single-Page Application and Mobile App use a JSON/HTTPS API, which is p ![An example Container diagram](images/container-diagram-1.png) -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#Containers](https://structurizr.com/share/36141#Containers) for the diagram. \ No newline at end of file +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#Containers](https://structurizr.com/share/36141/diagrams#Containers) for the diagram. \ No newline at end of file diff --git a/docs/dynamic-diagram.md b/docs/dynamic-diagram.md index 673b6a700..6a5879e10 100644 --- a/docs/dynamic-diagram.md +++ b/docs/dynamic-diagram.md @@ -8,7 +8,7 @@ As an example, a Dynamic diagram describing the customer sign in process for a s ![An example Dynamic diagram](images/dynamic-diagram-1.png) -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#SignIn](https://structurizr.com/share/36141#SignIn) for the diagram. +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#SignIn](https://structurizr.com/share/36141/diagrams#SignIn) for the diagram. ### Adding relationships diff --git a/docs/filtered-views.md b/docs/filtered-views.md index 7b749e566..c0e700872 100644 --- a/docs/filtered-views.md +++ b/docs/filtered-views.md @@ -36,4 +36,4 @@ views.createFilteredView(systemLandscapeView, "FutureState", "The future state s In summary, you create a view with all of the elements and relationships that you want to show, and then create one or more filtered views on top, specifying the tags that you'd like to include or exclude. -See [FilteredViews.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/FilteredViews.java) for the full code, and [https://structurizr.com/share/19911](https://structurizr.com/share/19911) for the diagram. \ No newline at end of file +See [FilteredViews.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/FilteredViews.java) for the full code, and [https://structurizr.com/share/19911/diagrams](https://structurizr.com/share/19911/diagrams) for the diagram. \ No newline at end of file diff --git a/docs/system-context-diagram.md b/docs/system-context-diagram.md index 1c265dc0d..0bef4d543 100644 --- a/docs/system-context-diagram.md +++ b/docs/system-context-diagram.md @@ -10,4 +10,4 @@ This is an example System Context diagram for a fictional Internet Banking Syste ![An example System Context diagram](images/system-context-diagram-1.png) -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#SystemContext](https://structurizr.com/share/36141#SystemContext) for the diagram. \ No newline at end of file +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#SystemContext](https://structurizr.com/share/36141/diagrams#SystemContext) for the diagram. \ No newline at end of file From b78c5bf565d3c1d28a477db91a89768a887988be Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 18:12:12 +0100 Subject: [PATCH 023/418] Fixes diagram links. --- docs/deployment-diagram.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deployment-diagram.md b/docs/deployment-diagram.md index 972b51363..3facc2a88 100644 --- a/docs/deployment-diagram.md +++ b/docs/deployment-diagram.md @@ -8,4 +8,4 @@ As an example, a Deployment diagram for the live environment of a simplified, fi ![An example Deployment diagram](images/deployment-diagram-1.png) -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141#LiveDeployment](https://structurizr.com/share/36141#LiveDeployment) for the diagram. \ No newline at end of file +See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#LiveDeployment](https://structurizr.com/share/36141/diagrams#LiveDeployment) for the diagram. \ No newline at end of file From 6e02d523717b4f91c4fed9abf9f7d9f43235f00a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 18:16:18 +0100 Subject: [PATCH 024/418] Use the published versions of the diagrams. --- docs/component-diagram.md | 2 +- docs/container-diagram.md | 2 +- docs/deployment-diagram.md | 2 +- docs/dynamic-diagram.md | 2 +- docs/images/component-diagram-1.png | Bin 390052 -> 0 bytes docs/images/container-diagram-1.png | Bin 386295 -> 0 bytes docs/images/deployment-diagram-1.png | Bin 424108 -> 0 bytes docs/images/dynamic-diagram-1.png | Bin 273565 -> 0 bytes docs/images/system-context-diagram-1.png | Bin 257184 -> 0 bytes docs/images/system-landscape-diagram-1.png | Bin 354820 -> 0 bytes docs/system-context-diagram.md | 2 +- docs/system-landscape-diagram.md | 2 +- 12 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 docs/images/component-diagram-1.png delete mode 100644 docs/images/container-diagram-1.png delete mode 100644 docs/images/deployment-diagram-1.png delete mode 100644 docs/images/dynamic-diagram-1.png delete mode 100644 docs/images/system-context-diagram-1.png delete mode 100644 docs/images/system-landscape-diagram-1.png diff --git a/docs/component-diagram.md b/docs/component-diagram.md index 6b7f6a2d3..2edbe47b7 100644 --- a/docs/component-diagram.md +++ b/docs/component-diagram.md @@ -8,7 +8,7 @@ The Component diagram shows how a container is made up of a number of "component This is an example Component diagram for a fictional Internet Banking System, showing some (rather than all) of the components within the API Application. Here, there are two Spring MVC Rest Controllers providing access points for the JSON/HTTPS API, with each controller subsequently using other components to access data from the Database and Mainframe Banking System. -![An example Component diagram](images/component-diagram-1.png) +![An example Component diagram](https://static.structurizr.com/workspace/36141/diagrams/Components.png) See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#Components](https://structurizr.com/share/36141/diagrams#Components) for the diagram. diff --git a/docs/container-diagram.md b/docs/container-diagram.md index 1592be0a2..ebc56d8d2 100644 --- a/docs/container-diagram.md +++ b/docs/container-diagram.md @@ -10,6 +10,6 @@ This is an example Container diagram for a fictional Internet Banking System. It Both the Single-Page Application and Mobile App use a JSON/HTTPS API, which is provided by another Java/Spring MVC application running on the server. The API Application gets user information from the Database (a relational database schema). The API Application also communicates with the existing Mainframe Banking System, using a propreitary XML/HTTPS interface, to get information about bank accounts or make transactions. The API Application also uses the existing E-mail System if it needs to send e-mails to customers. -![An example Container diagram](images/container-diagram-1.png) +![An example Container diagram](https://static.structurizr.com/workspace/36141/diagrams/Containers.png) See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#Containers](https://structurizr.com/share/36141/diagrams#Containers) for the diagram. \ No newline at end of file diff --git a/docs/deployment-diagram.md b/docs/deployment-diagram.md index 3facc2a88..39b80fc92 100644 --- a/docs/deployment-diagram.md +++ b/docs/deployment-diagram.md @@ -6,6 +6,6 @@ A deployment diagram allows you to illustrate how containers in the static model As an example, a Deployment diagram for the live environment of a simplified, fictional Internet Banking System might look something like this. In summary, it shows the deployment of the Web Application and the Database, with a secondary Database being used for failover purposes. -![An example Deployment diagram](images/deployment-diagram-1.png) +![An example Deployment diagram](https://static.structurizr.com/workspace/36141/diagrams/LiveDeployment.png) See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#LiveDeployment](https://structurizr.com/share/36141/diagrams#LiveDeployment) for the diagram. \ No newline at end of file diff --git a/docs/dynamic-diagram.md b/docs/dynamic-diagram.md index 6a5879e10..8443a8234 100644 --- a/docs/dynamic-diagram.md +++ b/docs/dynamic-diagram.md @@ -6,7 +6,7 @@ A simple dynamic diagram can be useful when you want to show how elements in a s As an example, a Dynamic diagram describing the customer sign in process for a simplified, fictional Internet Banking System might look something like this. In summary, it shows the components involved in the sign in process, and the interactions between them. -![An example Dynamic diagram](images/dynamic-diagram-1.png) +![An example Dynamic diagram](https://static.structurizr.com/workspace/36141/diagrams/SignIn.png) See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#SignIn](https://structurizr.com/share/36141/diagrams#SignIn) for the diagram. diff --git a/docs/images/component-diagram-1.png b/docs/images/component-diagram-1.png deleted file mode 100644 index a4046f7e8787540726bc70254172182e68f6f2fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390052 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�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≪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 -s-bgO{ace?b+t$D|+vS z@*)fNhPnwtN|oe~2&SIsi;1uM2N8_CE&9Fw)&{BLH?vE$h3-9f>-QjusKxIN3HMn& zQdjXomRqWweNRwEemeda)i$&qKjphW}Ftg@qKe#g;Y!LOk z2B~}7I6vF{&F?$^f_OLB8h&oyl_`*DHvZb$+Wz62mfv18ryhUz{`a^%APb_{kf^G? zi>_=;wHvzmP=x)XrW_8w9A8m?rcZ_&o-K90qe=R1{y{%2d12N{~O=^4d zUUPle#uTzkY7zVHiomo29qhH2&!cnm^Hrd7{yFGez1#Tlr2xwknZo@BSLbL7*v=8{ z-c^V6*j^p>s`!x#S&iF0q&T|h2Nh9xMnDJwGIPFQKbyC`Btz3mEx@c8x0HO(<=)z9 z;6C&E@U_1xBKz_X*aWYf%_}VrTv^JsJp#Y%%L3_KmZA@SQJ;N7)j~KH6UV~JDt;S z7&~@X`-ngq)B8XnKjeMxfX-8;{YuTke^Es?*F00{iU3(~;j|TDk)0J;?ZZ%yQ)TYyt_bw@9cIbQIBEh^CSUa?ukSe$UXmd6XSg{6TA=kA8jX3N5) zN%j-Zpdxy4KgT0Qv)X3zL${#If{2i})oaeK^D!W*n*p{2Rt9QW-*EmsoE^1OFT2;L z!uaBOS{XHC3Hh-PwEQ0<-+Fdi9{#0;jd&Ya!g*J)N9*HJX47vN%grPe5l%r4{xnkD z?QPe?2?6ck3cUam2St10DcY!%c?ZzQDsJ;{Kvld!IG+wJY-09^*a;C=X+I}IR&&kB z?8QSWRsW%vok~rHzf_ty8+bSN|4fO40gBY_BzJtmZ7QZ!T$I|ZgbgMe+^MR zQb|6DNSC|MHWn;zeSqo&?(hMxq6NHmvc%V;`|@11-qo0@mZJif*Fco`g`D^?aE!K}+?Z}ugXm$xI@7D*c7pux|8JY20a!}0{ zDMps7S8wT_-%X+Dmy0}2TK7h?ZJxNi*~t&PB@vRtHUg-ZsgELD(dafC^&uH#qEDC3 zPT$3vp5RsI#Td=HvD~L+#V^xX>_ZSDT87E@d=#9lyw9h`qfZQ$eWw`Fd@s2ou8_Jh zRLi8drB-Z>z)cDqt|n>{FC1LDcvWxq|=4KZux6{U^E=OGK^hq&?rV8|EACp0`@5Gj4EKtootT!oa1-e+T zQq)QxC~>!IypB(q$h-+JYjr$UL>m5`V7S*GlCg3YxPrtEr-+&Hoyk?dV;EK5A6SFJ zOr+$oiy%wHr*61rUS@QFc;2bWSz#Jl*djPWyOSIwg#-%o4;et=@-?W{S zi#m+i`%^WVJw<`CjX)sV?SUY9526hhR=fl8Thb&d)0|~Z#N&Act-B?LK8+ZK@t{y( zYAn4aypmSS48IRR^2kPwl^*PyE?1XeB1_AFsD#7)l>al+_cpli`l@Z;f#5jn9`ETR z&YWq0w5z4Z-yvJoFh$`{T;yUr?5Yh$z5VT~CB)9?xBm+T8MZ$@6T0y&K_{ufJvvOv zMWMJ$-x_L!Qxhu=DQ#*p-D;mcIx29D(Q!f+OVf#Q{(C4huts$t;ltuI4zQ- zZ3;N5(NlZTh^U0yb3jflZBQejXwBR;+AI$BTN*wb|8XHZ3^(xQ#Pm^4EA08BzjEeG zGjF)UPD6mZN9g3vFR_PCjExRuh`hd2yA@1uhKKvOH=3I6TdeR|2U0nWjp6adTJigB zr?R>3&f6x$I~xR_-Vej=EAy!BNtPccjj1 z!*V1PUHQswpBVbpg|&rQ+nttQ1oFO?FIl2CJY)QM(PCC;L9XR30bd}pmQeKDVkN+r z#RKC@bSN;XhEBJvOR9w6A&6_FaPMeINaC2Y|hdXO&fh#bc7X(nA{&KbVk7B>j`BUJ8 zfjG510E)2=$_ayZSAR=?Wt17Ne1#+$EwbK8iS4zxAJaHP`3aze(S9+P!@apmm(>a@ zI;PApOmxIA95@$0`$*R<@zt7{eW8Zf^cJFH`!Y!$)raQYO3$zkG#Ro07WS{ z9ZAHHF|_;6qJLk9!-~O|&9u;?it@B6o3aRu4eCnZJi<|&iSS)2Ys_{7)U9T<4MjV+ zrvbVCPSD{}+M9<%C{U%bq;U?tHQG(q4ehqv?sl1lrKNolBkDvM8YedfBo}tsaDgZ* zYY;~iQCmf=OhWmg!~P6KTar!04b~GgQnLdR^_s-jdAb>B4j1R;DB82YLqe@yy`Verg5iVy z1DAV0Cr?h3+2*UUE{@E!IMa`dGMx2+K(#-<%D(q@`cUTTe_I4hxmOL;fag8itKIbI z@Xo1hF-_wx>?3B=`Be&Yyf}|tEo(4b@9R;!q2a1_R-R?iDF(vvD^x#)nrvzFXhvlX z$tv)XdEi^u9?c^&_Hiu2s`<_k>a*+)L*`D!nh%WE~x!G3vUHPQ$eV> zWq38Zfao-id$(JgzV+UBrc<_j($wCH+2%`mY{IGj~}^w@jR|JC%aMw5KcA7O2C{Ytz`J%VDnU- zT5_7Tf}yix6hJeq4yHw@vgVGS{>ymmsHs)YLH)Mx&E@k*-zTZ9-VrM}fk?&J{Ui)! zjFj(&bf*(M{x+i8t^Y7NG#se8M?2yY=%)Q^C!QO&F<$KVFd@9vY`*$3yUi^ zW4~-3_X=5V@OrA~E&an7m|Ig_$_CZWiAth7&7qQC>&-*G$Ki$WbRn35qO;y^{N(X@@5#cz6_ z)2~VCJVr^ywgW)w_coq}RM>py`yhY~I&iSZ-*=kj52_&l-3T}>EMVyF)#}uYByfj@k?|?woc>Q z*}O!lY>H=%(`m2>ahO$t^hFk*z}f%Jp2)g6f19FwNZ7?NK+_kRkKTK@Og=VQ6S^( z1+TZWG09CI!`y>NUHAQ}A8Vg$$FY}JL-ss9D(N>q@lYg9qblqPZge0EDvpN4crE-q z(f3B5*vws9VML4d*n5emp?~RjqVoa}Q~^jX_J!|9t7oBs<|?V`yw+Q1tm>9NJrY}P zx)|1F^gtuHIy`+GTpUYPM@>ZY&BUU9DV?^$Aw@M^6r6JCy9Uvo?Ik;DN8*4nqcHH` z?BeEbJK=1|t1-v#}CYn8)(qz|L**~7VnU3|6D8dWnx(UE||?Ee_hy1~Dx z5^B=HC#Fr=N)H#ng(K{j_V$m1NP$%5O$6`10fWc~22n~-Vw#^fFEst7N5@r&%0bX< z1%OlFr*pia#`ImJTOJY`^e8rxyBuoydLi>T-RaiYPF>O4&4nOMV*a;r82m&S|pUV&+@bhU$_yu?*pm7Q z6p2i1e-&5$$|IO5LCDi{=wgi+>70t9CNl6J=72}{;5FHzvx~l)zJNJEAwuk;K!e0RJu`!Oj?7X}5HwUnfDnoX0YGn=Q{1*iJfB%$a z2?`*a$cy5j{&1X)0!mJFqC3O|y@{tc273A}=-bbhhY6LCx#fh_OJ#CLI28K`5-~Y! zy(P^SeVRvj$Aq8iINn7HSP$)Bz8G_^+#=)rgnK=Boa}rex=cUiE9cV<9{ApWedXGU z7V}Pm880;&b8F!;s&@EijrG+*DJE3;GYIifjgi&Hbjh4)Xe=O%_B6#+%Zwl#4-D%Z(>N!CC(m)D%pTXPSv!I;ZSHbr_d!4CxtpA)sxd>4Jhfub+y>mRU3 zUwk;m-oIZaU?Pd~>a+OP@Z26ntsk*WlI=j7~s!LhQ4Y&};$i*;+4~r&gP^e>W z>D&C1(V2CEr$$=~??gxfr@=*FC0C?8bkoJY$R76qF<_%m^(NksN^UviB5)tKFXD0t z>A;^ViS|-JSa=P-R{me?y?0oXS+_T=D2gEBj8deEjv`G&AP7hkl|iH^gchnuPXL9` zi-3R)P>~{tfb>8D0tvmVD7_{mbdVZCC)B{Z<2=uK&-uO?&inrPeb@P8ytpQJS@+tj z{MOoQt=%Sa2S1D)5Xf_pMvDr8n!MijNikP!XF;<=bXe)(x5cd!AV>5CB!QP+nryDj z&l*y_jjYbNY~N@I%F=3jWI!si$YmcN%C~>5tW+cS2RWM@I7PIv9iTQWuj7=_itgiV znh$T>^T}7tB6~M$#w>$!W80{~t+Y3p^-0_O-qy&fo)Y&ako)l}-d%~hV}>f8S(^t_ zts=QE$vY2Nb~_US+}+o{7v__H;^TN@e&n4$4e}^7)HZ#NE0wek$|SB-j=e!%X7BU! zmzrze0NJ_6J@Kk<)BQyUBSFI3d#r(P8I+e@uGty8seYwHQMZI%WQZ#>&g8rHr5Mpm zgF|(Solgq;>VXKa+Jc^r^AW;!kpS|dXC5VBtZEn%UZg|}Exmn@jcnFAT)Z_h=4m!a z^KU*f28!zsr8X(<8_)I8b96BB4>-ut1s^^KJT~)NDIvBu#Xr4fW_~l)8$2iAY`^Ra zV>P$u5|a7xXi$iUj{<5t>0^GtD0Q$Q7k;c4;W90KP|zYgY`AIpzSd$+Rc@By_ltd> zsb4jcQ|rzlG;`G5d`l`pDcc3tDFpAV@76;^8J7@JJZX72pM0qNa=dpEXlegxb5R4K znPuDza`9TF&SxX(6X#-zzu_<`$=1fwI1f!*$4bH)!T)OY_s);d)?J!6LPhU0rn@T7 zN+Qs=nU>i|kAbUxKE2kdOXyBs#XRNeP`TmymS* znjSvE_FUdz9Nfj%T=O9ZV5NpnruOZrdqeN3TP8gF4Qyk*WBzjgiTItkCsy=ZwqMoK zYaVHNSGx;0X~>Ajpo;nYgL%+g{NN1N;eD066DapNA8#&RkWHtL{joP!{6^mvzZq9M z6aDyS7AWpymk9g?AkQs2dK{9|1JaPwT(!S6(8o*hkiUxoX<(p_!x{N>g1Ayu|pN;-%;W}OlemN*KdGMdX zGbo_!q4yqaU3duCa59JfwMZMTU-JuUhk(%HO47Yg?8~7laL*@C3YQ#U3jKvl7H7kL z0ro2+;FlvG;*b6#bf^DtkG^WNaj%a7Qqn<%?aQ7RE7M;D`_c=&hTcxf?MwbD{hlGL z^-nJaA)f*8&%6-GIN58l?N3Z_# z)lAsGuDVkc+~Y%td~+U*3ZQe6KJHUe(DhG;FjKIF{l|qbKj@{GAj0A}wesM1rZ3>E zBb<@3#$?cbYkK?xxGJbGTZK$10fv>Bdw#Z_u|%R<=D^#*igm^IhUnJ{ zo|7$3vsF`WHGemvDkT93GjwIhuKzcclsvE)O-;@9zcC%gS$M|Nrla+k3cMr3-wW{C zks}1)af8b3Lna+9_d;qN!9*$?>4&l4euAd^2s}{4Ij+<4@X74cU~OUwv+V6I0PbiG z`4R#h{CO0LhuUh^5fz(1)2fq*g~1k>Y9O9pqv7W zu>R-4*|ZDQKYm_RSWiKsGr!H*dA1eST(8J>@%Y^d?Fe2Et=cC3ntSzcmj72w2DCXE ztjXQGElYa|#!CaW0a3 zX)-6VXJ#K&Szg>&DezHpK&fc$8jnKW5Q(IahaQ}JPi&^lDYtvz>;@l2KvqJ=5woSY zbMw*1CXLGrTg~6sG|hYY1Su-m-HKZ0E2bFrKiXpAA@-D2p(%XS`7Q(B

*(R?s)j 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
!@rfBlFEkIiFl%K(0`_E`efG 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 diff --git a/docs/images/container-diagram-1.png b/docs/images/container-diagram-1.png deleted file mode 100644 index 7ae653de8957ea0ef464ae603b0b5cba3b7ce215..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 386295 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
g@Ax0mBbv0kcvBB-|6m?DAf&Tdo-WGE5|c5T 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 hMGRN`Q+bwQf@R?F} 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�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 z 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 Z4pCQ@9y(#fr+EneDaxtgie*d#{s*geA2|R3 diff --git a/docs/images/dynamic-diagram-1.png b/docs/images/dynamic-diagram-1.png deleted file mode 100644 index a50cee470afcdcb3a4ce202cd28eece25820f353..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273565 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 z%0D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]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Ð(u? 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

fVx0*84x7~KD_ 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 WhccEDiFbE7ifFdB>ol55b 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 z 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%0A!u`t?ky#z>?W4KIaGrL#B!M3u^bedg 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

l`^>d4*MlfBG@{KlHU*7`pAQ;pC_61pI_0F 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%0AdqoH-h> zjUbYxey?6a@m963)b7cE>wC>8kzMTNEkn-YhDU!u@Jg@8Ed3jq*uV-_vhfo#wGp0y zA0)_9%=r{otfrN>=9X*|69jd7#)%PMkI~OjBgm~90tBkw)q^`WU5rR){fb}LZYM2q z=^+6hg7ta>$z?s({SkG_WJi#u4#A|&3GY4S9mmWRIoz4Are0^80R0dwHGd9*M7Cj0 zEqiocsbFZ1iXuVPAk$M7k2SGL(`y@YfCy+8{kJ3r?cxnq!Y2jV)QBVa8;Zj{Zl%J2 zzW1<&OkCmSP?IePJgNCJv-?YEuZ;1-Iv1pfk!8~_ap@BuPL)Mb%j5DIFINKOr1Epc zQgwT7W|&CcR7x&~n!2-|Ovz9gNvfYW>}IAm=PJ|YR<3DGZ>?MK1_R-MLhbZ@-Cj$S zzBrWd9_V@`rGbBD5=hkkI8|17dz-xn{@W3S7d@Wyp`o9Vd}WdL9bHIR5xR0p$i{b7 z8TrnTlu{@fh5O8K*LZu(fR)BI5O%r7%oNkaSJb;iw%};G?cwP#wsWc%3GX!+erSSJGLv4dGlTiXiWGqhZOxw; zex&Wc6_xPOsBpR*gh)br-?&5iI6rQ<;2~fC`X;qWKL;N>U?Azf=_M6AJAaFEqcGs(C4q_Vx8a$O83h8nfDqW%DM)6o*$$ff4N?&YacYJl2u_k2A zS|?-QxTfyaWo*Sa3x#eneuT|>aN3_ew*PR2pnSS@=AUEn$DjarNFPpXFD~^Jle`N~ zr-B0#LXd_rIJCwS6lEBJC+8(xpq36XLrq7(6bd<22qzS8j27-F3R}Ap4IWr2s{Hyv z9(s@!oEmeblCn)vfqCujt`Pcd5w6~leZ2xeM3j;l(1^eAAzJq_RF0Jj2*- zYLVagFCAQ|ZW{%8ZCCr`sIyw(16yj>>jBgQP$5Yw;9}!M!al3)Gs0!QdB`=JbpA|7 zDu8t~dFdwb8KQRF_b*H$==HdvpP2PaU)Fzl$zK=pPJ?=Ube)L+$FD~>*tuLx z{uzOa$vXqN{&>;@EzsDt;P&yDKl18-c@l#LWa`$=I8%qL2!3tU6=g%P6Yp98eMFZC z@E!Ww;Zg8`Xzp-^41=c}P~O=yM=JE&c>H-nMoHKVz}|K)EeuHB2T8^pB#Y^X((*4oPL0wm z*A5}D--SvF%D1HP!mlC8xvCRt zs-QI@;dW=HBv?V}v9B9dS;Fm(&Dq?AP*!?Z9q)=+{)^GdegiLYe^aI?b)gBvuN$?~ z0lD~i({P4%VdnF`Fzxep56s2$LJpkc%xrW;vI6%}?aunbHB@44f;asFwHZgIY3(Ar zzSe63d=-w>OlCWw8Yz!RF1t~?tlqXmpKD)jl`Ih}L!#r>3+@UA3vjPqA=2u@^pX5=j`Vd( z_1y$Kw^HXsUhd=;o+`05?O|tiCmL+d*7nQx(*Xt-=_iq?l^ zh#L6gTwHJZy48Z}CEIG^Cax?*s%i!Bc`W`MxDO83i-lPzy%sNsg|?h51hv{{ViJ?! z@YsmnX0DC!!-D%k`N;o%7Nz6AKs8g_#f$hQ8VAY3;mJ!(Q*Z$-*4=ZEiQkR%pgA;VEfvw{)q`iQHn4b%rg%t1$(1fI-uF;l_e5SJ2IMpfWEzjI*Q z`s~u=NXF9neb%iLds@^q>dP*;0f(s0*rxD_d(1RmzsMt;dvHYU~$m z^2d81M*6axt9HUTcu+*S;;-ftu~pEnS+h)i@WgBvO5pcV<{Y|0R2LrqiCX$AAPX&I z8nnYI?767XS=aK0>sj&9>+T%>7Gj{GMQbie8N5{g330*?isFf|IF1P~+=h$MeV}rFl4dmt?IEpkyF2Yg@^k77)5hDZr_Le`xmT zIY(%x6lzSUj@zcf$ch_1+WaOLgl*nBZi<{m1(xw#x|a-<_vDMi3^W2_8=en;COEdS zKePuq|FAPLmU^QBE?~BI4A6Ry#sz2V`nZv?1e^kck^FapuKeXbh(98CzZlNDG107? z8XFfn&+rh)D$)I{%0d?|pXeSPVJLY4%HgDaHN5jlWlo|jt2-6HwE zI{?s8n|VT~+6~AJA&=7>30ggd)f%PI2oF;KT{l%c!V}U3O_RC`<{7H;H>`&7=FM63gW!e}Th0(%lhuZ!^Y6 zZqJeUWFnVbayb}*vJ9WlFwfNcS&aBo=EwTe)%>PVv*iYg>#0GPW3ErldV-eKJ zUh16YdRe@1v>6|m+$K@y+E?cLVaV`#hJEFA%s$8XX?rV+;j&96dHhYMjk|U5rP}^S zX`1N-MDs}#{h&xhmN=|{VUZVU*rUqFiy5CBQ+t^)a%bda$&NwE6ecI1pfm|Y+Ahk^ ztbD+vK6Xip>tMEFY-ed8(dk_E`{UmQ|KQDE6 z4N(+VoqE|gLdjF|e!C~RyhNM6b7oi_^JHX3NnzLwvK}v<_5ZCi?R^U{e>@G%UBAwR zWk@BDmF@iI?7m|a(0i%LA?z|HU`C$z@Z(UrK>P+dbVA#LMGOBN%$cpazYnB&u&id0 zVGl3FX|G{tQ2GfSzx38RyhBd%7clgYD%Le%mo>-A?!9HBG>aJKc;JBf}2Ux$%^Y3mQBG&js_sYf7}1^r=aQG<^iqBEnW_mJ)AjlX*38 z2J8o8!G1fH+7jn4nfGr6LR<-MNOp7o&opOjR)N}nW1LK$O$UZIQ{aKBP^KuRd1xo( zmu?K5DVy7)VqpW4q+CmCSxMOO-24}qOoF;9Zu7^@xle{3SIwOs!UbJ!^IDqq%9{Pb zJo^H0FkxOitD& z8WEQE8inw$ebaF}910KG3K#sc##oi)N@w?cZH>egp@tb|D1P<-LWr{oP3 zHA>PK4g9&}7Z_Txv}2(g^Z=hg?iENMxIUDn$C0V~ooBY~l{2|$dP8Ghw~}|Ur;}QM zOGEpP!*$ye(|E&Ll%bBb0&Xa+DU5z~b_T6PQt8|AxA>wg^>28Gcdr6+Sa&w?PqBVI z)-uOeNG@|7?Ynh)*42?RMRH2qYg{a@(Nj$S8fCQmB+|7462w8$u1;Lt%cd(&^^^VO za?hSy7R5@x=O>m;_cvLC)^fg?_o=uQJFkc!^c{Xo)JjmcIYKlI0{=o+c!n;>^zr5$ zXVZ0V7Zs{M&v2cA#-;j)4*=4jY{)x8speq+oZjaIy))r9FJp$*zoj!|_-NU7p2c7o zdyt@-|6B>Dx__LPrhx8F8F-P+EaUAqi?L9sqcw!ekESi00pW3&^;ZwhC$-I)HM|<> zvguhz2`F@}K3nY!nSouvsBbR$jJ)s*$)(JE+P4Dj&SX8*)OmB`^9mNAymyQ&F|)yk z-8@|G4sjxp_z9{Db3eCamhB(1w{R)}FvyvOxk0bJS=$Ywt1$vce~YVJ=^xAqRqIiH zQUz2Gw48%ejy*GqkBS(%e~5QzKXi$UJKkn}kS5w@mLxf>7Q{tSm!*K6M6s5bYqt8H z)Q)SV)p|<{kkL7YoD>(gd{s$k9fJ#E6BPoqWzp~sX1QZWmtqfKJD@6(HiomUm_%JfGUIc68!p$o_&GbAx45NhXzb!QKY;u4^dGqes9xty6 zSbgDM@>P|9;mjJ|o*0WaGKpQzQgvJE?tXU3m2kCs{>gsI({@M&*5|YZI*bl@D?zaePdgmVU)d%9D*H;GX_pA|T9vi5k?!Ij;@_G`CP?=z^+gsS02KLlX@ zcwp2=lPskwzNz)n0rut*#w&wlm#kHtpjWGh%E&RhOF98mSl^<7znj1IOT>z>3HBW15*rMQ!u#!&B2e z0FdFlqZzNp*bsMdO|@(CE2XJ%vfixL8%NJbX*{{=lY4TK&ga|Hk8G%Qy+1#rSp~mMq zCEh}_M9__b>D}yTk>dxHmnk zMWqJ)Q-WUUiwiBBvaVR!D0a2*AsptkMGQ>=blyr7Bk>uQJm>{_*oguNnv2G7DA5$? zY>Q?tMUsTOp)8_kTSd&xKL$|A zRmY1*PS(WQ0yMC<3R*(?(KRC2^yJin_?c4A?s*;lr~2aOI?^V;JfZ~l>=E5%u#YWM z_IJo&&az!JI%-L6m$S-y4I(eCY9>(af%h3YQ{jrRp|kX5N#qSKeNnCkW-V^RvZo!j z;z?{r=jmTh^Ut`353vhuN%@KhfO3|64|4zRIqZz0AL=F zX!3~E$Ndhri}UevvC$dg@h$~SOx_O!)V`8I7RVwU3N2F+ z9|MVD?eWFwct0y)i9FqX*W=ZiFGz8Rz90IW=^n=EOh3;8;H5q@z)=1ao97ZF@)Z)N z=D@Y@81Wt%CV5V2mY&pF^l0H@zdQ44|Kc_ASC$XxHIH0r+j|~Cy6ZWwkx8!Sh7y`w zF1;A-bdP%?WuIZ0+N)vzbr6o$w)C<#+~z44AJ?@0a84JC(qAi`9PqVU?TfrjsiryL zeX{HNe{X-34%|=i74GLZ9>_m-zM-net?~|9-vE8H4x4Xuzu==xzcIN^<17b)c+tQ2W-0KIceAn4dB6ZEUG)lSf)!SPMu`lww$W+phvNva+xI{VXo46f0qzqh0;* zvPuU665Zas&O&&OujY1tL=*FUtt{rYQQ=o(!I0>>di-!?f?F zpXp|qD1kE5tG@M_>DI$y~3M#PTxN&G+ITka{6s`L~t6FT_XcQ@4PtAl3w(zfn2 zh;9w#JO105S&eEBO1jM}`Fccf{S&k5eGdpI(LauyL=C4N=pG<41Pb>@9Fjep1&w<( zKx(OdJ|<+#f1yTtS3!+MTfy3cD!r%(B?H`09TOF1*>W0=vEMVG`LkT1Wl5Ud1~&#ds)t_bfX|+_7-6cOQl$(Zzm69^T1d3$#Vt#HqHn z5wTU9w3s-4eWK+Wyv7Q8`}exsN^titk7S?>puSz9+Gf<-bHnW$Cx@a%(dFPZS5t3` ze*Is}ZT+_=(ELFKIM-@`JP@oJBBtL*>21DV!IlreQ;8;!1+`bk5?*Q%wI5Mhsnq;fR8^+d($tvfO6pAOjoUy8d) zWEeUbgyok3G^qOYqkf4O(T4VB#Ec-&g2$CU_)S}ZdV6%&`+$Ke(0DgTC7zigo#p<* zip152v+W_k*bO?2hPEJU*rY?f_lj3%3(5DL$Xh6h69k9l3pDZIDWi;D-Mw6ff_zti z?cqj`>q4*koA8=!;G&USnW%Eqf94OC-+LEGJzM7_k*NgT2GkLz;i|XHx(k@hSnw-q z144C%7I(9_E3l+gY6YyPR11|UgFqZh70Tf9kx!Kdnw)0Baa9O14IAwIZAq@IYN+9! zf`PrLy}g(EnkiVg<@034RbV`4I6?ObYlXZ}>1A3SRQX3;xlIGI#QGzQ{a75^AB$;C zK%WHeWyT;n=@vm0Y&W6@me8P1KvmGwRLgxTx`&&D$OTZUrQZl0driCZk?Upq=HQ*3 zs8l-v5b^Mri8@FIaa}P#<3qMxJrm2}5pY zeVulsYUSxj{bk2p48nM{d3`F( zoaZtYQ1$1={!yfAR?=`JQhfOt$n2a&Wmk52h5^U!VEjYfp+T+W&MtbF+KqARs5c6exu4-nVY49kD z#ZgTdn^N!AZlRbPM%jZkZbzXFg`rtYsAr8!^JMqe*p}O1^!If)#s2-|o5JK<=;_*1 zVL$AEe6;@FcN!5s0d;u2gwOCUwA(W+rt~}< zq2?-5%S0u(x3&r=N~cw9q1SH1yjYFsGSID}AGAw3xe|%ml`^R7B@-`hH0y=X=d?V! z`#|++Ma`!tpXe`q*5)0JZPB`+f#&3Cv!9sqFp1 zHk@Ssd`YKUgi{}CY&`)k!|tA+O-^;Q#o>nCwIz4b_O=gp4Nfm^;a}7&1?(bLy}RUZ zYrriaE#A0gh0e<%N500{3Fe{aNJ}`gSg=f-(viY)&uEnmzIzM2~&c6RlaMz zMlOj-mNUBxwvI_v(a`$F46~UtXXPTjF!wTv1-d{JN~+WyOXG~f;5<>m=GBEC69CAG z*{726UY$087arSxEcjNI}O=C|6;(fGy?rNqg%^#(yI;}n~o#}tZ z=OKCasY4G>O3wi|t~OJzQFwg&LX)2A!}F#1lMzGL>F=91B06Z??GLIh`-j+blpKnG zm^B~%>Ii}OCJ~*sHb^;Ht^4g3@yjuMD@x7UE`sm^g( zx|Fl(K4Qop`?#8cSFYt456u%!_Ykj34^ubTBu|nM??qjcv*t?w(;DBBcPMc++1`@p z8-f52lXt7}M|47YwJLcKez&ATJ%C{WwJLG44p`NNGD-=TIgPunz1nPV%!;|eb1>Go zl>t((;$p{xSEC5WpgAtnFzs;R;s>I@dsn9ZJzZ+n3OEm&#jsxIT*=`c>6;Ha z^Ory)*7SEu^lvAilqj@On_p3PX*mP7$*%BxYK0P|{T1CiBqdG`*iA4E-Jpj0`e=T@ z3tS`F^do-s#LfK+2@Bs~0#2;9B=E>Gjumv;^u_NuDc63?S`Csh6;FqX^t za?Ga4=>~);xS^wjZMWWqZ>VdD|@R`~(ZC&J8aC=7nUL)kcEJ&oIL3$dXl7W{= zcGd6^CAT!969Q``kK1jN)_km49+O3->!3@mz8GlCveaO=ew*G<+;@Z=vtBH+Kuuw2u&i!n!h1>Q*H&yfv?FOqVzv+&d>}v zH`;oByFq;}>SkvI4eY|U&&tuNnk5drzj}iZ&x_0Q^yvQY0iNF3`2RsL*?h=5#P^Qf z*MXF18!b-~M+G_{!x*bX@?E8lhQ=T%$IKm>KK#pW3Bn-|W3{x!FHEC@i~dEjKc2P| z^4?6Hkm=Q9hMsCwutkcy-Bnz;iT$@NK&Gls7%>fV029)({B(8u4qhH7@=zwn!B7ku@1!ZP@opMAMvMR=e`pot4aLUSlg@BvV>Bpp zn_m>dFl2afW;$33havmIvpgVr`PMBtMP-EmpSsGXzG8NNv&Rr$?t*DIwIw!E3ZN-~#!`vT!Fd!7$bvdRRSqDB zGKY2WpA;%pp%FopP}oql+9>LuS8wYDSgtCQa9ynDsal!>%Mp4%7W)Q1lLUfDia>la zLnnW;82@*XK+XzBz)kFg5{&i-FIuc{U9M-tAkx~v4$AK{xj~5f4tY}@3CShhTgfK_ z=$~o5(fd;I<$206h6r$dB!uwve1qxgqBQvF8=o>s>%fjP8X@dY28{MlVjCd^{gGn5lt^>Yf7i!n%(uf>o ztfl<0!-BbZcAH3#;j60@x;G*Oy|d2s;U`W=j;VR1x7E$Q_sTNB&OHq%Y>+GzHJwJh z7c|9or-6KenfeI6pr7v`UhZ0&LU{K3=d%`b<9)5QBA6D`ojh|~+GE`R8Ljs4qWM7h z2Cth1bKT5IXAK~IkI>6s-Hy-@fwcAbf;o0yDR@oa6vh9(dF^}D8hvxR`IcX!^GoKl zvZGCUnD}-`))p&t^i&?;U_+L)wz?C!5|Two;HQ5qP|+gC0Cd%P?ASX!#?Be2n?F0N z`DZa;P4_Z63EW!ltcz98@hTc{4naKd{EgQTj~h>Dv(`Yc7hTOIvm=e)&o zTL0ql&t)qVm;1g+4BHxdRATXMZxbogJzNo(c(39*r$M%tPag1CLl#`De8UA(ZsEik zP-8zKZOz9tAVE-9fhzHUAG4v>Rj|SIoyli)IEVT?E>t}u8DL8*b;YMOz1H0kKAYDi zfg{(yEu~LSsnk=LI*zCjx`pQAhqW-3!>Nz7PrL%)$dh(h+I3~dNcrB0MfPkklT+D7 z*V2uY68l^`e-Qo5$aDEEYxC!UY`_cA!pjmJsvuJX(r$Uj<0vi`tSpAo-6&d0@>AuE z>C>ph=VQ51vl~CU5ARUu$0{6|px|RtbYkh^jS-m52=xz~ZRJSl0Wmt(Cfe!Jaw6=% zEdZ^;o8>h)8Pne}=vP@5 zRzH_REOQrDvM9AbEA|Dlx_ly!G77!u}GA{acR947h30taelVQ@^&gY zVgkwvrtYgF{uW$MCKdl!!2-~UphVwZss#pV7LdCu7GPiyM(J`nWpda<4F2A7B^D(R zu!$(Z9MBCms7GifZjcx)y_mEC<0Fz*fz78=pMflq%bC2PTs*BNc|6S^%m6$5eA30o z5Rq71Vy@uoCw#%R{aA89IkVPXrH_XgbF!QCQ;o4tge5$p2y{kEx?ux8o766?%GUc# z*lUKbpgo<@htcT!2OPvXJYGLk znbBalVBN`Z_jr^2t*^pd&Y$(utoN-MB7n?IxW2f1EuC(#Ns_c3BfDW&ozW3$%cG=9p$0^w}x|r?gl7?WsrC zf#h-a{5}W4IReWSxAoMgKDS2wiBMI%W@k{N83~!!F*JaW7s%Z9#YK07HAN7~Z9= zCZwqUbc=77QDlXIgP5hH^dJdZRiWXfB!AKC@3fZW zCP_4UHffo#FwDrcTmvG`)bYccBCZ(~&Kf=(DA4N+)bAcvJJlfCcHI5Lr6t0+Gp!4K zs-s)#9A86mDfnQF!p{UezD9J-#WgR0cN=(U{5`mW9nF_@r@U%btwYRno~?f$$R6ND zku3j;8K5rPMfC*MQ>cBGEtCNh4`v&9BYH1t@=`ajNriazC5v0dBl@Qm1y`m;s0D?; zIAb>|&Y{Bb^yD7qU;aoo>PsY_mNkBn;X)POI6ye%jdC=&R-A|fa`6>^*zFBpMIFI8 zJ`u=A>D|9{dPdH7vq==*aAUS2(JqRL^;|w~JeG_962%ToR97cK8LsWGb^Iokjk8M? z!5t`21LL|=PQ+^S^q-oR0TMK|{J@D}QOJC#h;%H!94*!#Y1aXhHQGb^>Cj<_JvzM+bAtvP{%!c;n3q{lB+qN|8bcCncJD z94q5XfR3Co=ubCIrNefrfg>WYQeGgU_yXv$2Aq*f;0qt7&^<7}u; z;X}p2_=LS`M1T5Ase-yIcte(@w=B1+>sbw4aqrc!1VKzMEWwkZ= z6Oi7X#u?E4`6%nT%g|+X7MQ-RI9Ak-@59vAG|T!_AaRZRcAj^+IP;gX&98U%zP$`| zx~QJCYe@B2Ir2S=0%W^{#?lk!9qsieFR23{B?;}WA>yUF7)s69oUvaH=Ox(X#Q_B^& zBYRd{4!UE-S@bNN-#mZYJ5m2ahR8u5(bn;zPDlf+rN)tW(WlYncFG*AFx|H@_b>vy zsIhGB^PzsKrrjna3-dH$cx5c4;oF<(2L#dmA~wL2q-mTF{_UtXKsXngmpW9nh=%{J zsOT=#&xJ3aOhw|J0`WXT8kgC}Uk@+U3t{%&!YgZ+pN=c_k+}=_+tw3}WGXc1)VV)AJF_zI>;%qL{Gw7; ze1ntgyxHKzU*>h$@j0~&FBdfHKflrp@L$;A{OrHH9#gB;k;l<--GH34gDcJ%Tq;6l z*$cdXd*!@VCarwLRcqyQ{Qk#!;K-{j=R4ltzH)eP(?Hbdsz;=)RL|u!k|hQ3@N*u6 zf6!q^SYiLj=PO4z-C7Wf-?yqhnR+l;QQg`P9g?QY4P2+dzZqHon*(I3yzG-EJ{Df_ zNx@kZq~NS>Q!n8Qp|9ebvwPN}=i)}tok!WjA%BKw-hXk?z-L_#?{S_x_zZPW|B|8O z&4BM`+l~FEJ&{lggRH10ZcG*a|9c<=%_=m37I5iEJcjO?Z#kuMz*1{aOoPp?&A- z^Qmv2fgHd;mFBX)(W2t0Erd3^5z8@gmLR=6nAxqg^%Y8f;n|{N@_8Bl@mnc~ zXzRD@y&CmfIpC4|`;%_249TxI`~Bh2(3-%|YU+8|Q-^jAHCt7$bNJg2|6$DbNWraE zafef#~&E}=Fk3_zFaxJTdd%%Ota$OQrGrbZS<8U2V$v&)z5+;I8 zV|R7g6!0%=`j2Ibe(>f%Z8UsjPEmEA4a8tU_1cov{&6p@SA$Q@&{7RV!c8eg1MT%K zPXqU}R7DI;@s-`m82c6&o?o_6d#mTd+U*g^ZkIjcZ&AV(PkhA(~u5P30X})Rxv#0QuMbRHZ=n81%W=1Qo ztlR^8d{eSvRyE0^Sw7e~s^Oj9KHu)8dQz}30-T&<4H~upL|C1s5-**BQmPSg~am-+X;^PgM(8)N*Rf&J$}`p=5}$L;o?<@2BA^M9&Y zH=qao)%u1AzE=%^)tecg8wWVYR+fMBbdthsgA%IUfm9tFH@kfu|Ayymb{o|#G8`{5~28#Ol zo_pfJs@(TJQD!3-okeE>VX!qVSkv$pqXM*O-nX==<(06K)Y(L6Cem60Xv}`#ZJgpa zYs$mP(mvk8BGP{IT>y(H={Zy5#1^|(>@re!C?dXIAi9O~#{tBRANtIsGNu)sG^yK} z+o~c@WLg3#084~nW3RgPGDPW9o4%I-(C8l)0V?F=KBm*x5(wct(XJMdA75^E!I~{zL(XIh|%jt!7Io@l*lhjucJukY=^-IGdpiJ2~)*#5A^16C4G zYYu><+DBmFrFbnZrd*1bP*ieebV~{4GPs|g?&Q)a4XU9V8>I!*`wNu>npr*W{PMs4 z2fkW@8ZG9CfvR63p`ZgF5Ud>cQXi0+rU15TR?UCg8Whn7gHuQex?$-rKr**m_t<{J z47`}x+O4cQ??&syLwE*L$j(6@qYtOpC6)+UcSIc5ebL&>vM;c>+V@0W|V-K@Wr~HmwciukLQTz0pc#r*X*ImBO+2o*#_(tsN2LMay{UZ>~@+?faKu53I=l{9xi}_ z?fad3iVS<%zQepmL5J&gq6L6Q-7{)d#MlK2%_(Ua(TCj3m6|k+@A28&HTX~Hk$)stKB=lCkJ@qPOUv*zjpmU zt@67JPxsPv0xRTdfXL9(M=2_IZi6a;@y(*VkavR*FMI}zSIC&JJh=l9y!#qJ!8LTr z=UgL@vAbDgj%QcU9w~eqaU4vlBw0A|+&y;oc^v(>SqrHNmn z;j*tDFbltPhlJ-$ASvr3TXfytjKH69(H$YY)&R_V%6}zUjFr#moPTbjPFh}%cD^Ns z?Cdx8SeWlq^-kmd*UtNf#fRo2u^cqfI@|Q&;_N|9Wo2|$TE+~vFjGY{`>%fcSqPF9 z(I6vss&M#^X-3CRHicQlMw&8mo)4tec-dez#ue z{Dh(h;Y0_)i*^YbdHW2mkDh$&W&BavpQE~U36CprdOB{VrBK1X`&@FKQy;)cd69BH z=P^PgQc|6$Z1|8nw==tMX^ZQz#~N( zY=39lPo+ai6TLOjV0&M)-hNYR4v?JlcOKN9>~Wupgnr^FwC^2WNN8S<&77?t`H<`X zS`{cF0S*t%-zMI2sL}*}N`{7Iq)8Ybg2zsKzAy!=M>vwCVp3#E9(-T(?O}Z=Fa@Jz zcI%$X9|!hdj>F=^+*@;rwhdt2u^_W<$wn|28w%E6uuT{DCj$bLU!+|v=(JTR2Fn*K z>a<0W5R%ETudeERTe?1ukRLV({m5iuCeLPad&-7rU9ym=I;i<|0V#`i=CdhB_j9GA zBc&gP>ZB2&h~&IW%lx3IXL9+wC9_cOro|U_A462+8xC73WWH*l08>Q89_F!aPLARR zoc#>#V6MH=#g3zvA7jp1*M7d3aVl^qTB9jO_`OK-Q_08T!yQ2tt>y9aIym71RtHJ? z&A~Dr0L&LR7sL5KTQu_; zNT_7X>3^Yayi*GG10nBCvd{wU{A5i7a0njnx= zkguNSn0Kl8`rspB#?S!1zSk;kAHIspO?FA)lqcpubuVf5~H4W)%3APhFYOt zG;|kFckwiDYj3oU4q?UBYh%cG{~kgO@Us-k>|H3*MDI&W2_+`KX^Rz z*sdYnb2;ZGgwhYU-JB`xK0_fVk`wmeozuk=oCf2>-PZP`QRYJr8Eh==`O$oI_a*7^ zjvNhays^Th!?<(QzS$V3@oD63OjEd~_Z<{;S|I(DJh?!s?dWpf`cIbB`~@BUY(F^f2V8(Yk+ufanSXT9*6RSPjFrWF;*?QSA79+T`HsECzFKH8S-@t@*1NkD zd`ZIM`Z#J@sJD-c;kiNt?@LxcJfgDn1A_S}Ta$fT`XZF9EKp0$XO6MHOt_4k^V$AL znP5ZYGrxZOmuq$_5K^T3k@NC^MN}|9j`SM3%7sO(CcfIud zmY|;LiA0k~*|@SmVUfXvd0?vYBPAqDZ97L%Xb0D@fhz;#PJjq*v}Qxu``Gkt^0Voy zXV6KNNxm}izQnQ-Cq85#acR4dZ8O{Y_4!C!MMEf~MMF)0O5*yr%f-#XTOJ*x(SFeQ zRQZXO=;Op3PMK#h$8!Cu!RWU|0eqWXl{ryk8le|O3i8~3>PQ>go!xS830OLo{8!#Q z)M~udFLQPyixqCE|6U+N!KS^Zjzzf&v}S>;{vJ7SNZ6cr^|~z&0(5PsU?ZK>>g8nM zmfsV#3&+UTnnD+lyt8U=ayaVSX{Vd{L(KNeYY|)BN`az~INz;xZ*+uGZ%wHT7d7J$11gZRBgD)^Rs}qCYE_U|56j) z?p6g~F#~+4xZ6dKlk#>AJG6JMuJ+%z+o758m7m9&QblKFVU)*bTpa;m)KsP19Nej0 zMY)5z3bfBN5}jmAUtKuI%vDQ1pp1`Wd=XA6pWDObs*X4qiA$#wpWaaPxl~mXA>9QG z0&MT5=#j#?J@-w>!slzw&r^Wzmp7iAywfYtM7Q~B?CFD^!&eBbW&h|4p4v={;V}q) z@<>IhpC?CJNcY6-UX~xXke0D4U$kB}!FfHAdX;l2KeW4lTa zgZz>+aNL*AWGB_3;*SYm4+n8SUi}eB1+V;QuD3*{zFBi=jDcbDD~E_q=#QkLVW~st)713>$&zw6{cI{@_ntZtKLo*C?BWC$4OM!A#Q$2MSKN zQjP~!nzEsk%JoNuC;awll^7azC)>oelz&2{Ul;ytY}#W{Oo z1oX&Aq;ty)n%i^9`8<5OBbpCO;(cQ}v@ZskX6Qqhl*Qttz|CBYB$?`2M0NK)Hk0Pa zk)qYT*LAkCSgygE#5^>gNLz5Zn&|GMP9U5uQb$Q4t3BPN>_5Dp4rzbC{0x@R=<(u9@9vFLV&#}6kodv}+lg6A-mX465oL6j`Kq+c zgyvELTR{NXWkkGCqUPzm++}A(v)4TCFtqF9D6?DBg!a+0wf!?aA|^aa$8Y~r*l_AB zE#{y>uyF&x@>#?_1-k{l-sPgAcy9^1;_eV@@E4XGjB0wcS5jLjaI*Vk=$ zo=e*ae|W=ky!mBfdBE9qZbRxL*}T@9*=5)=ZAMlVO0?x}xfD8cDuh*VxXi*vS0t$i zm)J#&^D*5zndB@Yjn^B8`ktF2=3DZ^Nj)lStdN|P;*@sM8=9J)p3rUCq5MbT z#Iw2lm1BESOko{|Cas`Hd2fKI7N7dl!@W`GSWDezGkcA~Qv>a6f?C)(v=n!o8n7sA z6)D4+bql_ui^&muw5jiVYvI+NlEMtkiGD36BaQ1eQ`es|&=!czP7M1RNhcdA2+s>L z$?L`k8?kyEpHOeeIU!u8Leh9-p(Vy+wpIQI$J^)~%GjaEFf+xFLk7X0E+iITx!0(h zHXiter|`o=w`=C&UzxJ>kFqm!9mJITV;r-D4T2e_(V+wxko z4JK0QZjXp&`knEE|CFXzyeWlQ-+tIJ2uB1?6H8L&tAi{yDaj)CyFI~)dYLDmb5kmL zYv?PPV}xM)6Zi55$I`CfdmM7xbLdrGo}Yz0Po&l$Xcq|BF>Pc_Ru0yFNT;_|;)g+Y zfc)bDFHH_Z%v+uvq9cu#W_2MPjy#9gTjOvPQN*Kbhuu*E2B`ZT`3i$BDb8z&*RJ=-Mb&5P9e zG6q_~e8wG;$HvT0ynbiR$yx&H-ucXq?~Ecpx}n6)6SX)y*#}jpgcGlWTyr%!W9MUy zZ`mUKmPxkP2j5j4i^XM_>mtf^e%bM4eU|}2f?kw{h@&otTf3KF(mkoEfQz6$O>R38 zZK$v_m=t(>TXveHP}g-;!p;w&clk6Yo`h)N%XTqNwcfeG^SYrC*T9JB;let6Yk64$ zd_v`;C-r~m)_Jyg_$Aq2rv!+HlOHhH3uA{gMcrnsqElvo@3S>!2pbc1%0lIGNY*f* z1dBfHx~%m*ul04=-Oj5C-(2sGN{X``E}N0pqtPDO|KKSBob`1q3IlmJ6g@5mDBI&t z&D7@g;JJi^g}*dG7HskVqV6rDqTJhmaYfvUfik2BD&62nC?JS5Ll4~u2n;DhiBc*^ zBi#){Gjt3{sC0LSB0T~E5|aP#y`Se<=eN#!j%S~j|5w|!wlc%aec#{f`lJNO2}6R} zfIfbwa#Xjg*V?en$P?e3N&u#tqpa*Zf3rXTCn%tn2wIQ)cd}F>+ZFxXg!lrZ`Id83 zs0YTsEh0FI%x=^>-6xkn`ku2o^lfQ4Pi_7D8Yts59@1tW;_67wbK6-Gs7ysVF$YZt zE{JEnkvjJf9^)_5OP8J@0ic=b!8_)2 zAnzNRv_8E`mssZPz3|ty62F{zX000z*P^rTKV7v2HDJ~Z;R}%Q%c7TwQ0-8=aYf7! zMM9}5YE{f>LT%KAc13($OviUWy7>JNY2?(f1^j#CoiN0;R#t}n7&Ph&Q65XkwU+Jl(^E~>i zXAOwPT7RPQ;;nU08_`|f_OYsHkx!KipM2YC&zuq)Xny!CdpM^S`?bhR=j;w zi7f8y;QXb}HIl5b$?__7m{-ph3I+I-P8N{tX>jt66le1up&NLBxH68QHC0fFQ&_z9 zdRZy3Xd6p1^1V8=w72khl&#Dv_v#;~bRoig>89*=^te-K){Sf+mRJl^bQfp4|CB6lkIg@jSpjcoCJokl++I;j% z59LY`ljpI-q97fxJ>hjW5ut`0Jw1Gvmd&o_+h66tB6RVH>sd69Nk`IglzI~83V6)5 zB4rS=te+Gk&cMNR+vye&##-?^=eLB@gFLjUc)d2eHjy5qCgO@q#l|8Znf#Xm!I6)v z{en8;p#u3l=i_~dJj2gPTaO3K|4LqiLZzQlMpocLalI=E*jtI%dAc)3?vGX0E=VP6WYden8SfAhyWT1DCedfvzUC*kD$*S3p6L32h23fS@c;ZuPA{-N|csN#@ zDA1mPJ+?pMRjq>a`m__6jE97~h=qTsluI*N+JVs3wOLoAm@dwx=4f}X7yDQ1T~U4V zRBOV~(T55g+t+dd9QHl-oB-Fz34mCNRBSt#Vz0Rh*TikeSAHppI>a`wC?gyla%IULJFWDukg1C|X>L%NI;I4BQA@7G>nH^;gX?Ki z6%{%OE-BcjKVMS0+yoj7#-y6G8|&bc9pLko`-T1;VJeP1M=tCxpDh?nOU10V+2SL) z_M!PM%(6rA-ulnM&DSep3Ue&9IT7DO3@GIj$sIq)3fVOi??kZC7y1pmGj!6GFqWT_I7dI&@91PYFmJmr{Z1Z6eKcj0ymLNEs$)Srb*V9{N4fnut4ppPU;Q(indc7I1UtwKE=keSjCdw67$ei-nXa);8fl#Y zgeEtdeb^(#45l{+`yqR8IX0~S9^-)=so*YttWSviP&@(ImWz){Q6;tnL#}Ld;wTU1 zLDbhl9+Y@V*lP>Uh8PAr6|X?iyCx1&^!!AM;;ZY7n%4zQnGLd*hNme)4B*o6(hWc( zn2jk8a;XM0DgVT z)!~|!fzOBs0(lG3wHy}d@ z3@4QiAaJ_LUhV%_s#g^uGgPQkt~+8HdJi3fUM(>3)0WGr8vj=9B|-au-8pw~w$g4! zs1h%A@d_4BdkxCMb)MABm)q6BN_!5_WlR@GFco{RgYFCgs1Gz5Z#sQUW_&6YE$YMl z>g+MTedLy?drkNSt+HmxxA~NbeIJt~HdHrG;SWo~#!JRz^PR z4)Uo4cJm3fc+I;@ys{PIP3&-sG2Qid&>4X}da_YSNmdH9dMEEC>RA2H3qm>SZ>bBG zq`nVRH-c2OH9j>TXN7z5S_ePo9y7Ma4G~nu9T|3?AWlB4r!O40#zhHLm;UCVt}}sa zrFN0Pbru$wN)t@vw{cwOt+wM0{=Vs{NjZI+qv?8gTNJjGH$J>#X6yoml$O4wuZkU| zHkn?HvM#!nsb_lkgghnAhrPoHcCw3n9F0lgK^zr1^T1OH6=BPrZ2VDsA{El6_-BJz za@q2++|J{zlD$$UwX;M_KCeOW8ouK0Yf5F!LLcv!p(++ z=cKM;cJHsLzSpcqffuF#6KL*Bg1D+x!6+34FCBsinLS`FPgpWF@dF2QA1n8Q+=$Nb zGu4dHa0Z#b)F#YB>ioUAVgw#2Hc`OBa+O1#Hc`Azc?{yuAjI~1Mps=g2W_3{j`lCn zQg{v~o8y5NrZw&`7*l#$P@Jy5C^x#>!pVHtGg+R#8M&3eQp>uSQi|IcElQjKM22;O zx)BsBlGZ<^foN6T$NAK}^6P$8<$Gw<{6^6Z`$~(b$d8`09C?Z5DDMd21ih|>($iUs z=bR0iCc&k1mtfyke=kY;!y4E%thOg^%i|LS55z8d;)i`d+b1J6VPpS?ne|_yrTljg z*?Ii#iQtB86A4Gha9Td4{Kb{EwMN8I{NuPl+R!|a-EYCXz3u!v=-hAyx&4h9{4kG3 z6NTe+`v_ez5=_^e2D(3*9>#n2&Xl!(LZy_F$xX!Nh%_qsmo6dw%5{0uVc)|acXLaT zshSlgPUtVARE_A72YIqok6_WV3G`u!q6?r>HrvC-^c-%)*uTj{7w+*fJv_g4IT zlQxCLAagrZYB%EbqaH=%XT1uh!FVhEqRF(9{Q|29&@P{C6sjroM_yHz>~-pq`>V9p@R9jF;dZL7CTEl*6-bb z{yYJsBnxb6=BgEgi=^kIwEYgpbEUQqcT=sC+)|^Z_HA^pDU046B;0*>>4pG0A@Zf- z*>zk`%XENzikWHQ6l4$rb&Ye1P`hT9xrU~;6o0fjE+s^iqVpa&^l7`It)1GQBgljOmN8Fa)7RAso;tivPhE?pWGL8l^Qde#c9>IG8(HL&OZC{9^+yJmtLKR(rJwA!!B3K3TZVC zWU#$8B?vQ3r5r_40soNc|wwqs){JDcf|@ z!B^zgnNA9EKF6988@|$d3D{Dr+22u?oeo)0H&yxCM>AZT6*e{rJ)J z$_ySf;oS#uYQCxo!)5~wbs*WpmbwuTT$9B^OETOj?%l+K0RVmnUyEDe(8csYn z?8&O3Z$sdG;0ET)cmPfl7Ob2*1(nyiBj&1a-dZ1kC?I~=98(tvxU^8yiq;LhQV@9O zfI>vQo0k|vb4_qjZajOqNNTCS(Wr2S58e(?RJ-@rZXt7~0K!{80n$UQM0q-OC@FL} zFz6U<1OXyJ`7%&bj1J+7aK6qh_tM)YT+P&8MirSQ< z>J$!Da)J4SKKF&hTtD+atkQlp8mEm1wOQA*A1akaXe({mf}mPn5VoOw^SK~>4&PQ6 zAU>2n67 zgK`<22~lsMRysZN4)Br`1~;VU^%B4_& ze%-rrBp--3U+-_wl36g({s0f#Hg-DWp>#aZOHq;gwCERAXf1Fr3(u4NWfdGibUZjo z{lxR*1R%-YX>4cPaAAsTnU|b$apM5*#q-@3w&a!pN3(1Y#x82-D_9l7IJlu4S?OLg z&jHT?p;OSmQ52b(G%Dn}VI$=7BQBtnCR|M+H;4cAkmAFU344e|)d4f^+OIsCQEVx4 zUY+EV9CQ1b25Z@}vm9q~`jV2;+sU>L80i{wYUX0M?d(>*q3Yf)Q!Cnoo4<9~f|HAV zNp8DX*_>|q{>&4u&}E&wrOx5I3SC1kqxx;LY|aKhJe21Y6Zkr=*qhEzk2ef!lfR4X zEt8@dAN28%+S4w?9%6s(aA0dPKmn#q;cN5UETYq(!teCXL9O9oU3oNHPJKpRCnN17 zF90`Jv1+{A&9-}2>cIV=Xvd51ph%5TT$S>VH*p2T==V@s%ZdqD_TNj4fX8E%F6|x~ z`k8jX9E~HXB$SZpPJEuXQ7HNxF!5g$L-b{RDbyySokuiff1x&#yh+OHL+Dg)(T625 zX=%Q_*5^Y+ssK3343D8_)1V*Fa!B}ek;f}fqd2!DAAWhfOurs%Telh7gfN=BL64xO z(WPiDLpXH(I1l}7F+eY9KabXU+e5%U^EizNWjz>=ZT%4o$=`^h#K65 zVuMGjWCJ97$lca!(v_U)4MV6yHi;su4v0ujZIa-!M zZz0N8fgM)t1xaQM@M-UeuLx`g4Vi4Rnq+tl_cwSP>Do5axZIBP=K4)criVxMMbYDH zWDP!X!eqp)tES0*=~#!(37yG6ct3~l{{`=lh46k8`eeJA+KJ)!kSH-+tq%aHuw*t& zf2202I%8D$J^G?7!B+>|Pl5jBAuPY+lk7>vIjg1a_`NVqf@-ty67l&vM(U1Z2Ch1v zp5`Vw4a%AOl55^asA2@7iHBSVbqy<&8&*j|V24=F#Hs0ah%pSXc%g$UV~ zD(c!JRD?Fv@(CCGztgvI2GB8yM}_?ahhed=2;UR9E4@MP9^aWz5`|iia<2N@@<}eh zzt%>*;MQ>}!B%44Jzpos{6#B>&YA#6knIScLlzbX&hsA7=W5ep40haHlJm159(6Q? z*{Pjd8f{4gN2A1@?J+qD1(9TFErZYVxdd}7O_(+{eQOzg4PtM#q)JLg@cO=B`V?<*m z5>x2d<=T)QF{5^)yj+{Q!S#I$^|Qq}UYvC2D}vI~1=T!=ezZuM_SXeq2U1l~3ItUP zuS4{gAZ>uL{-|*E^kl+AdqZn&sZodUS`?m%Y--^r4%`~UPq_y+b*+muWP*zWZKtX# ziTmj&`yb;0aB}v939QdL@ZmRqoFqjc{5ebpEIxjqrQ1kVhqzoo9puebTxU2bbcNOiagZZ{2slHqm6AP^@_k$DN^(AN zEr*CE(o_!X3)Z5ZBy=Ym=9(a@aotja^;vcP5SNwYjP4FBcGIsdOCsBGDWe(H z`7SHpHKt8y6RlmIUSYTBPrB2ayv8qZUJnP)9xPQ4hQ5_!yjoNC>qU~NoT32jB<^KE z)?JU;@m#JhUQTEp1cEQcYpFT*`9pCRZPtt?cZ7eP>Y33}=IQZ;F8uc6Dis`jVip#Z zcUmeJ4tt&2HT74c^LB)l?wK~rS#BQDA2ID?!i10BilI-zhlfsf%IwGs2tcVTzs&>% zvIQ}$Um6>RFZ62Xvj$v`;+F5V)M0XZ$<|g7A~4B{AN;zn2pZeZBhKyYH=fNv^~FqE zVb&>wear?85X{X`U7x`eWj&eVRq1lUy*G z!(H`m=ksKNP%FTmogrFsm*3I|2gPV#*+4G~*gv68^j19KW@%M_5F_+ns^ziV-Y%E% z&`A__FHC~CLwLP%t!3Uxu&?4VE!*x1_r#u>vWUiVeIZPIp_SX1)URS6544uUHXtON zzb5cs=j)4lzplj{XxlGWSQ{oHq9~_kmuUi8InLV3MtiG9=fIGZ$v+9G-?Yl7u@l;W zO1lq=JQ@|O29d<6Q8Da)E6W79R9Ht+h)aPa@Q&?gWx53CyZiOTv`pRpg7S;Dye=<) zgW_uK*j%97dt@JLQ~&fWA2^vvXpl$j;6}O&pkprH7DkW=;aWXNOv2&5OKI&18r`uwim9R= zRzyELpBH&X!*Uo$!!6r*O5>7UPsp7OeobjuwStpsiGn4+7%dnaVA%Ed_S@M&2G<o3Sl6ChN85$gYh9eLlT3LkUL~W-> z#5|ZS-Xugo@y$j11KN+-YVVl5#=%N=z#doP=m`$Xv8QRL_`C5dcLQ_DR$aqut?JsT zn9802khSo&W8QPH)9^wn*PkcwVP*gTX8kTwTVr1k`r|C^Oajf)VM-~FCm#Rmm(V=t z1i+DOq~;Z2A{)l{9ePep%p~&d&a6>FJ@&5PM69y^-WtLSCI`j(MG8HeXN+az=83jz z{!S5;O7Ra`l0d%d5|I89S4|Y@JxqM-&>lvYEfqxZNeF%N2wYw7F8Q5aw6gfx^hWw^ zQPxY=r5N(>^jk@ikL2LS?K<)v2S9Dx4T^(IZd9EhfBodK#)29ky*iiS4-56j){9A{ zXQxMro+pPo-`=0z;Q)4o2cWLpTVy4;a-!TqnW37q$Bi?jXc8=2V?M-n!Epm(f;1_;pd#ut$}^#INjPEbk~ny5t)zX7rR z5eCBP^zgrvY>t(P13I>>j0{0thd`xau74zP6Vu?(H%m4tYs40Fg-rFN6^G;DYR>JR zM#4uxdi}-?42#xwnU7w8N~BR;Qg}|@_-r`UPe6cI!E5=_`e3mLj zR7~f40Gw&dGqQiSd3N%IAzTL6LMcK zJ!1M+zSm&HINUE}IaBK_7iUU;3BxkXOQ#$zy9F(+31{Q8VeJ~$Uj$cu*T|p8<=CeS zO?JH!U`p*PwUA`xHg|Xe)|<7SSSNVPtQX-+K=>o;zQZFWSM4j%mgeYI3cn^1UPs45294mGCS z+FpLS{&3tv5r-iwebDHz!f}M-|ETL2kw~*5v=K$pdV()-L?KmlCXm~_FU9tZ z<7pEJK1P9}^oGy8_a2f9k}(plroN^BXrUy5pYBqzyNMS$$GMxiZq#-S?Bg;8jM}h!Ce@uVMW3Y$feU%&Y3cr_`pK#SZMA zs5`qk!!^2gMhTyHbsy^1aTBwIA^REB(=$@Zd?T7t<$fxmf>lNjS(yvssYNy7&{ ziRviGtEcPa!UZ_C6gbe0h_PtWpMVZ9A~mg~WO{4DqnY6Jv0nStOQOqQZv0A}^Jwht z>^?9Yn6=Dx%-z6T9zRamf{Y7}c8iWIe|!*qzo!ySP<1HAvd*CIM4UQ$3a=#HvU^^<$wJ=u?TM(J*c``_pW?%5nl;P>g~oIkypd$ zI1a5Q&Aa6Q5j;#oB>w6212(e2a|Bgq7&~>VOfw-VLGN~t9*k%qnbRaR{`JRQZYH!r zlSLKi>#ANch^-wO*-`Ts$aWZ{{P$Z3_D13i(H%^c4Vs7BfO>vRo{;o3vXLA*>QQR| zm7M$|2`82YCym1s+8{c14)MiPWL}7_Uw}SK0LK{5Bbom=E2wvl2OdH%_?%Q_a>%g$ z)rj010hb@zD3#psyFch{;zhHI4`LeH}kSAZSg1)x;l@U2!&J=^C}4cru+(Ln5K$P4rt z*zdY|L5o6}n^V>HMm@4C!hgSL_jAZ6G@N#z+s7YJ1_{%3E3KaYqHza0jvmE_Gi2^| zSEe8?;1ysr?h*bj8oD=*WBc>_eGY>@U7;7cU`%^ZYVaSoPod03=;Ks;(*$YwoHIEe zG=N`Y0x}v&ai??t>u08YBKinct!x1GJm3Eg+7J1GOgP>7m7PDJY2K&xiv01hbdxm} zTv+EmK`La{i?g6VUXR=8Z$?&yobdE`M>$g+#UK;Pnb(Le`gnSX?*XWz_Ta8Q;lCzh zOG^UixP=OBJ7%chCO)@>oGkekEjzAL*4+Q#9Wp0$(HlwrC(z{F8$oenEGgL0Mph@v zb8rAt3OSXBuIl>_VR4TfG6X^Bat3N3LLtiJ7qIv}1ogo2sDX@@Y5!oj1jIFok<#Bs z4Kg}{eBwlajV@3}BZCn&%;#N!>B`YSEj1cIH>pmjKjN?F-W<|HmyGAhuGS6^>c-hO zo{V+_j{#(3tgxS(b;6JK0D&5&;n5KExxdM$zwfqf#y}aHrXWl8-MWC z;s0_?{+H)bl+wBLdaFNbV1N8V|J%;gdC%Ls=-NyDEC20({?R3afx-=hgt0#V55M8x zAKIG-m+u*KT(}bp+?@aAclqBx(f{$|#a0P{0JS%i8U{UIZ?nNOwtdAHe<4=I3$KT>LA3quR=(fZB=(FIHEC{Lu zjL@Ye;1u8m0JXQyt5r;oNf4L7vTy{t4Z47#DPyk)G$e-P+dE~WNDqMbr3XmU%_+di zIRv(YY!PkgOaJUE@x9FV>U^S)02paI`E%Wt;@g~lj1OFK?0vkWumcPeaUesuwE{K7 zJhrR;wrN`ID;N~`0Bn69j0C(;9X51#)9wLo))@2(2yk#ufdAa|q|m+ws>>2^z~W_X z00($pB6K+LfkFr|`CTo3fz4glwpuO*POQ;nXt7r;_-lQEnIA?hnwm8LF=iPyNvJE=$=t825_wp5RHRkH6`UMQ|ZZxnmvc?^(ANl3NDEW5FVYk*1j zw&YD_*Utsm+D=X|WkAZyJOZ77J|OyW;*d6S9?|GYd-|?Zhnc)iGq^4ImMPS*W|;`_ z#s!6Ch?b38R=Ila?)@r%Wp~ZA*A_;Zg!XI-@5*72O#q>(OzT z0PX?{3ZV3xzs`vDCyGy!i17IU^?#7RMV1kL{Q3Fn67Wk`94R<5<`leVN4;)Lzi0z9 zW8;<&)*(7S1xcGyZ$LJ*2_U(+eZ{SQx;m=#xBCbiQH%O7_b9bEBfxZ-09rzy$KwP4 z8kmzfx?XXkioi;aTLOqMFHjT;02^s?rpNV}?#;-Cu_J%veY_xExaE$^_%i`1dyg2z z6VkQv;)*{Tm;e1?-MjHdn#=g=buqtRX!C(|2L-}!#M>Gcc1{ry2j4(xeHFo9g8g#Z z5^Tv$0Od6~1=i^}kU96Iu(XvL^HBYBG)$3|7E8zq;6I!CE91i=7?S97>TH+TeqWvj zOxF@fFXap3s?vV9SzS>-H%w?#{Bb)_(QOL+8q6%l9M5e~1WyC%Dy5@i)TA5~wrrGQ z=m`XN<@@Y0f_9v2P0=a4pkpjYstU5N8@NnhVs=vHIcmYtK8EM57EK{?MAN?D`A3He z9f1a$aTj{a;nHt}gbUcev@cQPEaj%t%Yfxvhova4ME+7Uj1$NA44pCf#yMO)3mpPE ziEI+2D_K;-rgaxyF*x*t{%(_~P#Yj20FPv+I(2frKqja@NWllvj#*7INZ4`|B zk|&${0wCP+peA}G)Est4f`=|2O$u<%kB@8D5|^BSFQvjZFmof*p~7`mWyj2qzs7 z()_^WhxUOp|5$>fNAZ@GVU*Hfhyng9WVD-WTRH(x;OdiseXwZpgDbZJ2e^xMTQJWF z4`B_2rzndskClu(Q=5@_>%Ul{#MSr7Z(z*G@Ma`+eWFL*8w^DZGe(b_xz&%IGlU@5 zsE!`>JH0VG!-|I$DNf0UbawffWN)#ujN5>YO9b(Tvz}SM_r_8S9_EY|n)Cn%rWJ6% z)7C9j&A8|Zdv)xlj5={C9nx?0;~M0>C(|V^Wx|0>8y|z)+lF@FX=R<) zMq{;Za}d7&Sfy@Kcr7xqk8fF|C!{t1Fp$)wxH2xMnNYI>w{4pAC_wcAQ|8hc*uYDP z4JJ9J>>KN6DAywQlJDAYx8fe-OQNWdjMBMw;ibQAmmO;ApurOSCEM@2kOK*SEBD~% zfXvdF-g^SzSY5n58@D;R!T{?sV^#BpoxqyexJ7EOMqBFy{g&cHz6QP_X!@ zJ&l2VT5mi%-4mWMVq*k^UCY;TaIC)=%Gcl!=0V>`f}6fyACqHAHoHiGtx=CUJOU;P z)WT=7#7$6x+5utN&Rj|W?Fyugka~M1fMfW^TuXmXIiU@B6qJ$6=UFOSuA@97rIP-- zPOUC)oF%BBSAf0V*54D<%IFYvT0_6(Gn%=ViM)dNO2R%FJDKW;?t$uM&F7l~;_(v4 zpqk}Mm#|L7+1XxoY@G-3>`1L~O{y0R1jaZneM!dPBhQ)iKIfHR|XWIL-4`Qn5`@gL+mZ`Sd$?lXGZFwbR6j$!03FbmG9{34p25h$t^w0*vaav_#JrVjRH(YpQC&DO2e16vQOBh6qP_iKUWhSn2~EXNvP`_)j{w zw4yI|g023Ly2W-glBFlLhGRF6cvpi_ci>HJvnHB5GR^`3zk9;5Cdy|#?}hYZo5W8X z=vj=+vR`qJWPa!T6zoG%TkK<{)ct))`}4ITO2J-P8hXk{m3e1lx@dw2L^r0VYlPO` zk(zY)E(2J$eEu5IAY(I(x6$jgR&@$SXnDQ;sUrRL5lXFHozS<`_T3XQ-_`P)pav@Q zP*535ioeGEG?AzG*I0R?#d;8sxsCYiF}@G`Dfb+FW7T-&`6my}*GY(Glj>5FN|P+r z?VcsPE{z)R!hPsaRI-+&k6ie;BAVbDkUeVn+E!SzX8;OCXuJ2xZ?tjAU}tF$4@}Lv z47%~8)|#e+DKQtK=HD$zXMmIKv+%9~-WYfp`8}K*WXC!~6q!1B7)@X-mBl9URBnB?n2wUS6VdC0A*IqsMSk2#D5D0QjY0b}5^ooY`7)p_M7X%JXh~ zj%#8UoE`Uc+3&wIIIE(dl1lca;zNs=Y1Dxlyx20StDHEf_2wl;`{4dp9lTXHEmMnW z_Q-gSGnPL?ex>h8gZdj_Xy-S20^k_)?_S9?u?AVB{*PU?0rMh;fb!%o zmaHJ8TTd`q#LvN@>&Zc|<*O?s7PIp7Z3NWQ(pscIa4SH?X*}gI zmGT2;ZZ4tVSd|{-iM$J|5Oi>5*~yZVdY8Yq5?);Ux=T$fOMmo(BlzJxXj1yG-qc__aC5qa}~ zG7p|m{8SVsgX=~$L=Y>4`n9sc6XZqK`sC4@zfIKe5Ve&!%=+^khXc{2@ms15FUB3~}-Ag5|IU+e51Zyq}rHppfz z2a=lSr8a1@4UatrK4s0n?Lxcx4f}epJEKD-!PG0*Ep;8! zoJD22ANsW+Hq09EeAvak?9JvC0PdG^`=vV-^``gKv&g!$TRb*mUD=>ZuaW;y)K`{5 zBsFL1o-J1`awa0RuOu2vzF(oaQ!}_2VDx8d|K9+{lHuPE5sqQGn_@Zk)8M%-MgGr{;4a*D!=4S?XVD&R3e)p&igPOr7++_A?(PNBx_4dIb} z-^|du(X7G4!@RR?A9Esu1xqsOAZ&3le`1y0^vmQpWj60c?>J6GY=Ovp&|fFNuM%nQ zbbBCuRz@>PF9wuqADJ+D-4Kd7JyaH=&Q}aTF-i|7F<*4(@LVD(=@>x}d!?VG1k<&`y+fR`>>FT zl@hr;9HU=w+OXHA-B~VTuMk)|Fdi+fX=2?dOI8C?7_`Tvv<$sYoE^asHzATumaZhZ z`6QLmFeYrWr<%}g@&bq~@>lwBS0=L02RUncY%RXk8n7Or6z&A^5D+rxLm0N%pz8IBg7WX7(Rths;N#k za#f9gzGHK=p>f!&%&@dSL~DM;ZP#EOiwzNMT&d$bpGZ2Nmm;aLmdXL^7XhW4qm^xx zK{b1fd||w?&DXF8Chz;dx#}9ZyLbwtGb1?+3J+{+hm$&LH~MD;$5FF1V{u|wn)8Vp z#e5D*H?uByY{8VH%>rAp%0t#znO1>SF$$+y{yFYDDXe=%cP=~^$n)*QVKXOyi`1-P zqOj@L=~~Oy~%S;LnL2%TE zDZUAA-%{*AT`P!pdT!cy&_SeD+A(KO_p~N8MSUG;?mJ}Dsyo>WhUF-ew3=e}MOx=# z<<3_g&W~oCaXPyX9!9i;{aIV~9~AO{FrmVmYe|<}$Z6zLy?$vKBIZIO7N`em!SZTd zDAvP+pkvb#Sgu$Sz9Z<$@MPBj*`GC>1FZ`om!I0o|6&*z0$CyYjs<)qU62eBxX9x8 z74TPBT)6^P^k37x3Fk2%2Zn`iNUKz^{t<}qYnX-y!kTz=`bNUTB-qTDSYM>Fd8~l$ zS>2q~3<7}=gVjgTlMf1H$M1deH3C`-E?&SC9jPXa7v^5ji@7OtqB*G+(6oJdd{`L+ zbIRym(DqQxnbg#6LJei>z+;d~*pkmzTzJ}T6+XvhdEft_Ewkzbfm=gstq4obU7WeW z6O)sDUv^jt(+sh5JrA+SRx@cxvxpmE0{kzxi#F-?)Uc98=Ut?i%dDHl5YSw%9GgLw zjMnkE!BIsv2kt&0PHG862#MQa1mXJ5b&POK8Un?|0>${G>)AsB#NYFTb{$nGxUI1O z8gZuHUHR_H!MPjQ1``?CtXtL$^7qLP8=~T($oppYt@QG|K|X*I$E3#USaGT%NB)Cn zp9y!s@-^ku@hW9)ViW1ktmZCnoV)zyu0@MXubow2K;{rt)b^e4lS6fFP*=3f{lu!J z@+AfynYE|?1k)C4a_NR=1B_DB=U1QC=@9m^V5+PcsZe+9Wif)>tVCT)HaXUNs@^n# z`g&-dA-Yo1($$}O^Y@6ZfEsqf9dKHYX1OY4iw=u8oq-bNfjfbq>3pBRdIrSQ6kdo! zB=}36;DFv?{aFo9w)|cr0Gb6VF>8zDEj~1H+aT$11>+QiE+7h^KRy*SPv;B3rU*Oj zOA+W~gdu^diM;{*+r56H&lh`?%T~&P`LIlLQnct2CM)`D6L~;A5FhB`w~>SdcA=(k z(*TGy*MYEpsw9rS7$G0iYzbf>XId?BoQT}`vM?#`Y2J0ga=GD{q=GE@BFQGqFXe9-L~u-;wjpA?xvEm-NlKz&%VqQLTB9{se%Hsvz!=IlNKKMt z2!G_E? z)^Pk}22I`{bCLedPIT7;F5^VGY3c6iCLjs~d@V7Z-SNCV2V38JfY#J3@z;$L=ddC^ z)5GGVVa8W(;KdXNlbMDoiE`{}SDATDTd#2lykMK%&bk?Gyfmz)TuG2thzwIH< z36d|*6Mzaxb?x5in8b&%S}75YyZ4sHZGXPXqrK0(JB0btQ)eM=U653t5H0Qm1R8u( zw$%ojHU6GYST5Q$+`n(1)TQfp>iHpnOTsn^#<2a1iAd-J2<*uRZaiIwRqnbUCXjpA z5iIVL%Svsbv^{{)w`m#1%1Sc5$!+{lwx>Lr;*@8ydt3{uM}8DVVB?}GO2|XxFr3?< zrL!IK2F7GIFSAqo>2F4P>df0>GH?x88Xd zMU6D^|K}E+*bR9s3Sh{x zU@zp_qctE2;p_JGy78<4ZTCwloau3Ou7VkHA5Cb^(&m(_DX1HX_n2z?`ROPVf%u~% zyEMx3Db@*{qt*fQ1wkh}zLstmFMz}|IFwRwVg#H)HbQDdpDO-h@`O4k->%M!=k1BR zC#^`%`3?#lzAfP!dl10P4RnxrLdGrMLD*qSK=CnI#^J2T##O<=QHRlwj{(!M1f0WI zrfu~4Z|n3@!4lL-hlUEl5a3D^=_K9{fBq``?KeteXCwaR5V#2ynzDyG0iNqnR1vA; z!1n7TiR|yYZ}eFnJS&PSJcmcFRdh}zP`mN$&?}&@Vgq8LIMRq)uCNedvw#fWqRuH{ z?L(s%2bVDibz~sceol4B)mTlKTccunozF$E{QdmXl@yS9SFU;m^`!OiZTSLkdena0 zA*`f>PO~?w*S3mPZS9{>B94?Gy8R45Ns}JwZ;0cfJCT=4*kMxj06^NiKZ;qq#zjtK zweXofhmB~^>1QE5Nf=VPvxuF|I5KVZ?N#nv6!$`J2A>_GuNoZ3)R=wsfoGM?)}YE* zn#DeLO&yT7iNvE4Nocv?rAwZe;Ihs-qme%&B3vxt01t|W^O&A#Yw66o)sZ!n|w zIS?dg^lwv^gNo+Amvy21yaQ7e_F)jk(&vdi?SjjSi#e#*YCiM>PvSVh)R3VRbCnw; z!Z*`cW}&o>Rj%E$gu4OcQQOYoCaTC9L51`S%_C06)F=#{oF0@E?}3i4qQV*Q5VAGFT^t)q80ee1B9ZlXh>N@xgXW^ zAHi2d)VolT?eX;hUwe^4yD9rd^u5;TLbt6kU!HE&+Q*pt(P9cr)|{tz!n{?Eut^ow zZik!Nx!7qeBMc^wdjJLCZg7!5v9${qiPXQQ^ZJSHG<+b8=dZ}iGTbkdg`CT(M}bjO z%LAlmc-b=^X`;?2I$nzU3bJ6*HJkO6pyz>=aDmdwQ3hOmU+%UZ=eYxSatX32XZvU{ zWK1j%LYk4Y4U-T0)CTTDvoS9TcSHA^>x zTC;AN27F}(`l8e`FwU2hDoBZQm7;CHqxUtwVkvO*bV*b`J4o|*OmxT@hQnp3eTZtHFm>nobj1`#?&ty4i4B zImTOYz7Fb4p*OiV7cO(s|H$A8QGQ#tVi;re*;3o`aS2sxb$sX^__iW zGfS;D+Z`supFx{((MdaIjk~TzyqH0nhjvMB006U9LoY<0fbBQIN(|m91^3v=$@94CH=m#x7nSAKW_y%Fq;{4L)vI zzbKf|`8|}d7l<-IvyEBZLsN|Gnr>qV6%dV$rfdcA#IaE?myJG4sid~flnw6pC~KuK z`LPqC^!aS`>(fiL>ZGsrQ)l=)EK)?r9Blw6yZ1Y~(#vG-XlG?`G2eDhN_BsLR}arx zAtUcmMHwPT=Lv#}W8DA_Z0&(L`7wK)hGqB3s?ZWIv0yOb+VQ5>8OTGi-sK!Rbheeh z<^@v6Mt-^yJa6RPh*$X~!W@JGUjujKX^~7_)>} z3&`zaph((Zn{#s&fLrTzMYj!zXfTFFs`T;zN4wGKGh9RLxS04oqVc4uL{Y8DGSJUn zI!fif&mOStWQirq>Y%?w1s;tLGUt)V!@Rj^{syr&*KqV)j=}AB2cTp-9%L(xeVfC` zQ{qn-^iv>tjw5&^#X}IgrrK4Wk0lczZ71aAk(7Ijj*>PSAy2V92r^<^6Gw;0OR0Iy z6+-l4ME5V!9@a@xp}!mfH6^}+qnaH<8I4bn_h5I;0%ti^(_ltMoQtT+DMZqST~`gU zxEiz*;c=}r>ZTgOT_QUD0qOyveph9Y-Kec(ZxctRfmV_IX*Fxn3d$&?ddAfvy6Qg0 zVcbS7-o~IvG2}~;JQ3rf>~bKI3Xv7d&y!z#ns0y^+ijyC;}H~^L3eSv=mWlbl*XvM z)cN~nIB=KFZSUxecZd`$`;pcj&7tf&MtV}q3oXRM_v1LT zQP!ePMVs6CCvgvVo&5z<|H-c6c;ihzX-Q9B-T+Wz69ASnzfgMQIr-5nmG3WFr!HDKDJ*wKhG%}Lq?k~gR&CpO zeR->X$7ba5(BYzXSEbC@Z?9^Ny>s2aR<+GSVl{k-*o%TX6Jn`o8hb zXUj(THq~VU&Ah7H`N@a=a`Ow*h{{W3q(doRsOwTEwesQ?b1B$$Z@(hO$Ad&ds@RyA zhxmAvjn@i!JIvhZ=Oe-&l4RKa!R&aBunY*Hy6E{XHE8L|U|NU0BsMr5)2>bv0G;N~ zp`*@lMH8IamRd#(y)j2!Bp(Q;>*gn- z)XW0Vk>G4fE##CtK=<$>!HBe2=u?X%hxe+J(nN5O$nS1RG2$zosG2{~h*?jpNg(Ytlc^8}z`2T1DegmEi;-AN+ zvp~MDIp8>=aco7MJ`> zdVH{PrB+VG9mv*lqvG%*jWSKoA!#)-VHiWhRu{|#&pKd4jf8-5pFAR!3tEYFs@EOsxkDJfVE z@;E#>J;(&U^!GX-Ic%LbG{Hk98W0!=6wr^gSb%g(c?b9~;(>;&#jqSGUE)Ay_;PL4G~)kj@5{rX?!)zw zUH0q~FOt1Pwqj&#MPwP<*hW&eK_*$ozN9RvWX+n0u`|{zlPrZuG8EaelqFj;cAd|3 z-t(U8T)*qQb^ibT<8oc*Ti?%ow&%H@`@SEt_xlcrtqnnFL*@Zmc%mE20bui=l)w3$ z(2$`#tr*)8xIuyGd>f)msh(uE=d}Cd+jZb)p#*&JGHuStdH&H>Cx3_hXTq`UmBE0( zdY!cV(ZW^{z*|aJpACOhWEj+q=XE3t68?tPuAFDPHzXMNRd}ex@^lTjCkgyyD$@N5rw(r*YV0L}C!@cKuDTY~Cu0<SN#Vn# zYBCk@zMfin3ke~OJijxWL~j1!V>dY}-8tWuw|4-JpbYZn_aQ*YRcRcsgsQQbqp!Cu z9pnc*aOdUVp&no()d$W1*DsH3#M6&w9^qPaH|it-wTC=8EgV!&vGuuB1IX3SyPVGq z+r~a5jZ2l3lDTA=mo#TyfZgzm@ra%PYzPdwtH&tl9B{(T4plqs6L&Ky>kY=MED5L) zCdtBVQY#1JZNMNFf!UWycUmVn1Abz{hN%z$Oi(XaYH5jjuo zgWKMJ`m6d0QvxrK-7OPoe!`@pazIWulPvB*%PH#*e)l4Xdiy7$EHsXJEj;EuhZZ)a z{FC#Cf_gETtUA7B90E>Q#PqBGq9%?=KQ8S8zSuvNRFy>;Oit)Nk$2XY?E-2lMXwg3 z#qpOZi-~_%fM*^VYm3m|<0JP^`Tk-RDxFE@GL~&WgT{i|R_1qQ?eCr*;^?Y$f;F zSlMdjRT!|^x60oIPMif~XCt%I$@ooxht~M=CskIxM`IfsfN3`>{v!7Z5yWLfj34)( zLdSt^!90-tm7)_!E8zVExvVZdDA>+ddasQ|NGp$fM+N|NEpqd5tv!rquCacX2*iA& zjUzwBdqLH~iv*7U%aqqVZXN*6_H|G!CpD(lmD>Z}CK{~Q5%w?oKLUVUt{~Q9cojHG zkHpSaN8yd+N?XZ#*m-bfS5eJ0i+ybUt{$@QzX<|anw{d$+_H#YEZK|uy8kNL{pX$w zFZ?BX)?gY$*TU__=jFk`6Xf0;tf$^re_1PqgH z^u$2b7c{v6fwJ{<2jB%Ds*jj*kNE)VV^q`ZBLlaN<30x_yKslQ)l(5ngofTXl;6PU zas6f-m6wcf?yq-?B6~BQakP#=GC_qG-xOcbUb8l8h?s*ZD3mr_JURzqntN8;&L}&1E&;NmbC3;O<^a3+h8xs~vl!5s`Y(1A0B610H8N|l zumS0rL~f*k*IITT;S*8_ak`uW*}E4FY8;Hy~!AQ9)LT(P~R@gr5f5|n8Ju5T4b zaOKV%0@wt~Bec)DBtjFeF@)PS`vwO9HC&mowTMD(J}*$Uv=NT#Jau5mAZE>LoytDg z+p3>jS`7>#7m#;918S2%Ome@~$nciJv)Q+u(SLS}7?W{3#+l~16q{1Ht3YorM|OdK zSt8&?JX;lK+IJ~!`g!agSYl;X7jv?_+ZinUr+64RPguRtZ?j@~2xcsAujx#IYIiW$ z4vJKDK>t^Msep}HbuC#zqriSi`)N}M*;j?^qJz?(kyJHxMZKX}e3oQ{wRYfWwgucQ z3*IlOJ~SR~?tt$ycq`RYy#aHbVM5>8w8uC3SY@#+gI{vLd4x=X-z^SS9-hq=CkjAPazpr88H zfSxe~+mp)b_nw`=KGK>6*Hl())z_BcZjh+G^Qe4DTjVrv2E4?VGgHJg*;d{`}u?+ysVWkyptagk(*?RZ zm*15@;KF3`9XaLu#d(iD#$kHNSnQ$J*9W1%8EGDfd}{hHoKtf^hSOXIeEU+lTu{Kc z?Ivaci1zX7a0jrKJ#0ACC3FhMov!Lc2oQBV)!ZzpR8+%?LADEg%k*+ezM_wh8w3fj zM^XzfYL#9z-uPMTx0;udOcd(oIZU!+^HPn!`cyGZQpF@8wfRwAv1`Kh6vQR_ccy0S zGH*H7v;VR;`_Qa}SbQ8=RKV`plb6ixjh(wX0s|fGDzs7#+lU3`QQ<$8_<#hA} z>(To)Blb(y6${o&`!}IMZPX!$fN!3xr8MFHMZN-}R1%tsnYB+pKbarMADTgRxi9NW zV>cMiW3%-vGWDnl&`sm!0IqEtVGEziLkXr>vAd0V)VxheKNtR}Gma-UCI4sylmEs+ z;mdWV$SbHFxQ6%>@72|2 zegTU-qcRme*9NZZCjT=}`MCwD3l8@eG*aCHq($%q^YlgFJ|V+Ze-%gL=k3JGM(ez0 zB%EgE$AA0nm%yE!h2!lt9SYq|3*H>lvQkl1p=5Wc4eMss;adtQfMfI0eCrl8#MA^B8-3r-bl(6^$WUXaXrp7ho3 zu&m}6KJd)uS-m&9R5}7`rdAVtuTc8ny%|t=tjOY@ZY-WJ6@lLs` z-7dk`nRfIORck;(F~JXzU5wK8Q-UW#%EPD_C364u$2%E0u_GSvu13IKq|GZ2TH1e< z<951nPOOZ90f}RlVqrHG!403KmpOj3gS~7r(VI^52$c+V`XYK}4|lD1WqXDfOLsi* zYaVCFn=8mgplN^d?$nw4;&QNt=aOo=?$B~GA&5a`bmQE+D}7{Il9Xpu1nB#)*o6^; z>qtb0wR?qZJ~0(r0vi7`;{q9&ERj?0Mka&K+&Z}Z%*kz`qX^kSM%&l3d1rUG0Ab-B-n@Xo>Ia-8|}e9%j{O=M2&F2Q9qVgZfr@SOFj9z3v%S*ax2k`9=?tEoI<&&!|WhPsDr%1vf14^u??V>%`}P!2 z^Sns}Yv}She3u%1$|>vWXKH?MOxy$4KINFl^l&mn*h`&|fe@SD6T8+3*tE=tu4#%l zRz8|7Clns=grAug$b%E^v7v^bd1a!T4%vfn<}(p@slyp0hmTm8yp?ay9rS!8Hv_^@ zEWpp3a7961^I{NS9_`5uJ@nrt;Kx8zsn))d26(HtpMa0&nO?zZKkNAj6i&T) zQ6ltI>@$wWop;!H(1q3fk(-_BcEadkBm14$rA>7zMYv^Lj}bd*D$VxZ-Jg@7 zT-~FpdI-K@{>nKDo5OxN)RCG43Cg-{`%)fZ2-RhHh|?_oC??97vCEL_dlofC`!{S7<0N&pQ#5#q#fjUrxSTmU4BC$(DqoI zSJz**z91g2T^_>8m00AaZ7f?KDXP5*c67=+_))1{V`6Md5cRV=e5!ceF+Hqy=j%KX z7!&m+dNDcrAvR-?eVAgl7K`e$eGGN^yVZ_2<$wE}#@#PdYwlT;~5VTsipYKzR5fh0@(&A~X=H5I9`~_%}Kc*>^`|C;E zQ#_%fBNvSZ8Rk0YCmaVvMR_YJXF%u^_Vl2F)fceV9NbOPLIir%5Nb0zf4@ zRh=BoS%7R}5?QYLPJ1dT%zTJmngnA}n0MNX&;eIi7ts|H0B+i3KZfZVP9qgZV;z}# z@BJRjR?`WZRQteh@|vRah8QcVm_}zQPh8mSIXXdqBty8oPgt4j+ogffmI#3bCr`C8 zB85#W%fd8Yp}AQ1u!aCBy?7u-Dgh3KTxZUh;3O`Q%6yx}KgOxC9&NwcOy}>V)FK+w zQtw#)`BwRUc}e|Cv9tbQqdmTHZ$b5Ea~{`O^CjLW@tvLF>&MVv)S6>N8X%j~S#Jj7 z_FG*tmar_9bAE)PmEagVJe*Ha)WL#M31>rvG#i$ zdUg>FzHnXV^HTG3>?p=FPksI}Z~ufnH5Ux++4bWyeVa&eodjeRVXUldmd;<>y~ws-R8nkAhcY94qWJ2E0P|#@_qz(5av6ZGnFe$_qroGI4+uzQIG186k@;3?r*;B<+Ix1m!PV;Bkz`ddR z5&N=;+y#tYNL7Ds;DBY@?(iazuI)!HqJoq@A}>=5XHbdn@a!6z;}(Tu9V3JdlN&|o zO<1`*>-rswlG%~@wZdg9T(+FKPWk48mgh~ilq zqg|Llu&lP|iCQ30liB2ciV`!Y=7Z?zocg3P@8$shH)0}F_45XnT6hbJg;^wZ2>lGT{CY)O} zq^zq`9Kb{~`t=o~W}RFOEiCfY*Twzge;eZE;(NH$9#JYGst!QZ zFBJ8D@ou9$U(UgT#!pG}6#B_oO&bktwGLOYBaF|GiC}(4ite@uJaxc?)nqo8uk51h zC4`Ww9PyRVVP%G*JK?VjkEJ8$R&RPmnb41x##4-eLYk$lZ9}D>24`iyzTnGOWJ^;R zSL1G~wG(0^K@?AzZ;+?*Ag<{Le-7DtF==j)B9o5))sEAcX@!QbDpA)iwEYt3_lqWi ztfa+vr8<;_+jK6`&8x9MWV)#ly-=4eO;VoBJ3x8b@H@n|e5m_Q-Yua+o7>$QhgArm zr_04c77}ka^A3PkzlvZ()`7&l74G6L@N2SGnY0L7w2m-rQm5J&RH&9pAiN7S!EB>m z^Ury2pIix#Z)W`zVN1QcizN`FU;9|4hKXbDsQ0(FfU}l%K&PB#WRGL_=aJoVJB=%z zbm~3lzg2`DxEJFty_Om`PAX)JXj4iD98>B zlO=2c@>B)c;`jZbEOv*D;xG7xyQu1yY2|-)6j9beXx*5yW*->f+-F+>K`C1^x+YL3 zJNY;=ZpkS}E1Y=49b84_(5=P+bDT>v$N~7Zj<09k1fgGqa1wK+j?HMHoSDs6nKAbE zXB$JJiE5@;!Wo}R7x|@u?1ue~-V(ADEHg2TgAxade9v$_nHr%X44%WU&P6d=3y?-# zpJ!PdEIIt-$2~`3<@K7R~ByyPDIC`s@wm-#>?i`#dwe=E*RQ``##fVygjQ z9w|Nd!Ssa`Tamz{P1N(c)|)P62HvG5T+$;;7zbkT1PNUsXrFI52Yf-4rq>*$FrC@U zj4q_;*N31FrFrO1uR{;QSn{+KzVq4!=DFlwI*^~SVriJT;c+~tPdOxn zy}5QoNLn7BB0X;$oFL^v)U16TcdDsqf0G=vQz@{!xG)8lV!5A|GHG&Bso9Crou_|o ztSOmjB1Ol%_f?m^So+7{h*b4yJOR#7^0svqVYn5|B&74&&6oP_vkE9(jX z^0HP^ZG~aOwWydyP5JDaB4gcJMUtrF6S7oM%@4J5R5c)^D+615^UC1+8r07#B-OcH z!|?abBlNFy_dck#71lnTlajUP1NeYHLuKbzcY!hP2Yr)ce`i!Ld@)<9KY}N2%|Ps( zju-pu=2p3OU@B0b8H`#$K$C?!wFkSlt~#xc#Sd#aRmFT%UOxpP1;6uKQMtlWvEIxs z13B5;wUs^zs2J)le&_ne{qGdDb}0;hNXbBVgzSTzZ+`$Ld&MJ%f9oTU#0${gR|@;& zwt4CjQ{|a@tF6MG;8){q-M`6ye?^$;;fkj~;_rL+LKon^o40@#0R^!mTdi9y4AR`F z;a%+eHNeu1bJ%~1du@|N<|ltRDvI zVynVW1f%LQXNR$a!4O4Dy(fLU-G2V|i_=0>nx(8u?~b`Nf>^?U;xUrF+xX_aN-pH4ekEC61djg-@o7Mah=M6NcUpJ@o(E|kP`zbq_dbYhox zF3&Gj?REf`U&AF8tq54IA?Su%HITQudCo(<`S$xu;>i@|W3Ts~Pq2{4QaM*m{j>&m zhab_eMJmU+XP*X3q2PSdqIg}RGD$kWBEnjQY)n^R=mICsel~Dvf)<^O5!O_km|Ny- ztpP5s=0IxRGZid|db+s42W$* z5}pg3#OvzlVcq5FN2?OIE42O~0@-COy-BGgr8&s`ILtKI12?E!V0eAMJtwR;o=>62 zG0K(P*Q*z`5MJO{cV_Z}Wk7Tv?MJu|r~>4+h@Ej}I=JOJpmwBbLnw zS!=eIHp3>7)&WoyWnExNcOViAR>;q;9rT#hS5HbRGmPn&r3!v3dc7*7KoAxxbgrCl|3+$fhQDztNvL5uLJ)ecj&`e`uxF?vn#5JV;pYTj=ncZ_M0G>wz- zi%MH5wGBdWePZ&e@&u@}($X;So`}H&3wr3uOjsXf5mYk1TRXtCUu{Qk- ziZ{;cPo$pAV~(H1cR?zEzmREgB0H+UTBH08Cc*EhxmF{F-ngb$xb*JuP`kWrinYA1shg1_)X6`O3?m5wIbw9C8~RhF z{LpS5Z+qvsD&*L}-1`FHnHms6wl%M>EAQi4&4{qEL*IR8KeWl4%(?^RDx#@~0XW+$C|D?BJ7G!GcZ>41ZmY|Qdxc|7XjCrq8nRah{*EP}-&~+7Veq)1($o+v`Vg+T2@r+A9^=1Pdq8vC$gIdDBNrIFONRFt@XFXb*g zZHG|bsyHn_D3!zp>ys3ENBm5dsUs?pny&TfbR)vr@8QSq=hD3lhlmJY*2G5+2m-%G zshDG*$|o)O?gnTZ$Le1OPi#a5OzDZK5ptnLb!foeGo(Ms4r>J6-D!tgXU@aD_rM)! z_uU!vwH_XCMEB~c;*~rM+Ei|g13&+~^1XSr^?;#lD*!ZESJ3nMsF46`ER+ap+!l$T zyE*kjh=BX9?CH{obqHtFnE#gqo3?Bl2EST}=Q+&~!6mLQ8;7s+2<5)rPYw*2 zhIva>U&_LuxWK+?Y^&k`+Vj(uAfrw!u(xPlE^|@-H2WZHm&%^F<}~tYxWc?bg=g;& ze}q-We5gqm=ZFgQ>)L8ymAb$Fy=QaY`{gYvPmrR!)-C3 zNNi97lZqHzl(Rey$}0{HoGE|Z#a9gm-jYY#O{}u%ir(sMW>{k$I+^H(8keRlaM`JE z-sGLz^RIzM`IBu4IA*_QC{IM~7qt@Vl`>PO#(@3;bL3clUw{roV2=Ys8i*Y|57MrW zkx`#il*{d$J`h<$=f~?UZrVjiMwO^I@B>{O2pfv$y2XC2pIP1zFM{XRIr_3w}k`L7nI!zBffHZ5sc z2~tO7R&H*Ue?fZCRZ*a(!e=nf?_h01{ z!9QpC)|Xg*%&LHjdX#)ty8+@xO87eX+5~>`s~q{m2gnf@62Zue ze3H`p-B9|!hx*&!zVOK5l@1lXY5#Pf;B$D~G4POV>gOjG{|fy5&o$O{8X1P#cb_`? z$BW1KLb$6WcnERog>u>NclbYJjd#FPjBStL`TiiK_~#=-rr;r*?QJi_@59%BM$Ov> zW_`UPT;YEX_1{NWSO6Y!ZCswf{T@{KXVh{blt(4gCZ64DKdbcuAcbYq}|LARu|w6q|gq|yzF zfRuFC9WQ(DbM^wf+jBqMPyh3QA7^cR*L=qubBysk;~DdbthCqxB5EQW9GnB!#jncY z;OtAs!69fSB!Ev&$-GX%!J)^wepN)lUTeI^AwA5-MR=;A$10bkjBQ`RIh2SVv1B{* z{XF_-M5I?qf)c~Lo9eCOE}F%M*~V>J$7K&hwwAfy7Tv@A_6YvlhpeO+O4V2by?q3y zNUr}Z-W*z8b8M6p7dO8bVHIu_-gePy=*+uefxd>}`Sl?_2j|&!ZYlVm$5R|!6doZ7 zJ=*=>eo;`3yzZV-{p}&czyIyl^DsBi?sb=c*ibub}eddnX@k-xVZ5_f13iYJ^6RnL6Y#q9Y(<4aW5wUdNL1#suU!t^t6F`tC)zFrxrEEpAy3>(N-lnxaid?}dInud5aH6Y z02!-r#JY1w(R|t8OYo0qa4jD1b^KaoaRxO%GwYDkHh<$8^?DJ?)Jv#*<@ahEod7&$ zTz=df&2L?A4(&^yAb-h2`dbM=mK(D9nt_HptD7#vZgFs@yURX;>S9?Y7)O1hQuMdJ z1RZ%0AFq8Tf)l>V%WLNocl!iQOGKDHC~O2@MMH!6tsIZxBPY?RachLH;^9W^8osdh z4aw~`XX#7*H)Hmt4_cVw{a!6LDvNe!-MmhwQ`6T@xU1jqFiLY2lr63r@4j-zHH_HBnwWfw zDQBL|H@m!*>^^t|p$4+bGbTaO;zmewn0)2IE zqr2?YD~-}U-Y?j3CdhW!k%V407ZpA^By6*bTS&0=Im zPg6~~M}H?qukN-F%_;7~1g3+=i-?G`2L-y$Cz^h`}nO?+lS0x#Mu0X4MJOeIzgjcEj=ZlpkiIzy3u(9F6y=rP=H_N@?GdM!FJD?X z#x9yqN)%+q(syC@9zU{|&iQVRRR{tKy-v}Wwu)!Ip~ABFS$IYD6YZreTdC%nqa z%5QBY8#V%)YqM|7d-6@~vQyIh{rwHi%iS`lVJIFttUbY~6Rhf&F zUiKIq{BN}$5?FB}QUoF>we{`nYugSXxe~ocOQX5T=B)50nbB}|wV$n$WK~*gCBm@G2B!;UYQt|sg!QJ#pNrGe z?pyhM#Yp7?C zBO&uuKouE5dhXlZlJz3(YsnPs1KpReI56UT#b^VM^yVs1Fs#C3Z?pPxO((oAC*71Y#TAe+S1FEWWn;3pO`a2i{g4?>d|# z5tVJNkkH@xlQmqzm(`=A(NCanHhl3zkU@Bm#~w|vLqZ$`0IqDW?FVD~j^Gg%rl5(G z*JlDsp6xwuPtU(_K{F*KWzq@?t!JUVSsj=c1FJqBsJ<`Nv$SNNL!(Ya6*pEi>0%LT zRnI&0MrStW7;2skOfz{g!4>O7Z+kGZC9+~Di^6G9=grkVpEhkb!KD0@6juHV&5AcL z9!M{c@U&e($aM{Holn1Z|&|5UCkp16vTY`^y$cvBai2}>b{BzuY9>Q zo?fnRfn#@477fvDjq%#fB4(F2!pIQ|fcsfOl+h=EgQaSp^5)H(<}>r1Ypr6QTJ~Q( zEZ3&1+D!OI&!i)@E$!in1sc3|+mO!(utU&p&z{)V?Y6l--T&Z_exJ0MOq}i&&3RVe z%ckY!tGB;KN9KgjS!_oA6Rw;8tg}sduRncloAQ()1poN~s!0OIBLaGJ1z= zgUD!UX=AMnzdsZc70sYm&-dp4nI~TGwnH`Txj|uQ5tO>ZlfU_F2_0u=0i|RW(gwjr zjaECp3t3`^PCud^RU$?o4^*MThyu@7<8}uBw*bspztY^2^$Git z*no`if`S74U~1=us>;gSckiAbyZ5!s!*Y2rfXBY7ljS9XJ~CN%LV{h~25p4bSZ@L+ zwa)v+jj!1dNE+v~hC~_9oOxYRBFtbv%-?Y(4+L z6UOk5juRszq*^Q$$(!0QM}D+pFqrW}-ohJ8c~fP?l4l}&M7Kbk((l>8&>qv0q+Auu zWs)I8OVc^kRMbNdPsixEG7?{P)FAYx*8VV_KajU?I{SNXe}!0JV`;#$P18Ossq%@ogB@k>kg`pFe*-z9&HA-t}V;{rER;O`eja-#U9&cAmXo;XgwiOn{an zlRvxmaVVs}^!6SP3ky5Yozkk$>}z`qf(aK7PZbhOh$+tnj<6O+pzF3U7}4)zFbo03 zkFl|*A?Eo^U3x6EM}xf&+K>jao#*1B064)RBosbTYgfnBcGbdy{S?aPGy+ANPVcn* zub?PrB%0`q%b0qfr?QsA=`PJAfI7j!!PM#UtP`ndHJ0`D^$s={Bp7Nq4EnWJ|9rjT ztcbisg!=zrCo~40Abi|;>V0x%Rybv4be;F!Lf=;Lh)!&o2Dpdq zgupLN<6HXqvuyX>@&gJa#+*$j-iZ<3?~;Cm$+SL!b7y$6)^d8Z*svOkq^FQ)UYL0rdQIZ zs{;oAN!cguqT+5AA6WPVJ7SOL1MV*{AAY$l51}pbT&JIEe}K#tK+)XQ72?sZfv3-W zo`d7LM|-19J@4>`PmB=)P3rc-m_&qC5YF8hTz-YRKjUoW=x+M~ua0_EaD4FpUK~xM zC?(X$kuTPvhD0K+Ll+~S2rhPKD#acNz`>vZHBjgreubT$y;uQcw$CQ>INd+o&Z$_CBte2OigJ3z&A(MCt z``hqxSdp}>^7i&Mu(_VtqqsCSQ~?Weo?Kj9o)>F5j>K9neSI|flhq|02g3&eC#mU) zJU3Q|ZT5ixw_fWfv5oh@@DM-q@u@n%)9s7zb&S^7rZxvXc=ikVv%;VGW3OXl^>QBF z6^0d~VZ{f}Tz`VS;yz@>f^Lc1P?{d4N~XS4vQ`lvAo$FlHrPRV`ibJ!zCJn#I+jC0 z29-wfVca3PcI@zIq~RugMj{CN(Iv>E`R$l@Ji5Km!7699^TUU$)^>JcfR=-J=fYp2 zI|nUvbm-N!ji_?M!^1;G@2R8kJei?*YYw`JVA=mBb?1iF2~WmlymlS? z$tuW`xgzqnmAHB&c_VQl-?H1Zp8l0Aj;gildg1>`3Y}W$bK6{Mju%oKFy1c%@DCt$ z;v2Jb|Fi`_UHC$#c+DoPFGK_LrsX_cp;GMZfcRWNS}gdS12Gij6+?9)nYaAeG^wrX z7TNyR0zkI-=p??#h>oO(rQ!z>>S1L!^Z-B?uAkkb&#=o+p!^i#vSUU?OcjBkVi`cK0d_kh3TUMG`={PQGkGoecg|-7rV2BioA=0pgEnL=E4f7 z=5=l^4;&R@<>>p=+L0N=>S}HgtKGw|pXZ*hetp_a2qO2gm<~LI-Y5w^a!($B*7at-X_%R^3Jtke?r7^o{x`Od%0RU zBbvs^(Gh||YxO zgfS9?9?gip(v+&qhRUW)7%$^?+gQs3_QXWfdwL(q$D2URi)rNkE8@CF)t-LqO1JZD zoobNPaJ?6@O##WphY8^jT+`Opj!A3Nu#y@tTI@ajL+vSEH50^|dWSZye_Q59yq^V= z#Y+|JuFoe5{cUxuNY8pAS^Jr{cjdQ=r@EhBoW8~+2`oZw2Jo`y9^}*h+Y=KQF=?*z zNnaqm_Vo08>FiXKiA~CWkI!87xITo3J+01MRM_~lc=hLNf0ad>ed#Uh z!(Ziem^CnqObx_pyoLVhnx=X9^8<&P#J4DdbbPiFf} zwUMSr%ZZ%@%PwDnK7p^4qDFy`Y+1RvVg?2VCrlI+-XW_Fcb@rV390@S&6Pz&)ld_m zxwCS^jwD+?hAG<@)02`~vauqL8wFXw(8!2M!;9#oEUzf%1j*972A6|`m_#^BgEDNt zx5lq{ttskMearQB@ajO1spqYbU!~u7Ls)d~WyIQ@WA^3byIg|;<81n6>zo}&0*+-93MDW6HmFn*Adrm#)Tx*&(ou;Pd zm)2G#WO~_o+NQ4CIzgq>io6C9}{!igcauy%TI!`uVlqu6kI0KUWP+luY zB4#xDO*5WO*RKm!DRQ*2wKn9t+7o72Fa(7zPh5bibANG3l|PQrDFF!E$JV5QkDW)wgKhNHtMe$jznkDZ&!G#6xfc6y?y&O@5k$>-=P1PQXNQe0!^kzeb7znAVA z0`K|w_-@yS@^0l@TQwbf`%9vtqTdj1Twa=2FJ5@q3wk=rfKT$QqAy^r&X5GYk040g z47<3&5Rl}-iTj3Z)?fa&$2!I%W8Yuy7wWn)VhJ?ls}z^@!-nGUjEA90QdlYs^m8am zJvnKNw-x(?luIKrZqzJf7te(Vdd69;LZyw=bG@*&v zh0jU1ZABL$HHSR$U)ame59@f{9(36nz^f_CZwx^$P zbaIM*_bzB3CZ7yU0Ns)icdyAr34Dnr2AM;B>a93^6JI(uI=Ime%7h^G&ezR zv(8hcIZd~FRX7n^9%0+S0WJ!yb~I3?2+WhvrxQaODR3WRz_y?cBKZZmkrtM@>CyBu z;D`b?Jq6S#Kb)>YhuLrWW2s4%zLoHwc5jjHq6J3Z#52zyiL;-`ZhH-koSK@Nr<^|@ z;doXlG*|ZT-~R!cv1H2HF7wiE^)!HVQ(-$Sheh?Um$>b$!s*gc!uMxYryOfb^ZEJn zC!mF}7N>%v7-K>Olqt06bev~iMkN2gtrh!(BAWyepjF+%i%QF0#69IK*=z5k#Lv9E zD*F3%j@|osVtU%g&ABpS5&qJX5w^CF_a^DKt=%3{NIqWEsqmzD3p5Q72cEzmXjI;n zM>!C|8pVz~+|d9my>$i%6|O@xk@);@+fmwSglx+HV9t(?qZ1Po*=$uyc>3oMFm~n2 z70YhRs*1frQ<$S}y?Y5s&O(PreR}BXwVe;b{)Co8ma?Ew{w!6YE(%#fBo zCHDL6^qo4t07dN?vg4>wu~2-oOf;K> z?tE9QPpZ$P-ztBYw>%UQ9UFVv(b2KGwb+#g9SIb}8Air|ixJB=#(|YN@cB8VC)?eP zA*Ts>Ti2! zZEaY%BszCh2&0O`c8bc}q9;4+qurM7L8_8ts+z1_A_U5y2_3~i)tevd2@wN>ozJPH z(*t9af01tbIH~~1)10n9_zN*OT?e1Q&)pvPk4@$DF0J-s8Gyc>H^`fy|7#IiY27%5 zLWBeY0>`Ro?KGVG_s|(GxpJxOhd8!2Eb^ORkq}`hTa#6hRvrgPF3Or?Nl^TU#lL2g z-{zmKy*QB7B;EC;%O#>T89c3yXrQb2>hC^U08w>B)2*N77viL47(EHZ)X36j>t{b6 z3C%@a8Vb}MjWxAtMR>#wvfu1F-NNHGZ6`r;ic-UIr}3MC`Q(>@!2(`lOAV%;TC~s^ zDC{P59;LX2nN@#pt(W&XFV|>$TOZQ}Qa>vzi#(CLf~4L5FQ4!(T5l3wmlwxNJcB^J z?BDc;!+GlqT>~8k5ng}p8M}g+=ZSPy479M$Sv!Des7JXeF(_^&1Pm4o)$EU z@&^6YUxVljRCDSHo#a<^)KMWk7T(aFUYd&+rUg+0bgLA}5Ol7#)3U$nx1EJ%6>5E~ zWWyvUTJf(#mo9OdbZHZX^xxXr0w#_iTo4DbRz4tLq%9NsKeT1;6FGyD#A`U*iH(n+ zKN`eVk&4Vu(I0;NFx-FOK*iTbbjOi)-(jEtpupdiqxcp4i$om~TGBx}>K&;{U~K#p zIaR!q(9p>2K{fCWhhAF#{_;6VLLvfnto|3ML-8M8%?5Zin{&D%!*9m*Vh7B5EP6vZ zH0*k8I4pV#GhY5EsYYa+hjyWAe%1dXSLyHDT<;t3;_3bP7pp)nMZZO}bwOkG+h|f6 zo0`KY6J|@VnX%KEf7LmX)xmI?w0;*r;5GV&X^(!e*CdHy!Q2wW%&V~I~07PS*2u(jg4`1bSuD8u|dHo$s9GIhBUZJivNd)-o;qB&W22b zs!HfDf9P*vPP>5un9ae-Nxt{kT{O}Z)ic&i!#|8bzE8Vkj$cAPwkbrX2Bjt$qKWG1 z(VQ~&Q$F)IApkIqT^iIK7qU~8QL%?1leSo!_+y3lUrD_9S$r>F-lCXu`9-Y_S_;`M zFe6ouT)z@)lcd(tbg%DgMwE3AmmXr#wwQYmm;H3ZZLVXd{ZZBauD%W1!nsCCZ_!#_QELD$=TYna@1fdLm>$ZdmNZuiWW%74=Y@n1k-{i< za)baxwMb{Ht&M7A?C;QSJFg@p=8mR?#kIUY#q=D@1f%NkBE`cp^Z!V3P)kMbM=_n9 zL)rm=)}xb>SUNj9t3T%)CuTkmlol{s9dDx|HrIynnTh#is!#rJO_4*WEh(c~HnW<# zdS<`xQ*;WRobSEcw@=pdPZnMRL5_*yLrdrhynuY<7kS_cmN^HJqG+;vY+F&(3_Z8` z(Uq|@pE)i>v{rX5vJ*8hFu-yCJP9P!j-MvBdy*ISAMruYBOuSh{3;geI2#1t_34a8 z!8SX1DT{4YbO8gD z7W|Gvl344pj2_LF8q%WjyXSA3N`sEi-4I|LJ zkdvx88uGE)uo!CSV6~9+ea2p{d+9TL*R=tYSdQwr?xwrJw`wPOO>qjgTZz6UwQMd21f*Rc$*n0Ng>8jx@6@HWw_O z+g6PJ$&KYtUf)weA^tuaG-_kx04SdVBma#u33wEJP^g-6mj-2|5qZfN0aB^JL2@{rFLQ|Di)-kg1&)G9$9PwBN={+5mL{VbY^T>R$oTIL&fPS)|h?1>CH$4a5PLyai@Hp7e{S1nDz~)s;>gmhW63_9qxe z@}<_?VhZ+>wR_gq)*nK7tw!JCb5SYi7#f}x6%!kvowOIzu&kii%F%@1p~CjhO<%?G zhh0pK;iI&lZ&4ng>01(Q75TIJYqsQ(VD~JA?)u!~qQRguB8PywGU;sWn+G@;W*|Tm zk9ObPb_jMn8<;7O52V_ImM?Qk&CV`2`s$b&ide~l?*gYs8v>DfCSC22AuRkPSzhFT zLQo9utUS5Xx3E94IeQ*zexwGTo1f>E$T~QjW_UV&p7X+m7ZX41fO~(TVYujT||Vy7T_5C{UU5{nV6ZSv?(=fUctbD0Kwx&+v$RDo-E(o_Z&>P z#r5z^x-O>rAjYoGWzoBDoVEaVFh|l<=<2qJgY^xFd8!%z?pII>Nfa1>$|~THNL9f( zU+?`=py*(@`j;d34mvHVKirrAy0S4d8;yk_fy3d|HL!@Z6K^Nn1IW~#AB0POZ z@W}Z1Gn3SJXZRnsny4EG{jCMK7I_@0>LX6?(rehoai}Hwc}kY05aDctqeMY^Y!9@C zav0AeI$9{X#vLKM*d>mxP3lL+-X5^i!@}Tb{N1S>AgM`ap=;AOEJ=S*IE$ z*(+uSYciuxc6D}Ukc1=^qL>Q(QC>#M#j%nBmAHG4$%5jttq3u<{BL64a$*5cE?BNk zjWGdyahS7_Ovc!r1d*nCGO^)(`E*3Jgpqlol zBuvY*m}_|x9|Ef`#MjmNzw5fsBneIk!4NO|Kj6Qh(gwGUS?Ck*%}DiVufDk4RBi%L z#5VL^HPfJxR{R0hEs-C~&KBjbK+Y>!ot8w!>5mLPcf$Ckv!ohIw@Enou4R@Vel>>E+t96>HFKiZ>w+}!?LhQryF zp~D$=gV63{K{}w@ra^xRg5Q=R^^sk#gF#HBJi@}+*z9}D_0dt>iVv_m6pbCBt{~g+ zj`rqD6v=NQz-*Es;G_k7`*)>}uQd9Xha-G|TNfne%g)NW3R#ClJuK}BsP93x4sp;_ zn!RYQ@zQtSE2D)hgJkVF2nbBuze|+*1tfMy7p~3JSazA^RzR5m^qIkloVgZ7HzcB{ z+OxWZT{Mt(hGNiXeX*}(gZd^1CJfqYc8pQB17<+SV}_%#!=H`;ou&}g9& ze_uz=oQ#*+$PqX)7_5mQH%%)i3R2>1I!uo(O<1^v6uClKP;!kPKaN=>TjFlYA2+y6931a5 zQ#>DH!0#AeQ4C|cj+>Y@rlvRiK1*JQNMhe%rK*w()pN4yL^)wjo)k z2jwM7zZdometm)Ujf6K~k}mT&cA`g5;30_^$qvZO3UmWYVtum;6o`gm!n~N@XLl+Z zefLBM2V~K7!O$(`tE^RJF|oZdp?EJGI@eP(_GqoR%t-(AE~wk*@-SCQBqvMqj$^F0 zOhf>PYA(V2fbN&q*PgphoRsx}1UrAp#2A~~$Oi@`DQQFwdnk+39Zh7vw$fHof7yh+ zaH`DTTkeOhqoLHL_`H{4L+2+5{EOpQ>J&S@Sn&^PW-|JsZhqqRXl?YfJ$|EpBq=En zWt7^OYHIYev8}p0*0_lyn(5;L^zfVNJt{(l6wSn-d)&CKa@f6l-0z03<+eV*G|29D zR7QB+I_IhKAk5)qMZbzB8MAmps4{rbKz%P>6Gw_s6&LP%=fc@S6QZ}Vw8Tw)e${?M zGthwYB9p#!0vT%*cw}dR%7i&kZkA`}*dBtH1I=80n|~BBP%>eh@ewL~vDe8$0JMVY z1QPtViYQ)NwVF2J$pVY9$2AKJ3+G-L#-|63#P_T5F%o%_D@fF+-tN9Fsgb}?FTw1XT9O{+=xkX70H6b(ucTXOBhD- zK4+~4C%whX$!6)|s5(Z5JfJp&C+%%;i_{guc1+5NxSM3y;qDGb-Td8YmPz@D6@}+D z3fpG36KIeFji{oK0hQ+qrq|QR)g)ocbD+77^XI)NKg|z~((-atM96#B~c z`8|aMn-`bWn9}{UntVPc|IyJ{Z|Zft6MPi5fS~D$$(zY)(j>p0(d57xqL}DwP2SE% zMKLDGNYs~WR2ALttZJUVp1lljOL3c;{NIM>y2#oc+p?kAG5%4O3X_7*d?J;+ImlJf z`-l|343|V8tH(U^`=XJk>}ES6+qe>jH`>TwHRd0M$-s?vbJJEaQizbA=ZTHCoj$DX zM|?k*(A1<2A*&wlqYs){!;_0lI;@{G&aHTq6==B9q?yEkuupO_Oo7S{QhjD@F6{Wv znXBT57{}_pPWrYs^!G>hv{UO{9xFVAo5L4@9>hvuJ8T0!epyjD3Jc8}JagWseMrNnkVm9yKpX#7V9In2Yw>Rw|R z8-bn;a@cQiKf23~;)kH$b>cY>_L<&Rn?Q3p8&6Jr7{=-zXPdO?Ux`CM*KA9S?OPfs z>|xXQ``HvM8I?1VMt)GzExgLlwyd+&WUN$;x**|Jfq+JLrXxyyveI_$i&)iO;U$p* zO(hE=3E>ww>LUR-IM_*WI|L)YLQlKA1Y@A=kjeunUfF6$@~3sECLWn>daM}JFHPhb zn;lRrE$nZlz8;qyc=?VW7w`HaTojbJs+6^$@~67GDFu$6K@9Tva7eRV^uCYc?6wDD5gtWMJG#I;ftc&%uFa)_!VX! zc7V9~vr-3X%3avR73pCDuw&ImyVUiCS6GJAHdqceUXOA@;>?n-#METJ+$I;CPJ&ZVRnzWWb zW3$3$<|oJEmXpFDk<{)Tg-lR)KEDg)+m23@Jb@ zYgltXz^rjbuI<*8$C$U!(AM1>zR`c_N$xO=!}Pahi=wihEo07XgRrnw9yIB!$k`WD z@Y|~eH}kiRj%+(gAdHto-|ooJn})8CXlD>J%yxZw!iG3ULJ^wpG7rofrQV0y8$hf7 z1)XyMv(?Cdr_G-3hvxH~cIsQzLZnIn3}|HnycMmH7zWc#jSZvLwN^xLX%VFff>~FC zG~*-zuXg-a7c|}P?{0Yg22g3^h5Aq697P5~$w(#$F@FF?P0r~@@hZYV9ocwR7tu}) z;?4jQc;;_%YJu@jA#vp+#2tvv{ndO>^*~ZnsBNbaeyXjlB$-Z#W`kMJF4xXrJJD8b z-X!`@FML(=Pp&l+0X!H!k|RftJ^@&e-1w9&|E#y*LOWlAyylrQ?PxNz#!Ej18Q4S^azWHSs@k2%^|-7DAp4$FX3B&{Jz5EO2tB<({(NH zsPheXGXj;~e_Ta^UMK1XR`0EGOXBz?&$&b6p!yQ%olmuEvAA+TcG+!n(XC3xb>?MY zXy~)@gZ$@&rk}`Mg+9U_>p1*v0R=k8BS%Z6czVc@SrizK1+{2BisVn%qQj7*kN*is z+mV(e5IM$6!}$QK(qy(n(b9tu-N$RRH)cYPt9XN!f^Kt~t}5K=CohXD{I>ZVzn!^_ zLRo{9G0eI#xw}hBpUwBU%I3WGrVVs|&6cP9l(mjBf`+ozVhFpA09Vu`6aZpydNyKH zAPMe*$o;zKkPQcuCsa28Cu646+}zw2K3%N8#2_uaKBt){x$X?&hs}3~w;zFVbpIDI zx;k&~_LY2)WWJs`RNiw9_E7VEkc&{=2w>D`z$ujMTA}O8Elp{l++6A^S?}V(hvQ`r z(!0?WxUzE!}WVb>CJdt0hLuDP14*#liosyX&$#(p3E8T0itw8 zi=%OmD2?p0=d{1ZKB97)S9YU-K1*8&QM+Sr)0Zf>ucSN8twTkq-9+x*zGBJnl8Wa(+i<2?s2>Es^3hspT6@fK6S z1%1fStI1f5*XcwW;1-MTtvjR6(NcwYdrD23#jps5%9s>D2f~DfRyD*s)dstu!l)5; zI}C>*1I-o4eL8gOKg5!#?`++$0aDr8PSDu1>h}dgX#6NfFSsW~VDko;ec&q1G`NG? z&M|BYhAG6a5IOxs@j!BBy{9DGyE`F$GGF7Je!gP_^~zgukvwY~=w%=&;zzVky?6AD z66rQ?lqtecPP$Awse9+&h)T4c8*~D7+W72PYsyn(wy87Qgu%=!9O|D`Q}wxPPbu}j z#=!8~c(calLT-symw5pzf=kBNM@smlUBUs(cif~l8lQ6nodhjhLaVkAK0nr0h1?-A zE?z%Zgxp92jl~ST!mMc371vP)O{O(F;6ICFKOA85FoR9^9}`yi{&U4 z#_?Pr8xGKRI4R*3ASor_zH=V)}1r~NbnQ^mEdWj=0e1IfeHeAkC2 z-8NTqm!wOsuXOJ@B;YHk(g#${Ldu<2LRN^1hABRD#@{kGd`Y1SDSwwb8eDS*)tMp# zhEgt+e7g1iKE6e8ISjuW=*rOXq`b}IMk;GlE#yj+f208&Wkg6k*D+@?J!dgzjMstS zUPQi|IgIi?*y9&8dlW((xjHfk;ba>U?xqDIjgHKI(E^0$zquo9+2S=msBv3~cdITF z9ZCEnwRnBP!r(-Nbu3!Gvm!Azc2LUBVdfL#KqZZZvwGJ^m2xr6_jm+ve5Kns(I}k7 z(l*DystmK30PrTey#4N5Z7%t2=9O!HkVB8aEf_z!C;d>C&=XfL4k(iblFDphReOC@ zxud+d+OO<}k>tW=Rc}~n4_D7Bo%Z7+hmYO~|9I^1;p^fjPo0c<^5Tz56$N!Q$C{F} zv9ZcMauO!_W<7~r#wOyvj~|jm9lpQ+G1HUBgx5c!Pf?KGUnwabi?G>P9XFV?THsyX z?-Vhdsm0Dd6DpWIujM3sZ(%vS@1B%DG76^TURPor?@t8&vT>X4$RjSnJSCOt?8~84 ztZH|T0|Q(r4f5A)o9Z@guVoMc^Q=&6tGwLm+&nAj6Ny_1A}(3>X-ZONw*o+FaQ_z3 zn5IpOV$c}-cwu5p+~?b}@<(J8hc#5##*aY2e{tY?qFHy|m+pHttoluHQc=2Qomp=* z^|_2?FAQIuI*B5rcgOk94|EaP6hV7;KMU!d5FwZRFBkU+c1^okru0go_K4!(;u$NH zvYyU|D|JFRbSs;?&+be2bCZA{eSsIT9`o2e@di`vca#JXhDpF0gf^X?x~XuHjYAv^ z&ISpatRiWX^VN55)0RZP18Zl`QBdj-6VVv0LW=(g5|tgkR9}yoqL4ZaP+g6CzyrV zO*&-_%WFS)_RXh@rVSJIjx4d2#+k^rW%8hp+PY41(dwC+KKQTEwtjObBP2?H*H%q& zr->?i1EgRbO$izA>aSb@tC1uK;<}vBOuF|&a&Sd1$B9joh2eCm(nip!QYBa2`hVCT z2Xz9are)cgWfY{WS;4W{pg1Byk)|P!7FX;j23EPYA*Z)r{cArw2*1NV z47}p|7&k1GRMmH4n{qs z(ss7g?~r$W|Ic%c*qG!Ry@Y!mZ9KV14qWaOD7?Acu;A-xk$U>7(N-wQW{Di0|9Lbs zJaeq3AiEKiS8iu^i4gAYDE*R+`Lv5s%z_^j45dzFN78?RL?_r(U}xbJ6NwYTU(x^m zfmtl{Nek=fbclmrL360X(9L3YZ+o{%>gzVpR+~mLX%i<>+r7u93IzE9OJ)+YbC1(x zQIK!|bLmjp$#}9K?e%JhVkw5Kq;Prgi^=)mAJ6cbm}ikE5T}o*^u~(xY`jC6ZfvaV zdB#`y{nJbEL8}Wct%oxcL*s{1#^Z$D*2lDYe&RmZDAak6vRdr1GgxT3V(syfJZE-t zjC-`VsDO7gAv(i9qmE~I!=%1sJZWK3n`%aGzF=XZ%ithg1%tM>JuTvXNFey@2j()$ zRGLNFcLK_&^A^R+aV{H?iQhSq|BmUT`s_)*l408rE;teJ!X%bdhh_7|VI7td4g;)5 z;2@*X?QHlXX3+BVIY2t{^#_9d=Y!R?n%D1EK07LUNUJLmTfI$-KC;c!z5!(qSknLCp|@6JopYD6DXAbMB*g9Hq^(?t^9` zie-gqVsEk(#obes)3k;{t?EC8o-|~V$hD5&%p;l$vuRc39v`H8bTA=;Woj@*l&a+& zIWD)G4Oi}m`1`u4NU*;VAtX3EoPPQ$*F$7fxOOE@yBN;>514PSgTnU?yqx^7n&3#x z%Ua(PSMF(*-$a>;%nqfM4wY!%_g7$D(!ag;?=uyNLaFhIyY=l~71K7IE*@IX+~nHi zSWC0}@%cR>E2B7~xWJ^8re!u~*Dt~2qAKRbo^0I|;0Qxf3L{Oy4-N_CF}Lt@_JVf& zvsS!^)=SIOVaR5^#Xc$42*RlYM8V>sW(B24Ffe|~YlYO45NVVbmRlw-TNJ<@e6=9A zQ=Fbgx|#Ze)qbb>(~5QSKyn?d>m%4|J=rTbmY2VwcuT8pVp4ZdDTUSSSQ z3713dgaxw{6))!Ad80)m>{@t~*B|6P%(c68@uG`GqnhyFqGhO{ArKAKQmaeG70NdNY)x zXo175{%W$4$nbGN#~3rhxm+UHghcmxrvHW{W93Mpy##X;Q)eZqnbV``19H_mk@5tt z<5V6lvxniGI!(JCzvXwAZxgMa?T$-STVGw|QIn9Tt9{dX|MG1lY$_cQSFBb@sio7A zZ@(VJsMwaju`-rfCbx1X%l~-G9wk=AWr&vVk$byDF&S{}L5Ib`G4&cm$9Cpid@<8f z9KDuoQ^-6rUOd)S&$j`%?si|PiRuB1YaF!YG&-}&r<=we5FEH5M(2_Rz*70yW$E6b z1L^pz9n9sE&cpnzqEY!sMiZr|Qurj@=i2seP&Fa5K9*P357lOU4!_vi8@aKuy0WH6 zk!N;6SxN0=6V16-X+t+8O_&qnNrH3VEk7gEk*DJnQ|rANJ3$fH%$1^ReEkc8u%&rT z+t1H>SvWHcvuAn`Qw76~au%v>X^Di~CatwFK3O|A(bcA;IJK85yF8FX_nE{v%c*71 zy<8jK17#JX|7azT%(~Pfui_Oo7GqSit+dJ8qd_euAUln zZ0$>#TRgYuv!N9**5F|}Cg{)nT`B3lyi1~rVjrq<<+X2bz||tx^)|b=ZJE6eE zT$0WvuscB!nR0SA*ck?DuXk({lodOTO~# zxM1hQ$M;ugtl99p*g&0~Nz{ob^XVOR zzO|Z1V({{e)a%d99dT+UX)k7Q9d8<58-Mj``tXsnw`Wd{%cY!7O`La)3YN@ii9bCG zzm}*`;E(C*(3hoAaf@sfKat}oOSpOE_2z^wm3`bYz`k$r-M z-CSc_D0B+OGiq+Y1oVY2nJ~VV+HgVs$qhCY3Ud1WogLbwwI!i%?C9!p6O%@8b9l}T z9qKlUfD5QU*9LR37`Q23mwqq8s{VRNTZ{8Yo`~e*d?8J%I<6r=8VwHh5ZQwvbjLf; zmF-oenLFN2^ZME0_9m6AakI(ZqS^Xe?F93z^fx)l`A=e123%)lZ@9A>->tKRfl7IO zDCTk;T~z#5sAKfRw<;oBlzNUShqm*y$Cp`zS|Vq_yGKN#Gk_Ao!6*!%uLzQ0f8XzaG|$3 zc;r0&tbs>%YT=B;>cdVh#RWdlMb}N__L)Nq>_I7;u@M#P>32Zzhy@eQD~oWl9vw9! z+I+(o#M3=|DBY;<=tccNn0h>C(!HX|XWV@D;@y5U4N;Trid#rx^E;gY{s4cWmKT2J zBhqg11$L9>fd9lLCBOB8^ctVHU;t@_3_%o6)op~_HH+}(3yUQ@p+{P^Sxxx#s>uuH z5=us*s^HH9C{E2jB7NYUg!7d(xBCTe=;h~Bn%2(+y;qVSO{j@h_RUSXe0WTnb3wb= z!q);JT!5Icxok;Yaz9YeX`wb$RB_>F!K6@usoCd3q(vgy#=V6~{|O6j;}Pvkr=Zr5 zcbf}egDbDXZeFwVXnT=iZOoxGzQs6@XrmhfvOATcMZHWZxCO~#Pmq8f#LHyQf58PV zo*Zjre^6q~NL_j`v$J3Hv3LG<&F|(&T0Mry?R!DN_;jWzx4~LKFCsv6fd_eQ-rpb?^!lFrsjWP{8GiGq=?kupp`cqmDBf54-bCM zw;B6*BIEWMaiL%nO=apwq2KVjvrTh|D?ZV;y|os?KVuWdB>U$4t+(bC&%#{(S$@JB z8_NTTObP4{ZfXn|n)A4lt`CB5l%oeeOMkmRc z_T;x@nRT@ymo~W5lQ6?}JjL1$67k27FaEfJ*R%cMkp%b_w`@Xb$>zF6Ce$beUBKHIT&d^8yN|?0P3$+{M;a*LkP!s(!Z@Dw zmHCT(8wr|s_}qi;vEVibK>lw;DX{tzG7?8Mtl|TKZB)%Nd~O7!(M=nkzi1uleuQT6 z<6G==bGxWtdqjtXket4>(qrcv{rBDe`b{>gbkcQ^e^pPwg~|*(UIR4gu^*juq zg{KxkkRdgvp$;{du|hM)>+$9^ZQ)EP5js54d;!6iNxO2)GT%RjXxvaZQ_XVoU*h|>zl3HSp>_0qKMRC*oyxm@hU9;RkZ61j3L#V6%vX-pkg*X)YF zmljCQbATw*l|@COeMe1p?>!vEtC}Hg0efHG14gUiYYjb?)pXR%x_Rz9+(Ig@O*XCk z?wnyT&c;O!bGprC7TUsaEjj|7jDr7l9{%1aAR((3jsc&M69Qf?(970(ALtHBLs5V} z-w-n|l#jnJra5W_r4gQHiA(Vm1UTKKDMT^JiEITRhd%a(SGE(%xh5@7OZrl7?Lq}< z=9ytqrq*cIS1*OTOV{uIr-%Lzu?(&rcNUA(*9}aQFt2D>nc) zj$7&8B&E<*xtl2&NH+?^J~l^p`&3ta0*gH1nuq)U=Wq1uyX|LsPyynYWy=%S zF5D9{hobd0rH|Yw4vM3#9PU@FGPMeL&^b=tN=+9RWE<3Gr02EoELp<6UKS#!a<2A=ekW?D;I}30Wh(LgQ`^t+R5HR1^eeUS=d+7;>IsTL8sINxmCb z@{jOXgzL<94lNdIy0&m<)_Os?o(rr|P90|;onTR;bt3u$Ag)i+o1c2sBH^b}xgLg5 zWemCbe1Of#aLDgd2AR=rGjpLMr2pM|9Pk7aTaVOhmbK9*K|g)pNxp2{0+S+nC&iuS zLNDAk(<|i=O6Hu{U>XyJaUbWP7?ho%(r!JT79BdmpU-kN?J?%6ies9n_ij7g!54-P z3a7G)YdbP8A9Vmxx%`CF1K#wmi;py5FvYPojKyh+!PaoEUdT`l7X?n6WC5IGnXC{O zn+Gbs1NHA79nW>6SL3fZ^s?SOi^U0?uJSTeUDetfg|}`edN7gVXXD}XWC0SB8eZW; zrE9V%oJ?c&D&YoJpQY1De%puref$3LNI@sRKyzt0ASqqb5HX5l($JskDXEq9$4A2S zc!0VpX4HiM=1k|F;>ARUe!LBKiiJmIrZM!+GzBb36r#n>RuZ7oen90wgMKFd{l z30G*?ujW)_Tz!7*V6f7M%Qf#ic7sKU27ZpolH^hmQ((G}ENsQkr@R(n&G5~uSQgBAyd`vK<_3jEs zZR*b_yRI*5__gvB8CmP34AS!3j_WT5vLsE!K7K;tG2x&uG!?%;$Iaa=LcOb`OCAh- zL-+IRQ@4Gyr4#ckI94)hsMYY3AFq~!arks8JfCr4Rvtl1C2&eDC;PYS-?#=8eH z0n^3{!+UKi@QH%wn+*ljnzb)4v1z4TIQREW;bw}j2qCzRMwyxxDs!P|v=2A9T=aRW zsxbpKX!Q@K6b#A?*C3PD_!tn+;1_V-wDlQ(jZz*PO7;5;biHI8+@rEzQf86))uB%9 z3zCe7sftv(&BvP)B$~YE^KwRr^M(MEkgW-z-Id-vVds;#_?^k3M@%DaGPvOW095B2 zASl3kpAof*9|lRYj`}#^A(K8Z@_i!8-TkcNNeg}r5j0zO4;+{6%F@}zs%=Wc_wFW` zT)TG^EUnzbGu~o6ni4!2?n+^1G3N}#)Z!bL4Cg<;Pq?mpwUMpxFjqNxVuHHPt{m~4 zzA;{6(wpY0P3WcRSxAR?kTSdF`1|#vvfwH$Hj`~+qj=c9kgL)YkZb^_6dPZu8!pPYYDZjcRnjQZ|PrE>SCGxf{>BLHS0rOSfTc6%k=y0FX z$t@z&ZfimWx1u|aml7?gYX>)Z;O~+JcRa6lTIGUjdD!=r(17zM&GAyM&CwGp@dSa? zGQsyv(8-e}Z+QgIcGV;r>1QZ{p`8VJP30H38kv z4GrSK7t~$_H4B4s!O_UU#i<1O;ACw7q=8;=vY#@rOJZ8A4~Oa4YsC1OCm8HfqU1Q&xeUfj4x`tFsq&aJg1#fOzD zn?Vm^{l>M?0~Ebpp{*n&IS8>%qT}WF26{(jMX8U^#Uf*~Q>|4L#j|sU34Od4>rKCX z`DjpP79Q2~JwL?Y#bPrO z>4I9O=wHZ>H&>-Bw6#QG_CJY;-uQR|b#x9iU@N3;4P?Ht99^qr&FB(b(+mHZ#4hIZ~9$!T{Nzw-eNegIR(# z-)$Bhvh#l>9q2=MNt9#b3G1CH`rG{{#ktFigh!Hw5>3sJl7ZIGl7W741t2unwFZOn zXu=48>|cAN$Dl1BxxxiQyF03mZNx|pRmAXObw@qZb=xkHal2M2lLah(!3ZP3nxXUJ z_)JrJF#@Z-94$(ge2kXqlY}WrPATB(2z8~{)g+CGC6a`|;=t*shTmNNcD}@< zC^GkY>7iK1n|^W|6x}iNg&ed=)>F0fHSFRZ4K!Rzc$Sc3pipuY$BF=MmxV3VzCKUX z?S)GgKd;nr9HTj}w%E#f2BXh<;@rUzZPC6|@@OyP!9%KiY_(rwlKObg+@`S45F;=# z@RSfFY3Dw$RF^(k_v+U0!%)*R&k9e{m)7z4E-_EPdZEtHUj8g8-@%h>uFTmeO(cHH ztux$?bcz~HlC#R%hVnVF;IBv7P!h5%JrX)sB6=OFQeI1sB_N;S^^*KUBHWk>x8-cvF7 zRG^~#>N#l$Dx|WjDh45#s}a zoJQhigOJS1-GCd!fcRoEhMmW- za#e(yr^yIsgxb{S)hComr8!~la;mo}C!=FgJ}63&)?9Ia?s2;Lvr8IKSo1vIP>Ne# zZOW#kQ;lirtE`?TWfv#S@nIu6YBWMd723)+BxlpEB-wNFu6N|x$tviejPtB3GK&)% z6JP(vif>lpN9Sy!MrB*dVv;OkaMkeg%KGzW7tNYZVNscSU#GVK>GizYj7vCqA40C_ z+ij0iyAVbTKNPLE87&p|u*T~RShwq2^YS|hKY|sASJ3De0~=%4R#qbKDCnvtk>-qO zuBjM)!z{@Tox-$vUz{tL0|;UkNddq1oz0xeCKfDcN{}Xa~ zh-~a{=0v-WCMKT~V`g#k!HCYQv#?x#)ee|3i`fgL33WR6P#&n%MF20ZMr|_efkRsL zK3e|nF0V>0=g@h;v3ePv#0HKZd9az$3BhN@{>Qp1uSOx>e}UP`duX7TbXG92Wr4b@ zM%#c&uv}Sc8sfs7vG&5MQT3*Kix0;@^(?n30diSwW9RQ7Zz#fBa!8{vH55mV9R{0S4K%*Obl8f$vc;c zV$8bu%5$};n+E4~9u#3W{rf;*S%_2v_3LA z`jZPVQWq8l;q0-xQx-kq>csLiesGVbuXv_FbZ;2o1Y7_bADi!TO@*qj>x$nbiHU4} zc(}Zv7KPSgvH$w%tz9hT(%jI{QBd*GNLf}>s-x1^dVb9(g3rcCsVl(VY@jGlXOmOf zN7Ow;U?%%!XyxTieZ}S%uh8TZ4>C4woMzzKQcSBD76G{|vC)wX(kq(Fi@ZKKUAI-k z_3Lb?jD__uvW`+DlQOdRC)Xz_*7Xj3Vn?(rBzY!JE0}~q@2#N2Ues&bOfV_AcKc}T z_1 zdrxJ;?nRF&UmB|%vfQ%O25j#L@uJ*&;mxms5@fxL&7)o<>^XNpxvzAiI+3Ju;`OHn z-XV)}r8GZMmhvpDDlW2*)}i0v5*8Xb%X9|C$Z}qm_-U@p4m1GLtg~(@D>&YK$ym@) zHO<+zN~wWHzs;L{r9z&}6y^-syG(;_QNb5pPn9Fhi0CGgOoE@`#oD4os7RxvvYD z)cd_dVwb3t0<H4OM zBqTLXV~S13DL>8gVm`Lk5@(4QDkQ2}*BNS<72IAJ)$RJWq!3(jP4RHHDn8FZ720cZ zt&2O$%x>~Yz?>fGaZ7k{*ts+PtTrn{WT*t}G^aTHQ5_99bUw)|yeOfYOQ%;v|5>Eg zGm7`|K%Z)vxA=2%sOAM=8F3tiU`zjii=My_ zE=w3gZheBZ=^%F6)buVwvqBs+&(Sf`h>3(pwM8ybbVid}G5lEp+4EZf@MX@_K-lnT zX~rCjDndAd2v~Pe|AFa?^Q8X${v%T=bmbk9A>P9t+u;5fY!YH^Wl_2+zo^49l1<`#nz%PE{s{Pvs64i zdv}zC*NHVW%{dzP?yc#dhjMr}@8?7IS@9?NN;{^X61Le+-|jP&SAE}5e#2Dp0!K!6 zl0EAiy@d0EaUHb>p}q}tlLQw0ek{+NqGt>);WfYU+uE-7hQOuoq81 z&eOa4f#Y35yOmOAX`- z^KU`yvfA8*YM);*6!NzqLeG>qx8C{~fi7QF)#s@ca<3V-tRL~unrD|9AuR<@XX&jZaD7(jKX6`gKEnZE&*mIjc0H7>p?)qKjLFJ9ju z_<6{&iqLEVEk=X1YwTx^m`AAf5OPy#pAp|*r$u@8KDOJjGK<7DZ5gxoLg6ZRG36EiGzXv$CBd;Gy9&)97vsyEHHiFgs_b0 zSHt$(y*|%Y8jmv*l{s$0OeQF6-&b_sV$jBsHAly7btD;3^N64btyAsAu2)g{AoL1aB%b{`0Ly3Bcu-A)O&xW?^`k;UF|4WIJ|(?F?O|&asYGyq!38tkyd1(LT-361#Tq z&Cy$1Jj*?^>=BxVEU2}*w4*tcw`(3ej=)3emZ^M-}Lj<+Pvy!lk-= z-QU;epxxi@?nY~fy==Dg6UZVkUyDhYbOey)MORU5I`pZZ zOV@*xc*42O#$cFTwH*Fgcg!3%kf|FK=AhsvAA+^jmnqCO|kjMv~rCa z2-e4Sta8KmD>u*MoaGmRy-w|uP$Ji)BN1>^qvjUqx#f$MX)=mNDML12o&7u&3)^lb zYNOqV7d6_-MjDZ>qRHMyuWw7)T{&HXDMlZ;dA=)P<(t6<@uatxO0IEJ)`D`d3yTR& z!}HMNn70MDGuy3J8&g=!BXhOcGL$v@(G~_P`Jp~bYUL!woo$Eo+rqN*9;Uh(1F2eQ zsQtt!*zJ)x^iNJ{jt*3!!w7!jAUF{q&1fU_PSpfiGLyVF$t!);YrCHfwogT+iQ@BY zOoP6^&qngI!EeA(9V#&3>7sb3GQuJF`3pPmBx&8m>(`J6nNx@@u)o!i0w_bdZC4)_ zMixd8C8;FKp4ZfTLle#Vx!dPqi@(4Xd>*=+?UMsdul~SCJl9y`lbA5HsloU>rpOos zPw*ZzFdNX*34(Znmb{X<(4}o+di)yzIax|QIV{tSSu`uTm@bZy<}kNELrZNohYRRk zt$0^ValM;oQ^<)3cXDrsr2B$z>%6~)!QrJ#nUVPUIg^@$C*`YVGoW}H1tq|<3)T=v z&AxK3K6PEkLDVKo@ky4(Tz$@0 zqm_r;7(d9u$^oIsdUL+jrYub*Ww&q9eMraeptC0;V#?DV4`Y7jG`V^*N;J=31{ zB<8r_a7UQQSVI)mt4i^$&5Z`=h*BL>988j~5_?q`%1OEa;lyJl%8Ol~-Gbg0!tz ze=%F@j4S(^j@_L27(=JH2_p+~;MaL^e$NMop&{%|zkQ#jy`G>+2d`zvS;4DqL$6gg zWXBevi^l1tg$La}(^b?=dU-*fX0 z2z!AoLo8BE!C4~Fn9rFfaqLo)@Ky<_MPZPfB0VKqd0ZkP9ZE_)8vvS0;fy@k&e$dk zO&xh$MmUk#niT&aR?iN|w!2T2E-207ML9TWXP$|7QXiMq8A9rgP7F~&(74(;sEBBM< zpLoV!$1-)eSE4IWEQdNb>%hhti)i%pq)d~Ub1d%c-gWQC&TN_ZJcV^EZ2oXiem^ZY z@p&-IUp5WN7=F4Wh8coEc$RCTWaKX!AY?Y6cN`xQ9kOosR(D-cCi}QJlr{1C9W=UH zhBY@g<@Kl1?Vr*fG-RW2Q=#N_FI)%aa1;#8s1Oplg`W?V=58#BIWu0@V#O*<6Uirg zr`^^{%4yulg{0}OQlBh=jhy2I@`w#Eo!4@m)VOQ$!!p#%UQ&M4ZmSZ9&WMna`+WAn z=ZkD07+jAL-}*ZkJo9Cxd~3k>p){?2TX6SAfvvMmLUYA6>a8u>H!6WnzK>yDQy2dU! z*AK8=7axg+8uvK=l3FY4jed5EbVIIzuzMRd>hBcFg{?uChQOj|5ThZ_Sf-*Nm>1NC z!}UQ2gs|{0ZwMqCnV$q6GfV~K?E8zsnN3|(Wwq^hK9_(UE^KxmuJ$M4zvFe?IVZ&r z-`0SB}2^H|IWx z4G}~Q2R6dcc%gW6*y<<8i6eCgCn4REviG~eK3W->QR<@bV z505-~l~9Big-c7Cz!z102h!Hzkn;6R1F?mNFf};X(j`AQz=&6u_<$dVy&4oC6$qf&0!U$YjqGG_rVmPAx}Zw{_d)>RmpM( zWtl$nAdO{AP>nBae+Orhn0KJ&EM`ztj;GXRrO}1fd1Xnj$!E=1Cw0;zE;KIkm*ast7@~nW zuZMg6502c*0G!goO=NiRGR^ZV=6Q7bZI_}E;hqODLsB8IR8^|q=`+5hw&?QTxp!m* z(l8O;5XBn>dI-m{ipS-LC>JMN68)g+d8P3~;C-gl)*guH%qS|p79h|qi{vC$u`kEK z+X*@tWRb!?BtD!`CaYx#^$D=Gp129_CCQrily_C!+sW8Y9_ggWm>Mc~H<6NVihj9; z!=z6h71N7V9e;!QvVx6YnBr@O2$9NhEr3>_mm%8SsSy+~B71U;jc3}@R8ALXx|eL4 zY4n=D3t~)$4mgeVbA-JA?{G8h5Y8#aorcl1CI7(|S|JJ~~4iU~PAF|-?iVO#C zk}bE%lu_~WgOx1prg66sQj#-to<0mUv$F(eEAhHDeu%XwP7j>u0!UUKalflp9`(K4 z*b7%N5_x=JI8idNu}~W;RF#?yy`^p`An4hn9BzjeQ+Y=)QO)#>#S4{Ymthe6-DE%e9;8d_mM*@(djT>~F4v z=aj-PQ&uC=D~FwlBloclr|LfzJBFTOy0Zdm+meGrF0wvys^Y2Ov6tylk15ENe7?o+ z*XbT6I5XpK|M|C`yDxQRK9)QtngwFaelheS!HWU3CvRe6h@%@h5j< z4D@2x=EO!{9o?lebR+Y=cceLYLkE&ec}u*A!9M&BnbeD+@ZbBf6!i!lE0pw%B5(ak#a@#90J~7&4Q;;eqtTC$ zf4qg#_K?GzI}7p`ul%lWQImsU;H4+;757UOLBaf4)BcTrEW>`LFnG!Oy|wo5MiuCl zX2bv#hAuwa@lN|OBI$Qhb)a0Qmw0CduR^zkExAV*Ii$J8Ta4qrU2Pbv5J{ralG zSAjNrPn7TIA8+U((gAORaYc`B!z9x1scF&qFC4%g_T1ycC)fjHZ7j4&egHSHG)owG z8`l`8YX3hFmi8=Ih29t~314_QSOtEE&#ukIg1}-TM!-2hWRg(ZV+Mp#IQ*%(rFFI@ zerJrl1zJ0+TENE78~5XsJO*|yTN>x3a(LbI2FZSXndR@Q=?>8BbF#_lK&l@B$Tbv# z*v|vT3N^CRag4`*5|@^=;nM}n@eT|9@l6%I5nk6#n|uX+ns<}Kw7_rEsw88JLd@55$#c%6Y3-n8|+LdYy=BTGN95h9#? z?m?1+v~Y2O(0f)5`QBd*rSWC(Khv?CjgEhWe$Yk=$7|iDeh<710}x;}{LIz@uXwO+ z*u2~G*Q$`S!>W8dczNZo*U^XIJnCsz*YxmZF{Now$ z30dHcGe3Qf&cXAPkKvDiPSYqTL7dO+f-YNG$o^!9%3K?O+&our4hA-96}(;p=*ZPW zxmdMA<8iKOw<8rO!bHor9t4aWK3RDW+?r~;zY5tNA`!@;JrZ8^&wlMJxGJ)1Z{PD7 z;1hsEOBm$4sriO=hjKxgCmaE#kP@em>st45S2*0uLqN808GQ!|nOrcan(2e=`|g*Z zC1M0_AmS^4CKZTiJ5R9WB?eRYx{rB|zaIy2v>WxiO8;JuKZ7CUUzK5)6&tq-=Bxlf zzUbP1AiN#%zO;|;&F*gKKnz}8g{a;FV(JTR85*1m5LQOP!w7U34c7#M=FQ9VxC@e~ zd{qU*2K!i;DK2lB;dwy+!P}a~UkB?y{7q!zAZ+5+=ta5X_W1cozyR%iMn$vm6wR`c zT+9nL1sjM9Mqr%8E=mi5wedtbaITzvP(p2l4$T1*fjsDM@BI)JN=FMK2?#@b70E53 zTUrjPM{)p^HdqzU7!_;)EzY8&0O=`06mXY~5fVC5y9TX?S>qlVj{C z$Bny!jnFSF3)6BrYucO^a{)@_H#eiGRuSg`sHy1Ml)4^F9?o73;MasCBdL4ePecEo z7rO%KWf7Blf#5P;MhN6&3j7EiUj)UnPg7zOM=F{&(Ei`^u{b9@S20!gV?KzvMnVgJ z_l)rRG$)O0AHp<-eK4uAS{Q5MC8D(rniwiCAFu;zoIMSdy;iwfX#~F=&b7e@Anr7;4jSG{4O2h`A2Q?Av zG(?I@y z{1o}Y5GLh*mP(>}N2RZFcp3@qWZZDIkz%H1&_qJwS$H$N(h~hVjYVr=w1G3>d_hd5 z1v;-VnV}at;!V_S&n4_m?I%h!y;65?dvdb+$=f9Af86j5x@)# z^KBRfu;Ld=p%Hnp>!9SBOmG^l&YiU_;M8hmIrjZ&Y5#{)6fXwXnT4>y^gcN}?gITH z^!?_Cv>XCG7QIMdPeX-Me7tbTZQXJKSj&c$*8sYFAh70n0T!CsshdTw#D zqi!#~P`?GR-({mOoHs8|xa1d!pxrP)tQYBZ&b>1Zx*ZJ%6)ryN=|uI}mOyuup&J12 zsWQGXEKFc4y}FtCDM64-?vT#k#I}Fil*d2}XCuxPp|2#IhXnZQ`k3&RDZ+APeU31z zH2i*oY!CC{^lBxGMX?khZCRrb<8dL>+Cej8ghxElnHkjs2>Q&|ymIJ#|IXw1Mc?H+ zqQErO&kmq}R$+XkdzvD+?5!UTjTKuCJejVr#J}wApZzG?M}iyV^)g43UncJkK3kCw zWknk(ocjj!V9u*Z$!*=qOYLZMUY<7HWeyblD2+U`kmq+=seW<+G8cKA7flwRH>L-| z9&$T5;UO@bQ)d{Vc_^8^#aZw$97*T3OGFAD{!{-%g8$h-#52IbMBRFR%LfH3_Px$( zt=Iwdks=W!X3|_GMV=d7QrP*zPzJea-NK{joIGfhPk#}Wo&i1a{0JpH3Z?hIeX=z?D0SUKuKZxTQDd|QPuQB9K0NLQ zTL1g+m*s!DKprBVa3s-$*B9=i-;n0aKu5Ji-CeV^CK&C}3L+mZ?8k9QB1^ddB@FrY zLRg8Pmt~8e#lLPHs5a=Q=P)lOTNYUK zT!RrD?CaDrcY!4L@do97Fh=;FDPq0ww9u#h+5h_ncR)vunw)E6i~_@%3Z5J9_O}&a z^%ej?=-scGm3*!T$k2$Zf5Ed3&|f_imMz}Zqq<>s`;}aao)`7@P-8i_CYivC)hf2n zL}tEm$oNivgI4tcdDk``{<26YihSb_G&i_@uPDP*=#s8vyfzYToQ#?s2GDEedyC6| zzu}*d8!q2SOtOCuNe<=ByYO9L-YbLZ_<-m)jzc=+4WFoM5PR#eO4+x=z zaqVeH&(BPO^^fqro!1i-^rB$eWQ1<1b4Nh}r|!<8ke>t_q0}a7e4D^(RRQY(tvOEl zXiYYrEk_KS6|kj^R_90ZE@)Q=Tf^00j;r9Xi^J23(C0hz_xBnh{{~`h7I1bHv#M-< zBqro%o1co4EWnr+-me?aymywO{RLQH%~+l4?XVrB8MRg|+HT(fuO2J7hV)GTW+oKR zqP_*i>&gpwZzD+E>_OFJ0^t{T9^OpJV71yC z+Gydt*f#@U`jFRgc^?GZ)o=e58rBki3t})lDB-Xjn@Y1FA0Umcc)jC_p*3$#ZdmTl zx_}%ss1DQKEj=I-|B0Z#{Oh~6Z?OS*S|xmeFL?3unWX=v_$1ya8s+{ErKT5e5S;t^y_?3<41IH}x=mr&g`dFU>*&SWLBj5* zOwa$-VM0v);gT1j+-{xiChXK#BGkgusTask@1P8zn^D5+?1_o`54OQ*$(|@YEsB%*JMG)QJP?DgNE-dkFN~rO--9R|d4DT_ycO)l4X&~j)}@63N3Xm>Skm5Tb&0<#>I|OB!XLIDm;L|t zxayN*#a{`{ZLm;7=F5JF{?pAfh!P~ZRlN2iB#zWB@$tS8y?!E5+L$WgGK_&*(mB4yzr|I=5aTws1|gOj1^?{^J64KX>>(yo0%C#0G` zw^D9z-2Ls1yMO$a*v=!cFjTm-Ild#*|FjBst%KsjrhiIY^@?qhTN#Z)4$sP|3bm+( z+SH#<`eU8RPbf|j9lb3T6#qVnb^9|ZsicPeXelK>-Pw5rRT1Pa2N74iS-6fR#4%TF z&?pG|&rL{g+~$qdx15#p$mAIdQDO{|@&x)sX9gr62u6*xG~Am$9`g?^3YPfjkIftj zKN)Zj+039r$Ywq|q_wvFqlXA1oN2bp>9hI&^=n8y16y@jp5(U=NCm}*?Zb^a<*%}B zs!pge_|w~;IP@go2kvqSzRUS956}Pc_DwEDj&6A%nVhc~qfzZ6LI8Nr%wrk$^8g#m z>61+$5W!DpKXL$^CY^e~{q3_TjQW<7=mz8Kw{4(QEIg88 z$fl|phu&fg5?-ksx0Yn&9LDzcnxAR^Lsd47TtfQ3M_s0$W#Xxc++CgF)3JSUi zUg_)Tojd#2?U#zLfTYbBnz;a>IC;~0{MSN@Xdt*iNSOC_7}L&) zFEpKMe6aE6wuL5V1=l#(zW;XvEi^arX|-b#x$SmFN2myhA-D;xxbpk*QX*S-z({__ zj3{g`?@!EDs$_e4FaPh$3)L$2vTxVgpjVP`Iq0`#W>BPquRC&UBz1c?msC8+1SZEy zIL)?i>Susn9`K(2FK2F1ViabQa1wTtU*NMP{&79w(qtepQ*GVr?XNx=fk$uOo=a@} zYWsuk!HGG9HQV$1-HcnY{rcU^4zZ{FxerM^2_Y(x`S7gy*Uov~2UID259jF}zjB)3 zcDItksYruubnt{OoR|{kW53&f*TJ^83+Ww*$(yX_U?(JJ`Vuwty}AVCxJREpVCR7D zG^zjB?+Gk6GI=oysuZVk{C-;zJkNOq6$@mQ+`bv9ame!vkHsi$pOq6x*iypB_`g3t z93ampVqw3Gp)zxYA7NTF5C8V*RXhdnG_~iflfkOtGm?J_}sefPW zG{ljs%2CB^KQPmC*u&t%!BsogRPh>40-pZqTjkRMfj0; zhIZld+3hn@d%VJr_Mfx3hk^zCg4o*^1*f-{g!On15Q3wiW4s$=P}oO7&yx%3iv{qk zKDq>A=42>WEH@!N^hi>t0R$^UeyXo$Ca)vP6$v5AOPS;=SiUvdM;X&mUt_bT-=tl{ z$In-lr-_kZh=H@(%7YS^6GF(cN*CzeW0!`O8Y z2`f{14x=%|~ z-m{>F-u+^0APgmDL~cyA&b+*96OMjurXC(EO;uDZZ;RMpOOvN*L-PW z04#n^L|JP{`l(XcRA#eK`dTlj(M?-{xTv!llU%|%pFW@f@fKU85?F?un}n?K2%q({ z(k4KzEs#uaJtZeb%*+e~&l=IGhiLTwNz>aoFg^z=-Zfq70Y5JAeJQ)Q|U zl!7_!CflX*5hN?KKL6U=`xPGdL#6rw(3&dA5~*)axFiQ4qos*_ovfLYIBFD8IcduL z5V+j+74voqu&|eXKvE4upqqSx=yd`AwU$yB2Q6lKIm{T!Cqj)Z+Mu%9C21X+1Xo^;VR9s0n{QP`6375l|NQKfBQ-M8BA2J@7If!Yq$`_h zylspTFcy;ss@D@I5Pit?^4_!&f4(rAnDbcdGWtwfT5lM^UGy>MsC=a%z^8Q@;0>8eISaAI{o5XYv{&wU12*Qp>=Ez~h?suvyPk&y~Z(98PZ zQ{B_2}o+;4nQjVcc$=ArYh}bCD3x#TpTmUtDrbKKu z5xC3H+O7l?nooZz>c~swYv)c{{mO$;Xt&NRI&CZGqy?(O)1?VES0ON*+Q|7LjlL{Nk8Ctq;gi?2&_nzhT*P$F*Tmy|ljsfeH z4*-A~2#$ORW$Vo6_bZvtvtH_R8GO~HTXz19Q_PC(twl2jJ%~F53LftDd;=BFJ|L81 z5d{0qQsBN{Q7IDlKLfXL=#l{M(jN-}83`|*K-BnKng*1V@d z5mE;_BIx{5=f0vS0Nx-SrQ+iYvkPo#YQL6ceYszG78r%40yydrG21Nbkgf0NZe-vC zk45siPLSJsOzgi}pi}Cc2X!Oo(V9Ua=5r~{56kJ%Dy2~J$PYwQjR+*`ss|xOBl{x~ z?rl!cp5uO_QqGE45*0zwu5`GzkH{wqnanJ*%w#_(t&4POD?!5~^PSJ60);1qDdUw> zrMi5~(Gu_FHMxyqu&OEcr)~(=9>RS5ht(9Af?skHc{2lX8Cv#`{P(t3bdhSC5b4Jz6-mw*w()RjHYw?=XLVcAe?M zh}ii-hKB_TS=s0><8u<45}2`h9tT+;Z*6XlQXvo{kUh!ce+3l2vROjX2teDGz!KD6 z4OL0dgG^j08@78<_>`&h1*!J97fHz?F_qS4^D0-RPBqF1t9x7#<>AIs3OQIUU4dtc zd>ptbV8Yi0bx9p$p?G(7nl0$G|(_xcp=)_JMa@KhHoNoyC} za&?56M#mdc-aO3i4vN_#2(cPqEDsiVGV}tweM;MHH*jf{;bLxR?=*sh(aT*A0#c?d z0KprD{>oI&Xc(UK+A7=pEe!|2o7qP=Zb%uqxQDl+NHA{|898)n*mIJnu6in=8rl^G z{LDbYLvl&ZwPg-PrEhauIgS@#;hHSB!qHOResbKP5Yd3QL5*^rFyjNZyz8$3JBSbcs6t(@j~147Zvd&1|u>l=~#z@jl#^;e?qr|dnNcVoPys2>n0 zp^0Gg_jY_vK6!$hXz*qPZ$+|4=-N~!;TX%y32xdZnimVcBQI5-v2>$Q-6b8HC6sm1 z>JVS~6Zn}STbk_?Zn~x26{U*>){>5OXZb@MuXgg}twznRloHAh${sD{T$4wPqYb<* zHm6RRoEcniry-{iu+K0B=?Eh#s;6yaC0(AOOo$p)EDC2r7Fb>vE&%i1Nu!3AH;*H)fc zZdCiWRv2{GX0W@ksuZGc4Kf7qjt|=}`rgq#d2U5hQJinCi7Nm!`&rY;00iMiA9f1U zSfVCeVyrLSFEAHfB%%?N?uB8$IRX-;J|skY59{x{_(jC!FdH);6fD+<20z55&!2n{ zU%0Sb=dyPz{J||!Yt}dQE^@2s)g#4JpO4YI+r5m(Q_q@W?s6(Ixac65t{m=lAN`s+ zHC5PUSJa?O1>43X_j`3Vw$yC?z|cFGj-uPw-$N(Fk~_?2H55ynb+%?~i?0oEOH8d+ zTGzpupB>^>`45W;x%ofcMhK7-EWtobPabS^;^;Q?hlrmcaP7y$8)Q3q$>Xz#2MXDn zV=6%*-!dPWmG(G{CBca`yRhpiVRvPv#Nf@qN=sbwC~3;8B=%DEd%22s=X^eOX6ZyZ z@#%TUy=J6sW7EOW<^_g^tJ)YG#dOSWIfY=tI z)-OibEOA>4?y``x-TWO~_B@6{{K6w0W8R%5ytaKZyS+#8ew<820n?Jp|caYPd1*U{{Bavw+la+@hVz=*|j(zuu9pdKZNhp$QKf=>$cMF^Et#jCV z>naHFD3FC>mcFmUkZSCkLjYSJLk2s(MJT9%T^wNgfh-_#b7}#>6)2?)e&W2j4Hx0I zR}Poh@*|0^Wk3$&Ad5Ug?trXN|JkEak{l=2coYl-%r#cfI$w*~Wpi2wT1Q&QL(K}s zFq-^I9StujCg6G1QT1NkirR91YlAp_;pwsL3$EUCMS>y7l1&C(35&Q3b1L&6_B zrCx@obv|qL`+USm@nA_p1ybm2%)>dG9;pZR>eV@GqV9eC;|y)(?(PDrY#;d{P0t0f z)*d&eyQw#W(DAR(Uh|!KW0g;O&suq~jq4&yDt!yLcML~?b6=fR+PX;P+~mAM#;?wo zReHP2=y^5-Dzy0d*Ng?$+@Nlv_538ko;QH@9Xc2^u$@=!cq%AJ-610m-@9`faVHVK zg)@BXwDT)WDI(;9QO>()RzvL1DLQwSX`e-^5rv04F$FD`+^gu&3P)&sv;!7yALDXrktuN#;x}N z8Y|EDKaRFm?^dMz-i}Zbs%~bDXo{(B%&VC%tYjYUx;xr8qvzwq$?O)HLglkd^q0Is4JthnEIII4#(gr6T}VR@$Agdvhz~ zF;?S4yrFNRu5m~=`}Wxj*{X%~b7w?J9|C8C);Q-{=&t{(bAMLCOAFqOv^dqYP!waZ zKvenNdGL#0B^T7(>UuAWMnciDCk}!@*dg{aknn0(;SEZlxB zq?7M#@rcTT&W6%1i$*B}NsNxl(ise06pV(^x!;}_KkUSM7p22#rKh9iQ#4AW7d@2; zqImXcSKYZe9qJLO4yJP&vuF$cIFkKEHtE#Sv+6#XB`eQd28K=(SZr-cLuEN+y_a=S zleAB0XZ6gMs{)ah{_P&`o%8gAo8b8vePZO2*W-Qny?>Mxl#uxM@=@xjou7kPJYmt6 z)a=RQ#m)kT#R*_lUY(4Y(`Cy<7E_#2vY8gLfLiEBfQO)0oG3Y{wN{WU;7x;)AEc;Q)1?JTL~Rgttl7wjD$yr0*}TAyjb$=F;or z1H~SLra96t!9@g7jU%Ln%(BniQmI2DODLYU9ag<1n^19WmxWKa(=Po6JwijV$ zK=jI$kz=_$TK{x+H$}U0MNP3PE&yCkdxp!|amP)M4utsyB9=4U$(6J(TANvJz&m*~J77|J6dy)3ahl4R}%tdT~GNj0|@~MjwtNk|ZBxIj3{KRqJg7t)f98IHVqr7+#a7UETp>nT?@m z@2XgQ7zYMX9=u6-Ub{?T$^7wXl{v$~AADogpsi@*ctm1IbVF|F6)YxF-}>!bBBW|{B% zmnu16b4^Bz8Ki@b&q)<-><=BaYP}ZLR4fPeMaP_&1;~-#u&?5tJ85MNGuuAf;3nG~ zCA%SRM>=VI-pHUUD$Bs%6p)~L{t%WIr2vpf)aBP!X?e0WE>YOaA=4+8sjYxE`IVi1 zGJmBkD=4bcqlikU&Thkrm8xha?5v(u*ieJrkR$7+={h3iH%rImHs|`x1$5Qu^Xr5>rQCElfH$Fl0*M(Lx@KUP`SDU(5I+!yU<@pT-}y_6V!}b}?H~ zAm)-AIt5XNW%_VJxZt+=c~_$7L6FD3_`0_rC>EBDtckx>xRG#9D(FWQ+UrzU z*7l84-G)e1kKQ}D!@mc*IXv~HQ@@v}{(p+a|3@hne~||eTEMS!7IYX=&wCMQ{+*ih zE(GS>_2k|DZ8K{o_5rFZ{;NQ~mW6Q2{MyA3Izs$#=?7co3rpiIRWGdudJ)Rt8Qqds z{4xIzYi}JE<@&V^1A-!mqEdn=Dk>p@w5Wh!(K(-=(j8kw1r>t^3CTf9VCdK= zsDvopAyOjJUEjI}759Fh?>T#4hKJKJa<}qckuSkJ?!@>A8{N$?;TCHN1c{*3s;-9R8x&bt>su_ z^BTQnlLam$Idw5rfJ;V(>K+wGZ&@_?cG*i8BqeDxJ^jeNblqMIoNj=_>xWF&kgcNw zeSCMr>YbJbH=jY9QlOoBE7RKp0!p=@U8w-~g`@R)$wx}Y=i{_!H;3@)aPHJDP7l>W z<$1-dIoSU0u7j4&v)_!;bW1JK;C{*anu~6G{O)xMM|VFFJ5s_{8s-p>wIPp_C5O`d zBcYq$%EP($Ut5zh9Ny1JrR*n$4fhb*jU)ZQ4dRU6y=P?}t1TXFvMNB+$_G8rhYgKY z{ETFLM*3-=KcKE6N{YU&Lw#S}nD&}8qRu2D=pi$_iNgDL?{_PbzUcYVQbEy*8guTT z;KtBHt68ZC4e9Se&$s_k|3(&aM*-e%)wnZBw3iFl;MH@@$C*TL537R&Hu7)vk&zM( zGX|2#Ke=^O2dIZGNS@3kJ6l@OLy6~0f_0Zb)b{@uw+vzx#|tYC(YHRpV#xR^@&la# z#H>n@uS5KjZBW$8>Kj6d>k7=UocP}_EeHsiOv7L)>d1z}bF45mGYX|94SCJiGW-@G z&?QjRyslsS4C2vLf^Z{vvvsv z#Bl@5FcwJvzkforExNgfn8_?EQn^S&zI4hKSHX=OZukrX*USp;FvNvKLDJ`DVFn`% zcxR2i>ud<`fv7}l9Sg9vBYHb&%j-i(HADI`5?uR{vX1W*A+ zDWA>E@Ml%dyb^L`WRrAsnBFYFJsU%#vO?r-U?%K#hQgypQh&N1Ff)DhK7n5UEb)C6 zmvyQ92&_Y<>jG+?3V|&%c8odz7{)bIS-bE1tY`^$(|M`wW1jOY{_}L~kSP!q|L^a| zak^IIAs2=dMC5l~)+Zy^qne%?wXpIW``(s?+JwmoE&y4-STNu%L5%lVaA)501WXqP?9p^L2T-S zP`As_RnxP8eVQ{T3Z)eRm=R&o!-5_|7k?v*p4xyAhI{62cTvK3c04+s6D07~I1}67^& ziyO{ZSsHj@L?|}S>^|BB1ayaN#$d}?7}wkRF8Z(_6&YXPwG&Wx)`)7~DzcgfQz3|G z8|SG&zg1$PcMDbpQB(`&b~~FlpNCxwgH~n01D()r2P7*ak*~B1o4>;t62IO|n2P66 z?gEQ6W;?HLi3QA#pBDhW$}cQms#*~~tm`TNp^aMAuk+d~4+iJ_mZ=0Jh-JZcBpl+g z5nRyj^n$W{Tk0;%QCUo@DAisz619+EU6VqR4v_Dvfv&T;U7NK1At0n% zfQahtn8$vA|C_l$_*5uD`4ja zxsN~mX$je701>JIKcP_UJSz|9iJ#^1WX3(7$u(6l9`4-p)A&+ozCur1B)9XnpYAI_juDZa$$0(fg7( za2a_L8|Fgk>q*c9xZL{9-0Lu%VI;bhK1PKWuHFC`q}z9*K|15*xiNb_G?$3x*H`DE zFck_R77K13SZlRQT-y)}@O}yR&TR9RkEj&2rsgYR8-(*y7U|c*-gE9y6;+H8!-zFb zqA5qPn|2b}=UnO2S1Q;bwlSI1FC;1{ip9s;j8=mkVch zy74sFC%G4V*04hd=nu+sSSsId>x}DDyzAxbA`fo(k72-17iP=vK>nEMqBs0>8Z7gd zg4Z=fxDwkzi0BtNtaolanStFv0=lM$$K$^hzn9SM2acS!j(Kt04f znut89fQjD+>BTRusYxec*o-fCu8chx_Vmi~X{im1pg&_P(aCXz4q{xL1;S zF3hg`BX;o$p>j+n!iaXn8yQgEQR)Tx(o=vR30(yBKFg({C(MP3erXFRUv<@Yw9LM$h|6*gL#M7*pOxU9j)M^ZO@VXto)n#}_Moke^y96C#% z4Gr4CT0-KDV^7gRv}dV;$x+7aggT_o^w(!f3@ulD(wqSuXi*0|6ZlV*UwU{*2@C0` z1TL@%0%i>u16FH-N(cTh0TRHdQD)E&Rbq(#Z4ySU7#`ahLR$wStV!C|Y}04gKth@( zo%3zwlTs4Wkd;)?ykN-#hagz-tEm@vYH{sdH?eWeV{5 zoxfNZQA)A^fbQCZm#=T6M+uM~-ArTIz_#B@l*C{H7m=|aZ-Fz^QIS9T+HTM7ltIS3 zIZ*sy{r=+^l3`PTgV@hJPD*pP9`ykjd7h}8fsYnJJ&5>V9eyY{zu9QI{@i5`&=eZ` z{$QdPcq;`y;Y84r4fevfS9&GZrzL8Ep2UJe7-P`?9LCp|?96F3Jgh5*Q*2GEjMY7sV@~EKax8-dr#7(o3sf8-6Ua^ns{*+RWZ_(y z)?XRGTXftLpruR!JJ#oZS!IqdYmSrII}IehgbY9F+rntAVXpFzwFX*TL4=Y0kJs9m ztX)bSdtVsTRS?G4IE&g?j9ky#Ivzt_bqJnu$P}1)4RB}^iwZC z4YY#-P#O_{6M`RNr?UTeE6MA@#Qz|$j-CQ?Mgc$uKW_W(9l8@9wU#M8&`f}+@j3zG1$jMPWA!L znjn8W!@`X%{Q++JY0*;6M&ek55Q2O_ph@WPHDghE)J{QJcFUKI zRvWg{O7jmuc@9E$F!vSY>pp`_*`ZQuX~&?RaR+J9iU+&y1LsDtCn;ONe$v$#BM2Gq zQdxYsW@AjOy%RUsga?^>X)o-zNa6rsfFU?F86CGlVmY4Hn3RGZ=_Bl0Ee*q6ut}#Q z4e7iT;U#$$Jjpb6U#?q{RBh=}Q~FdPN1Vwb+-h9ItKkeFoOvGgarc!V0GF6rjl!ck zYCcuN@H+})`@Hd;LBk#QKcMp1eaJR~7O*75iA673>YLOmxTDvhGz1hJS+tL{r=*XN zs4WWVkyFxfgONo=bcovmti~9UiA8O?`m7cAQkHsC4cn_QT3n?xM9D4(e)M&7c-uyt zgKl(1yKJ0sKUh;C?Lwiaj`ti;1ZV++1hX8p5NlzVq5|wgoe*O&B)j**<;Q6(t5nlr z+0(YGC|G;6|Fl7PC@>IoF&|MYWFCD$&s_slnYZoR@~m&1sYGlKXXn8Iq2Y#WLm^pP z6eZcb;W-}8ySZZ@x^cEsWQ-@Z_&gw@A9KZ6U+%bn+YsE;n@BH7TUbn+^vG_v2nmnw z7Yk?*2b8kU&R;wv{$_l>@X!mhin~xLG?=mVmz*L;ICcWeio;AwQI1LFFDsI)-!LMc z5eK^Ic`DE(s?7$`IPNNx{n~$TeKKi7(F#&xmT(|_rN(n9d~eUGCd}3d)P(?zs0mJU zXkeHUyu%nN%Ixl4tSzoziYW)N?@*BQ4*Brx_)rPkSCa)Wl&=k`?rOL%ClF^5=&Sfy-M6(i zgNBFwcsH&PJ^5d1RXLfNYRRFxt-P1T4R%^$*fVN=Zi7iNnAH; zhSprGsB4f@s9?UfySK7`+`3rvkVIZwHsUWyQQ~8%238Ua%u?bD=c<|SZ36QY5lL! ziobdab>m?-e}Oc_Mnd4(3d9ZrRu!QeAEPeS3*!NUrR&S35AoxIpw|uJkEMIwP`X8t z>q(Vf*g@;{3W~WoJ^P(CRWKMMRGQA+m8zM3z+o@hY4lO-U&zU6C*<_dOgZGe_AU%u z8H@3MLI~~8+BYxhXu!%E4I~JVKjqTIK)@`X)qo^o>f>hDa zENG#IgQ8iB=sHvojNyns-SOdgnf*W&eHQGu!qiJkF=Nsg{WOq`oqIWhzx@O5>U6O8 zUIXAR;koNq^_;j<4LG9@9$uWP=1ndSGO~k--zkEL>UOj-OFpG7T?f%b#2vKv_GLZ= zq}RC^bA<(Fc!QDOb_oIz$**=a$N$LkX}7@xvc7t8koFiUPFfJEbn1E44;6R=L@YaR z(pgSh9>Ui-z*dAU88t6hwOg-24-s5Iy$^Fk3-F5Ra5P^1I0;fOGHK+T+sQZqVfetDG%D0&jCYjF;lc>Gg-*QOV2~N8j^IH(h*g#CP+=o<*pnape zSYbf^1SPeh@dnY97j*U3Ru%_=jRz2pv86#Zn@N8(G@~qhoUDaG$DHnmYT1c%arV7s zq%5k{be2GR8}T_xZu*=zWd^5(MydO%nY1W44*)$}e5kJw!uL0lg>P?x*1UHA056b= zYCfO-g5>rTQ$A=B7y8S|3`7zH+NbyoqpLT`_^eBpLve2{TZJrk0t;A85;BcT6 zQZB?o^|GYKzjT9KOwoao{TfVjCdh{x0JbvBkZL1bAO;ixXrZ@bZzZrKNkZm`Ga84( zQDgKS3;8G{6qOPVc&UysA@eC_U0PBCsF;QUi;_milDfx}T>G)>FEjIxsQKgr6=U-n zoeki)Qyu~dhEDYdnNz6mkwsx7wqY)eSMzVjw`Jb&?LtBL?Ug9BEy;=75^FPtwj|n` z2%gee_-Qze?@0O`Sa;c^hJO<7c}m8D-5)i}549*GLnK+!cXuRz_H*D?^y!3b(KwfL z>cEmr?`fOCuR`TOybcvN0noY?1d&+0@Lg%&ouSb0Oe~puyH}|-I(+_jy_Jpod9Sotp6@roED;WJ~9kxzmdb{@Mwh3t{NOai#u!PZzHH3-$Yny*y>p z%O2I~2RRyD7>fTQ7XPk8!D@Kf`$I4k#Zv_((ufQOf7C0YzoTeHQPKC%?)P*nT*fy? zThc`e9mdmU>)D{9OqC5{vrS7ax`RAGr4EaVDwpiDp<%yLO%R*3aRrT-~NZ0`+R7f7Ch zJ_2`?(78X36r#`gbx^V!kS$g7;0}K4a?Wbyj1aXhw`{xCvD*+peP+PpmD#S9O+O6o z#LVKLGjP5MfJDR@b*>Eh!Y^8kzs!p0;{w=8S3N5sFRGpFe}}6$fKf<;xaKLPs?AZ7 zXYY+n>QC*yvNWACK0uYOUFzNi?!65pmyaCdt`_8i%4F)(rJ2nrlz54rlDpx)A4v8; zeNSyj{hVGP6M~5lR9J#i@vGo%`}EWjQM(ip@$}}>ax-ueujX<<@--BY2W{yPBpK)A zAA1C!@g)ymLu3PBlObF3SX^HlUvK|*uKkjF$NgiISDZT zUT&WA-kEU@W}Z>6MQEu-=~N*V-I4?5!5P>|=N^-UTU9Gb??z3ZNeaV-4NGUbF*n|0 zg$Ah2V~~ou%$cVLJZ0LQ1$rE}vo9_dyDq-YhZ4BZw~ak8vPn(wkO9*!`Ucb2&2q3voj+N#1x7KF1HKx z7>%Jvo?{toySCyaVAWmJ$h1A<;$VR0Ujtp=zHe~kKrATd~=<*P*fXUskXOtn8#5Vv23ri|UlE!3ol;_1T5w{x;k znbmFuglYT1s(#5bC0ILmvwDU<$`r&WWCT&X##X}u%D^&MH#Ayn?mq&w<8!=4W@oZl zpQP2zFzVyG6Y#r2A4LOv)dmrJLz8IVvQm$@`f+ePO}Gzw8*FzAwi^cTSx2_ymhPVM z6~az}(P~L4L}KFedp-_6W%ArUB+JJ|PQ`eoeL96|9@5zi<*q`9T&!<1q{6pE(l=U!S~`~vnKq7&i=p<_Wvp(AR#-7 z*eFOJ%N;q+XaD^wDz!8Bm)stBa4kdcyxH=D_G1Y=7w8$}Bv6DAbW+<4DxH}AtlMx< zv04J!ApvIfg_b04^?JhNRmefmk0t`W*;j@L(jx)Buy@Y}i2fW?y0=}c$nkU0(zldk zDhZiZML`+Tkb7N^b})KHkjXf?7kAu+RKLCkwBZrabfVIJCD@@Wcu*JW4mq%4OTR6a zb^@q-!_y-K8E$)laN*y8HY=#)P>K-M2RKSl%kb>pav*=>2X~Zey@u*o5O|rnx&h#3 z5Kgc3$yxcoU(5gHh{(*8j=4N`l{Ten=Fu(bj5zD`x_vfG8U6z@ZhnUm1X=>)Z|>_Agok$2m!vEFHEnV1e}0a9sN($325Vxwr2s{%twWb-{8LYe#kyODL!sSf0h;|1+N|r8o(F zcXj;*XrB>GfkO>Au6w%f#1^wdo=jTHMoTHl_MPAv%;1&~sz1Yo(MJ9GA9wAU4tMD~ z5O<2WqW2Jzpw;`-Pm;F>bgii22xV;JbPY!->;q^sSnE1RL3$Wn5zeK+I;jWzpt$=tVJSX{(V zUi}-4F74VQtW|Ln8n8i>F7A_WU$dh282A6;wogR!jHA>XfsWLxsWLdB_BbP;(6acK*CDj)&QY?-Jb>K2n~ewTEd> zdvTmArCNortTO+R2hFJ(Fe?k_neRQlEK38@lM_?_)iQhCpnq-*2vL_^}EDL@6ILKM~yBpiYM9G}xE2J&)t+U6lm zQXQY>G_8DtH$^}4Kn)P-h-^;_ytNm~>$!Jg-p;847p3;~V~El5ch>?k9EOcjDRp(# z2G6dSESzE~cf{N_{1z>%7xWOqVD>8n+;694c;Xw3ky?mCQ(O71J3=ITx3W}vO14sz z&#jobC{whk+tvEm0g}tidHx3uWzmpbu-eyudRzw;>6oZn6kCDgmu%Q}gXQh_r39S# zS+H$h-P_UzAi=iARBVeYfcWMpxVOOt2c&~*ag7(dOu&su$y9GCSS9ul?fTb3@c;mHEs-Dg)7Vua?avA8h+$?-vCErc)7NFKLR(Qaw4 z9GhS65@ik)gZZy`s@uet`tOSZPj{Mpqr-yz$V=+;9xA3!&7h)VbE)G;-b+XfF7b-b zmaFkzob`r)r1+>El)+u50Xh~2rQ^8v&$?Wx*Rm4g>|frp2DG*UM5^DFq(_gxREiWc zg2uoepBTn-msX!UKoFzWV$SjmgeVxg4lo`@6t|t*em*7TB+s>1FWEW((qs%N1715# zVw~gspcKxBp#6O@aAz6a8OtS9xXT;UoiZpiDOgp{pvdT2AC+GTZj{AuEsN@R9m>zeNyG@G}#s zVMHkQvooKhFLgM?9pu-Aj1z%hT%)|pZ!gJZ%3|`WZ6A-UK?ycy5n>9rC|;UlmtiN~ zZVn6GFvH$lPdr`ZS*Lne;CUGY(;kGzsJ)BIxC)>imZi^9<&5;qa^HikOpDLHFX=?? z?e0#mVN=Ra(LD2)^-|1*)ld3}=^TO)HLP#YWj3c`)J<^m&Nq?w76&)}{R<&C^HET!1 zqI})II74CmQsA*}*J%Zb@RLcYCNG>&XQwkQ|Af|+`r_}rNv9Mq^#UqG*zy;&Z9-1c zqxXDLQ-^>q8PoW%Z0mD8l9J>wdd$NI)XNwAJBcb^=hJp&sy6RUb`vt$*JxM4m6 z+v`kos2maUPCZ7D6=7ZxSa{Ws0XLdFcFu+s)efPkqlkNw%$Nz?DVX-1*WrqrLv?%z zw3+8`YX-ibVLT(IzxBw1O@H}5MF|fMj{n@4BdnIA zG88X?fzWu1S`I5!;VS5>%0$NAde$L(S^pSL<~oF(P!bQMQg`Z56WTHx>V9*1M@|#W za${K)|31OmO=UgOp@J{KB8rjr2teD@z_hZb%e#cKU@1$g^1NGtQpQyJPAP};ZuNk^ zuQ}8tf}cnQHq}d;{J7v#c6Y}JtQoj>O8{#9UQ2lr!s&WOO$03kD z##atUK3mha2@|z8FsoLzlKyD@_5G6skn7y{&KfGCEbe#w9qeHQDDg^WI*XJBK=NdzaQ}JCI2_K4;}(%lQ-&A&k>)j2B`}GBydLs z-L&bOwS)wH5)L?h-Mt(v3Pn>Ps^}43*BO3I(vz4;RY`MLX+%9s_!fe9caqj#3ghz4 ze-oL$h)R?iry3kvfNgX}2xG1mFN}GLxHWPz^8*5@HS>GJ)?)Qx4$FQA)Vy*qmDzUq zos49oaGt4L6vnyWOCwbE6g|!dy5F>94)%5w1I3~nkkdV*+EFmw6%<9~o$Q^z-_8+7 zcTz2I8^%@*Bec2?J7jtSlE}wn>!={!lKdc|WU$f}4EO+jB0sSqt_sZ+5aQ1cR zMXbF_yA_@5k)G53BzdVJtXkgpqvotQCRZP+)n~^tUAOAQN7nAq#zaDIjcrnSxVKAK;u-ZwMXWQ|g~T^3w$mEtp%0Y45A?%m z9&lP5vw{(uu!gIKPe*R`Wz9UW#NSJsE9- z&TD&dD&JUB#BJs;IG2K@uXM$&*nrZ9yN&Y(kv3XFH2i|q*NH$?EdcHvfa=6+#9s#C z#=~XmqStnc8~l|UVUs%Q`ch1eh^0t^IK0CXu4GQW0h-tOxbyT8w*%YxeM*Y3ZUs0| zYMKnETmcLVfIl<;=ETUJah!0;5C5nt$-#p-^R9t zt09>`ydy|mSM8z@Y-n>^SeSgF6x_)XQo;=1b=`b%&?#)))FAVLZW^@2a_~;_=#AIOY3M8MU z3nCD@@#5=0YMm02!-VljL*Sw zM;n?F2^`VJkSyZ`U*RrO4}f>}i*UE98evX{X~}>VxHcQvV`?Ja zf!p=1GA2u1TLCjDcpTT&M!;duM1jlVHV@{nZ;r%Y$_2K`EWoF$FUac?AUYJV8;g^9 zCJzgb(8V=?Yu6YRgaT7$RrYSw`DIf#{q0`=tf;e}hb7pf5qkPC?H(^+uxLR64D=R0 zc=P>ann8ul*QjyDBE(A17Qj;mq)=xHi{tTO$zMXw(5btCP=>{X9M%b6uL;lr3m-mK zsw5zOcA4wJ&?tD!sa=(CsvGBPRCfAGUhy|ayQEl9Kjmz+n;BN= zCvY{HAXFsG8Ov#lxGmYTur>5`RjHppQ7hg5>8zk|ZCl~0NeQX2PgV9<(ps`K9842W z@3*UkSi&K{Z#)oWrb3Xa&Oq|$x0ZA(kCFG;=Z1exVGK<{T&H8^qT2Fe@N0yiM{Pq# zP|DZA$`n~!s>x)rf|QlP=BsWu4}?AL#AK!sa94`wAEn$KIu^w<6U0-*)UnO~J_QoD zQ`TW7!bXXEX#(sleAhieK)c?D9$8S*fbnZebqzpKptxv;|HYQy3rFG=2YyVQIyuh`X8Ca<$ zB=k_uLlN`d94hlol>@v=&BSqwsr%&dKxx2kZ7) zm%PcO*AXJm6Kxt_l$)Mp8%|RT8G{rfl_1=GE9N7D)`UuUloVg+3I37S|A={Pcs~U> zeU%Ni66QOa@3t2)SYk0&Au3UYB-kaPlgdfG+QR)e&z<)tJO%S1BIbX`n2WG1)tB$v zAj>NSf8K9N+?qGJLjkzmvt|Sc`q!BayMLELB!Ahi6b&iptsw4c;IM4^Xxdef(-qr* zp=y{5hEy#LqpE)@C*^?-Br_*uwQ(GI@n-t@OVznsK-5t){RK?|RLxHGRP(<$&ge5M zc-iOuR3wZdV~oU{@T%>AyM2Pk2X(7J(Fo@#-}Gr_=q9c%%$S=-B9sA8BM=~1iBCcYxna7}T z0S&O5nW0>pHyBL@mHF4WQYaXC%}Mu9GE4yJ&vVt;Qy2v`BSB2hm!ya?DCx*U$;YDi z$3v0sq)tiM@7LyAQEHOwh-p>&`-0DWQkll#6Tyji_O;Q{HE!n3c!BbcDtJeZV4w4c zJSFczFyP~8&BROwP!^?!+;0=Und8eVH{sh5iB=##`SD>2o|;1|E?OP7v)Vx|0P0jW zzsL)`EZO0WmyN&|nzCrJj&#HanXs?T#=6wtguY!rS~eJE^%NURhJC6TmH+$j{R|yW zhKF_r&!NDH2>JW2QD5?w{ElW%U}Q~1>Fnf%??ct(n}<3rB|-)&zs~lPG$N-v9C7N# zT@d!|ho}#!>jdtjEj~<( z%K3T7%Q-S`56DhKcFfF`;bU#bwD0pZuFLC4?Or4hWK-vRhm-NG2xMQ zA>vs8-J+{M=qq^-NpAvut^mvd?)v&u zul9(iINO9+F`5eHCI+kQ{-FnKZ8xsh&2RB}8H2 z9zoeqhuMV7-;n}mdBAN(1MHQJn}xC(s4 z09$a_LH{4mC!qxHcNB6=23BSwMd4>>#VZ`<*MyC0n9u~r2Ga27sz+ae%fDD|y${)n zy9!}6jA4X5Z`1VQlAgfQJh;=(j~zA zc5?|Apk!7HdX%syFaPn0%CRWGAkE+ZXKb9{@k}G6I#)ui+l} zAnrdtsP{1xTs`)i5?kGBAQ-JC=UMmAT3X%Lv0Lf zc>hms^^a=;8z@1$Gw@j@@%jEnpZxM{KSe)c1qQAn_3MF|MCCXdQ%W&`LyZAB#Hqn&S8Syj?Lr&eaO8$;tB`+T9`k+pDp0l z&j!!}O~njcJapzDlv$uDLoa?kD|nk98g}lvHE$kY0L9?*ABfLytsR5tXNdY`&|E_>o}M zH6B=RG*Es&?+Y3m1^dG;+v>+;`;Yq~A#*}YEi(RSuPsCyj+W!hiyOHSb647|9{@t72=IO`=asw?Dh=$*oypreGetwq277wo#2&D zkOdP>^6e#XW)?wD?{69?`soQRnh0Y*ragFmj~=&xXxHY!K>R~?rHwm#>agq74Wj$_ z73}?9M4~CN>7WOY^P*`@apcTx^+&rmMbQ4-ws6+W`4w6X) z5Td9-BYglxyoq|96I`Fpl2JAu`vhW%P}5+^@>qAb0nlzyrnhSlY845aWzDeFy7>oqgs*$we-5M7yo*4 zv>XV)b(^CBHM4*3ML6zaDZVi1g+pb^w7V!@yTPhtu@d2CueG$WJh%##E$f&qR>MZn zB{GKg<^7V&Ri_sb!4*nG5p4s&MuW3PTj!5zvZB^w(t~U9`eL@jdT2ali$wwNU|m!c zvufJ%(f1~cKf98kXMdN4y<`3xzpb`9;*~Mz|8wx4c;Qmq;On#H-jx08HohFDO(f~) za_GUmQNrJgJ5v1m`cfF+0eiI1>Xnfo2tufoZf3r;O|tS@c94}0&f#d)TmEDoE`W;+0jf-)#f3O3&&{xraNPtH>T6NY9OPzeAT zKC`gJ&Glk}tWG?6E8>hs>GX1*b^l$EHYq?oaPvC@bomQ0n?6@f6bwHb?_q|$$O`0=!l=aun4ia|p-<5*@|2*r z9{?sQJ|}bHC$;l$S0HeVMNdiB*2n_e5d$E}>V9>jaT_%L5vvQK|4NieNq?XZemL3t zMLTdW+?#Ehgh}mC)Q=0-EHRJvQ!kdAZWKdQX7=JhOO>tL65}!m`#5>bKxn61+Oll_ zlj>kycT-4P!kRRK5QQovraAL>($MIfcJ54Bh{M^XBBZsBs5@cfB#(ju)>yUkJg`^I zjQ8|(n=rRPQ&G)ES((o*rCCZ#K>s~}&@AU(B3c?ESkSbH661hvNu0g1F$oU1dc9lgNkfU4^-oS)(T#mu$bcejaZ795a`r z@ajblv|r=e=iSpaNYv)D(ri|T7gtxN>r%Qp@&_5d4p|&xjys9_oumFVEM<3a{R1M2 zm;deTW%oD&)?sXx>g)4q<>5<*_OikdNmfiIkZHGms({-`@j29)_h5Sl=kP4l=sNi1 z5~TYcAxUk39<)71Y!7D`SeFV!p2tM{N;#aolaNsdH6yiJuCFkGWy|tKb=U})u!u}) zecZVUW(lWmmW=49%MWq3#*K+-Q?jq_r*HugAk$xzdlDL=3NaBg(%7?H&*Q9}bCc91 zk48jyuv#s4Tqe+9vjnf_Hkhn$X=%qM5Lr^Qs6GnPRmCau8w2 znS-naJ$5}VkD;=c@FMD$t85Z?n15Q>BbW=QSqya?h&^RFb@6sDa)*qN8I1I6e3}3x zFMc=Q?8l?4%syFKB&zztjH2pH)ekl60)A+Y&r^>J@Vl=pyqXVNQlm8kP`166d!e!O zqhg!=FpBAB`I_~Ii`QU^klK5mju^gg(4-Dtx;pPm7Jo82Y(#9lxW7bm^sG4lGw}pjUXc zg0(x~*^-K9HH)KKQw-`BR9$Bdmtv(F;TR4w_q&3TR~xRQ=u_V(8^cNAs?}Oa(N}aR z8cyp({Bx)C0@o6@&6u&I;~yu-KIJkzjjdJi_1303#YwJF!<_W6!>Y8)pY;sk`h*fG zJB0!r!S+1+c!XhcM)=vzyqSFOoA{uiovGV=3O)N)$+AsNX$g31*gCS_NJ; zDD^LYa!c^wdY`t3h2O#&$f3;6py1bf5d4Z)UFCx{OcY9;i(q3{oi9eC6E?@da%R;vJQvA3z z)ort#%<0YY^i#G;+N)2Wh~y2eb{{%uk~E7sQ!s9Kut3(UyEjv$Y_KwJ`D&dXS4%!u z(9(&H`6T&yGiBW)Wem?xjQuO%W>emWf;;O(qRj4}LDU0=qzj%3STg+efRwb^!Emeg z0H1OlDq+Vr4BhwXne|-0+&})E5FnqC7P&hV<< z$DG>|_Hw>^s(U>x_T>+0ig`MDjHfC+OR;Z>3BjtT8%?9_i&E-RyDn|D-pv+0dc0-6 zj7>dKX-B)~ao-Wa`L zb*2Ic!{}d4?X$w2Kl(7SRY}OZ8e#cx%WCXg(Y$5Cm~8QRq4A}g9a!y@Q^p0ljRX-v4Azs!&Aiw3 z#b*nf=?0+=(IV_t6aH$Gl^^?kn$XldYvdEc5QbiyqIIT*qinD|tF3)*tGs}d^RmpY z(+vDNr5~YI8_YiTXY`BmAKIOyZDhr2zpwyMtG&AI(jPT7VvZ!)O*XJIBi`z?D4YlC zOoug-VxXrglJu49u#pt-?z{%9ZR+Y3j5l4mbyNC`5aZyaMvE9U*;5-6Bz>v|7_%M-My^@82vAN8lxe}VGyl!JuvnjZ-tgg|51iBUrH|IAFY2LxP zQ$as$D8vW8(5N^|hvW5<@P_Ubt)0^$;)C+Si)W8?2Ve;#Si%JMm`LhhXS3-uc4BxN zJB#OEMy0hie`H}$qtAxE!PjZrQM*Tk#-==>UA337tw47+(lj=sm2^9%EcT{0X(`WBKj8IFHye1Af-WR(Z zlpL3;N3!9u#r$)JqBcUg9gZ_QAv%n(N5fO7p^qkGaU={kR&)R_n5<`< zCVi+D$rt4WS|&^v*J(rJb8J3$=Crw-$~EqgaPx;nQA11JPs~@`Jrq4_tyJ2HlMH{P z-TzFH|HuaDPALB%=|FR+z#cZmY~nnS3RXn*H)`STTehWtN>_Ca#GO?{Ww4z1+hOI* zRKh5;v&X7nIBs_+I_I82B6K4lHc4}5c*2q!zy71UxNskzo+Q3L+=0!qqoQ51sBizn zQ|C@Rb;ohS@Hbk^FEl&o3%2@@w`x`JrAv#G3a921dS|wB6yx$-q{fDrAI}%4wqS7s zEPC9HIfP{-UzuEGV^uVH*9jaT8py^S6~o=YJ8^pArR(l50l!A}?6l=+>nRzbJ{>2Dz%$0moj_ z<-I-o|5#@>v!wb(DtV3}D9!nE%gkf<~vI#lCn| z?3vc8(e(f=_Z%LB66`$&8!MC1mgN(}b)8fxBa4kT1;ES(Y%~HdN$}LS5l!D9aKvQR zI&p8&5Gw;DrEnHpDRaY&`}zlh&rN7}zp$$9aw}DxJ?*ADp>Z!l$UG>uDx~|aE;xr; zENeZkKWwZM=14+;Wg$+y+5Y|YGxO&(`o6&AR!loEX|~rFGGUBtV$O}vUs^pwO8Bzo zQL{l-_ve!SQOll!vS!e>J&Aw#*e-LiGb0lk0^IBn zkjB~rw=c|Ap2c!v0kN{`-8cnI{7sl?v#(aeMI|XeUMnV0DR!4& z`j8^aoLng3{mhGHfEOJTUQ zBRTfris)2i2d<76G|L#GT8JagZu5O7FWsPD`2Y z$rUjQJuHmlwtbUNcI){qkny{c$FgStEy*&YvbvsMJ^qx&Z=dYNNA;Y$Y>>v0W8$^# zNWDL%%J3^qSl&0KC1(gKBGWyuK_blKlAz}+a`owf+iE^S7`f>KcvV3-3LArqV%X;^ zUyf_%1oRHNvWK;T8?HpgjJn5pDxjg2I z+^)uaFQ;5y5S<#-_P}U12I0GJ05W9|08QDtvb!=CkzXn5;Weio{pOr*l|Y**ayCNA zv$n8VCm4oS5T+Sg@B;YTxnMh5kArxlvV4+w{RN3s`ovlqZ8Q|uNUTkny{MpJM13Ig zSBpV}BAfL+3~Y=A9mDT0`$tq!v7NZ;*S_Li2WSd73}>J`77h(}`u%!xJtxPJ8a9kh zJya?Vuci1%nHS4 zQ@Jh|Xy|jX!o0`p^Z53iR)I$`rS*x66Sd*M1I(*W2cy4=cQn;}IkM*e&fqSmaNwPQR{ z(-hS+I+m)DL_35=3;#dX-a8)aw+$bUs3cKHr6Q5+vQwbpNzokb z9L9PVSRTz!EgEpuHZEC|r{B@N9r&KiKaXl);bly((Xg^pupLDEvVe*}Bz0y=EnlGB zzVtF>pK>5O1bL|rHxXA~{y*Tzlyy9Ckf7kT_M*@2Y;*@4WH6W0SfgC1iF;oNDZc3i z;##4Nb3r5;($P|sKO6`?A*A>3X~;+aSB6S?0Q>Q2hbDQ8YNbJ#^AEAVo!Gt@*B|Mu zQ;&6PTAd+yGIf2QenLr{A#m)db;N0L2LlE|(F1^Ij}h%_PpF(ue5M++zjrU88R!V8 zzqitux1)?w+9k14cE_%N^yob^>)9Pm&kiNxTkif5PSX*iT@-gA0rFhZ#WG%@#>5JP zDbQ%fPDxwV_rB|SO#9$ho@0SZ&-K#tRLV`Dv>ELhey;MPZc%36e8L$>Ur_(FxfH>y z({WmLzTsnb0`G$Mth8BWGPP3r*!JBqg9{SClWHZK(vqH=1uo5t-US(hmTPsSjV&jk zrGi82Obww`ZDsxJQKw*qZHit4IR$N|Bia4TwYhJXoytDuCbyKPOC%(k_6<7qPOdx) za1SgdB50Zr>E87IFwY%;C-00usg>3L|953yJl^Nrol5ch+5edOmyx}1-UAPXQaJoin`bfrU# zGDDft4CsmuJ?SQ)*U7XfP{mvk&e0N#k!pjcM6a{r1qnNk*~20L zL_E%qytRwoPh~}w_%SC9R%ONbg$+p#7DEs;GUK>A*KWH)(aXEg#aYiRADusC#z3sI z{kFagWoS*~<*VVKo87!{qBcF=FF(fVo9?7a(&?qD^nak~Jn_IOoBmVkQ6t0|w>!=A z-tQQAX!e!PeC2P7T8qq!O#M@1(%Bg|Y>JQGRllUBAvf7@ zr?_LtwBtjRnIL0Fyq$^GiP=i;t4Z1lieho^$V>Ryc;MMPJDKDPhMLfl;)e<9U=Ba!<})bY2@@vH( zK@o4QO*H;UtOO0t&Ay6OMuXQ??MW&7v>KI+|pHtO>G7_9JPH zGH{@l%elS@$q+tTN07M~?hn90K5anZhu% zL)NK2AEzU`3KP3e_xzSFr>*^s&vAJe=mj<1rzi4wW6#iwz9yy4_uAb7x&+SY2%&vIXDnK@N@x z0xaz&=&{+HvSb8^J120Ijw}j2?DnIk7pWm1S^emG)2@pOfLfcM88LAN8gg;V-K+eF zYh~}yHEc?hK^Lxci+PK1Qr|>5N!Bk;`vvZHEhke3bUru9+eJ>o9Jf(zzP}`_Q|~%2 z!w%f9E+FSw)Qfi$ag9U^|M0^!DC_|b!m9^2 zFK7T|Q&nE$v!dupFS~-xC1B-L+Q7u%Fhq!m3V@R9v{E2djb3$X@VfKzWH2Muec#Oz z89nS8t#0oCBHXVC5J#biw%=wY_!l%H_^*ZnL;zm!4k@}Bvr8G7({MG{<<0l_gCvXW z4~j3eYzh(YYfgh_GfdMj7`P2W_GZIiU&Kr;k9ncBnO>tcg3oxQcbDPtO|%G6cVVc8 zcf*Z!rGdXM%7?*hY@ou#Lq6dLyDVoxYu1vUHI#FG^%lFSD36h}Z@w+9ybxTfHzi_~ zLG3pz5BHVBTw)uOI)o41p+SM4ptc+;MEM2n)HkRVL*wlraGSt>U1wZIRR-E<{FUI` z1y|3QoLS*z{}JM^?6Q6#Q_SMo$_0;kMCtbTb_;irNO;Cd?oyY374g>X%(dg`CfxM& zL-lS^Z+8((d(z2X+$SG)`7Tpw!(6^IX@KROx9x$=fn4`0%KEc75=P4er z#ON=e&o;~{coO&h+~u3Q9QRWpVj1`ycu!)L#n>M1@tQAOVtxGAaWgUKI9C~}-JEfl z2v|DWXcxL0-F$_66a|E)VYub@H>DM=6ue<5fh^bNu37!*qEr_GC#x9TxN$K?YH0O) zSRVMaHkXRcW`o3<5qJd~@=;;og}*PukB)TfzWYJASGtIpaHh(Q{sR7N!#f8f+Z-+3 z{`c3Lz&4lfP>#XvS&J7CCp5}X9{mgZB|?kfoyX5u{-sN@4Zi|OG6ZCWFygLTjhMll ztTi_bz&dv03Vi*}km8zn{pK_9!Vsq5c`@VWTN|km@F_ zU9DkE1Ps{+h;Z{Ug%(d|z=6NNo&+wRe_(dqhMTjt3|vWItW8D2rEnA82#;c2u3W9~ z_wyBiUPB_Z?E^Xt%Sapuf1#=ZBq`I8$y68ig5Au1u+H`ePU{ z17KZJIs>-(FMZi^dasV7$VD`AZ#+wI)1?h~^l}4oiMxMX0M7?qzy+Tusrj}H@#YM! z{&Z0pX6T|oG!74nH9%{a_o~gGcuCaHn^>&_RCL4#8y6&6S{hpcP4^L5QFK@e_B z(T2if(v?UW;9h|pPO`paqW2%yU(NLa0c38N0d+M!9R;mXoc0qpBxPh+tECegS%4M( z4Um$~>*j;RQ8@IM?LADQb733i78sfoJ-`3-ie!=aKy2y*s5uG8DY)?!sYk0fhUL{4 z`Ykke{KEf=aA+9k324 z^FWt?!-X@ZxNE>Jd_lZ>0hVaOd5Zk6Yucs>P|k0Uo8OXWVUNxR)1;xB9+?i0Ag8@8!oE4z@Bzgnql}{JlY- zel`I18>{X@A}+tWI^{|EYY=mX13h2=`H(15X`NS-*?4&S-%MHxe}cjtRE6&QJk2|giXA+J6J)` z>6&j(m43^1((|<0bomEJf(6G5$*n+$P_YvAl|KgthK%s&<hjlC3+tyvc>Ire7(fJ)i`=6Z7V!l2cL`7#S~t zRNryvW8~ew0x*9AP=pgA&ZIl33_WzAEJ;QOWoeA-u&qEg>a;y?B)v5Z0C9Q2+toB{7lfhUCCG?v0IcjG zAd(yls;Yi}sa%jT_x45b3>MHF7@E|}{3lE1W{uZA^ZhVpj!rhVs_P_$`fmERS zpkzgP?FT&?cg(?k{7@h#N;0bEAOQM<8=p*(?#t_aoSXftcpnLV?oQHJ8mW)<*>i}q z@L^-ze$yh}rS>%7W>J)v*Z*Xg?dRu0I|{OHJa~M$JHuk*3BAFhmB6+Ohr-q!1qBXJ z6GBFC#R`m5{u#EvU9K$Gj;>o!y{BhjP`NC6dwjBTd>sqm0BV=Hc`9u%;BK!yapvsW z#V%;K^zip*;W4gzh3KC7VqlxUrS(rxR;aGBdro#{^YJl?1o%+O_x0t3>|$q$QG=0g z;%vaLw~tG*p#^#GT-O8AAJCS_QJ)11!V2J2{+-gcMZ9M3lafyVu>a694}`tF(5ucg zodYP~5}mv43}^R3pHvedQp<(3VsHM(jTt~ZK=KmABlQ3ls@`qXBCqb@>1hCzJ`Cx! z4A=wDjufx@nlY|Gdu{+)<>;(JS^_``STK7xZbvJGeJqF<<~ZNhrYvs}eOgu?9u-uN zkd%~U!YJ#y%982=Tr3AFFQLUdSms@e1ZUe$%+1YR-PyFhVd_VlK8V@4S3_xo(Cm_! zRkk=&{(@ED34LI`=3Ow2_xq0rxFx$qIZvaT(7sp~sgKczo?rfK0s>4cCtz2ykAn&h ze}2e8l(#4A$6v^2rhkL1cWk81F#wW9a5^*XW|Q2TXget+InjWnDJlAVL#OLz|_=I7DX zBZ0YxC|^PlCUr~{D281Fws6(p0uR@GSD|koL1iuFMPb2%o#k5DLw+5~b&)31JED#$ zsls9J>2IDb3Fq9QsnhtT+Sa&}cAQmS)wR6S08D){kOd*E0tE%d+YC-l&a#=%`AE$) zRN`hY{CT z4aiOo0-|9+^_>I9B|U%>I6PHpo%&t~py#i#iFwSxtiz!Y*|7l3TL0K%MIYEg)9)K# zFt4iPgK~+r@F3FDG#mPS5P{qQQd*glp!IBC_%v8{7wgWa;APLCXrZ`1Uv+&W22eHu zZ_eWQKQ0tb-6|OuLbq5OG86o4TVJ8T*w#z8qJfufYP4MoY1H^~R8X;L(gQywv*ih{ zDv=D5dVdelKh=po4=I$9k~PKL@4Frz)=wF#^~jG)v6fZKA_*$4DZO&fj@{py=60 z?ppgx!yc-9?3@l$x2%80X{21FDLa*@ej#h(Wseg9Srp>de3tdd`T)2esr=C2v6|Cx z4A_jFyYMl9s4fBa2wEt-ar5zj@+Y{*mlkhZ3`m_u#8OYqPQ<+n4-0#1ed)zvmA+d< zNm*UjLRPuT(}^BZ>!+Lz)!R1IS%3IOt&3(j)Z$N`N7zdL{0ydbcXS*62ab@~{z`v7 zjxfyLL!4oS4;~k!A16_Lose(`82c7FU>SJkEKZd6K5kk&ZF!2z0|aU_KDihL*4Os{ z>P#rbgBtA~3)DB3QE_BR0{aMG)pg_(F~=KdurGdeBBXTlhP&QySfFn5zy~hlq)EMf zeST&q<5XJWdiIl(heLT{Vpwk_yG;T}N2Ci^-N-Yn^3{-~dzw&~4$KiLBN@x>apAy1 z7OKTDNcAF{kYaP|(xwwm&6XgdFkbVvgEk>Rfb9Xxv&FQ1P|wC|38jC|Q|vPp=V?#Z zRcmM*5#Nq%T+a9x>hN?8V4tgw7oUE2nT_}W>#Ml9xDZm&A?D@m(4?8yzcfwtVxRBD z*rCQT(v-4;`z*k><$WSU(7qXZxMhoU?Vp@aHR~!@$h}|>lO*Nf;E=Thyfp0lcCMZj zNg@ECIq;A3+;2qlblN|S$J2RV6EGrA8iXt_$!sPV+|2OWcTd6D8=6l&q&hjjpbv#} zG0i|VwdtrVgpp|buXKP^ss8k=rddCVU8$Q)vw!%0dvOh|rL^kwYA9 z$yvQ;lZK<~2nM^&{}`zK+DzIcnk(D^Z%P`>w3s&la5*V=;&MNQA+9=bGD!_rR#bRX zvZ!7L(t{h|FP@3|`f6)rgzjkZ66U49j8O0bZ66_$we%Id$IFsbv?#+RvhidX5jYVL zyYi0*;0y@jtrzyxxNJPb zTHGiFK6=kHa1*15kk=e!*G+UBdw|F4FfOx;Tfcv?Agqdu1zL#G}w2* z(GQI2=}`)`n>i!b&)~)in2HYI+HYKAVL-AR<_DlrY>VB7lte$+`JPbR!>Hq>6SHTE zI8XX_UX0h-@-!kLG4OSj={;3AJb?%+O7Vp^-(dagTD#ofTFkg>!3PfWkZj~mEIE>( zDNO?$ssXAt=<$lS0(^ps5TOn3J!l`pvldm|;QZIOzUGxTS3qer*Q)MoqPZQGA|)yP(lGG&`lXhM4NN-{GmgK&hW7Bz$p;8ikt%H zMO2~t;QF`Mw?x|s>~7OhUlxaFuES`coOd6>o&M;buFDvRLVW8Rgyw#bLyyp#!RxT| z!y)wPlY_36qs?O_HXt@?Foon}U|)~GJ=%mGOs(>7uT42DB@80Z$fSdOfiYO)c*qM( z0B1C>@kq_)q>20_qM)Qip?kTp3DSDpK4o{l=AGEiEbv~!p)07bU!4t3PTQM@a7qLJ z*CDJH&eEWu7jL}AZKH#AgH2@@u6dk56WmKcS8={1w7S(s*7;^pC z3F+5+aSKKHoe1mBCQvK8s_Ic*Ru(6EbxVz4^_sNV@MfXIvL?F<)387x@?$oCB{vCQ zOIvTpW;6IMtq^cVfjXV8xw$zRzSf^W_V2tmH5&}6jmRrROPd>Ybp%2b;eO0T#oypYFR@U}7tgDtr0Ygub4MRP( zWTZ92pSBfv|74Q%G8Aq%CImL?rox(3P~O~*+b|Xc&?2X6{Tuk^KMTCL4z?SUi7K`k zN|o4sZh5Yh`Uu>xIC?kV-FfG&&D1&85?{}8{1H*Ypigp9QBi~oM9iKbDWwb@f6Pyx zK7BcyLVgzd18@fy8ub_{+8}&W?+-6E9+)1U5r&Ovp9QR%C%;Cpq=bh7d8|zp`-ZU- zAp;AlePCCiH@xuM#xEw%sL5%g3jN zdbIAFpl(#g!E|*%x$xLvh894U;10hWdF)^_LScj8k=)|71 zGB%FUNK?(AGN{*F-oD{W1VACB{LrKb2bmjU0=q2ftxLKdO;zmv*_t5-jCu@l*e?tl z(^{B_1y{ZoV3U)0+7<Z%;sJ^W?!NxU_fS8$bG#=ysu^wwjUKXBVUnvu>P@!?`n!;+2Q@~d|Q_1*nAQ=51=3R>q8=P zi;ZU|b5p_~`Kg=!EK61@H0B`>IUs&=rxN50Hs;#i1i@YkH6laglJotiz@LyHItbpg%q; z>N1k^RW$(Fo(zG@G>5@DJh8WMHEU_N&#g6CGw>FXaIw1;)VUHko#z=n>h*LKKMOjL z>_L0tA<%-*Z*e7}K-WIKWDZoxz`nSCgyXu}&&c`2v(= z>tk>MhSl;aEtwW>pybvEYUA&?Pwqo&MBxfEu5G}=7pBrj_F(%)xvU!@dX8KR33(jmR@nJ-@3W&Z-AbsLw+hN!9g7a2Yh+FCfhJw{`p;kMLo?7HQ_{;A7 z&4xcWmkIjy6nx5sZ~laZvpYdkfbgX$vQ3`~eu{d79c}*z?h-f!o{Ni)N@@U@Lru#l zCy-7S|Hk|l07R*36Idk);f2Ll)R)7#iSR*J#73jxAFi`hHOL2NH(cnic4A(l zTk^8oO~8AZ7|H1LQ-wSm>QE+VgpmBYQmHbv3~db`u$o&2;#OK=3alEtAg3>XW|S`K z(bmk~eJ_mPpK;l>e9rnJ=^vFp%qv1r^rY3$&`1XTi)uEpG+?(8LjaEZCg92*Z!E&R zCU=`60ZNb8{?x`eKg0qB0kh?Y1UGWyxYgIjN;bIg(L6mVb_|S>N4W0#I$PbbQuyNH z;>n1yv84sdQjYOZP0;72ESfQY<-;v+RJP+3Hu~grQ$%~GrcN4z$&b9*b4%{5=hs}`uOphF@uWP zD|mV-QPKNK*^JS1+}by>>Oh?Zy7_DNoMd8>gtYldl*)v3cxU_eur0_d-2S-=yNTHUIr$$$ zf*IH~P(lHC)z?3Nnxa0-#Oks#8I&Z{b{Gs-1nPkSo5!kuX1_#G@cF^=0>yy?C9pmi zSXiV%4^^u(1fp<*bgGf6cMt5&2Q?jJeZS>EKx^z2LVr^2`eW?7POrM*we<~g9JkWk z!4u|VjZ19`2gkt9!T(^pf1k1FuL!((ubPCK_jfu~yi(*#qU34F zFDfc3ev0?#`D6Rw5oiNar(E1qTy5*`X@v-orhrC{-G@V_4i4GCT%rU58XWTs$BF{+ zU++!IMaf~{@V@Fg!{GA?e_`*vNq%MWji%g@Uk`gM6f*GTY zhN`Y-wzS?KqYQjb@DyyKuasBAPTY|X|6G*PhlKD%i%FtyRSScUHr=;x-(JV~ImBG1 zt%1&6Mb(T};CoT0eBpL;_35L|;*p#g9Fqj^Ejs&3RdPJqvlGxX<=1%^rBZ6W%^6#O zape$f#R1;;e?`xjCM3do&idQszn<<0Guhxa5E78gSpNgYjTrp(OQ1Y+o%~jOe;J5v8vJb8l7A&Z(roNKdwQMn7mY&`-4&6WpL z-)qLGC9?bIIz$tt)JG|t2pv(tovG{NgN!}$iz@xR*B@-n)ORDuL%@~upVtfxJTC~+ZJ%LDunqJYI|Gv#pj*>ncr4R3%&M;$tK&R?4Kuh@vrRv#q*=I)Ev=6(MC)0yK* zS3mC%aF~jM6#1!>Up~|RF=$&-qkrwo|MlOUFi6&2VO0!&SS{R%@n8R;J%+M3pIN#7 z1?KwWf!3yS?RQuN**wC}Wb+5_P{4^-o}MQ_5y%Ie`*V>>Q=2l)|Kav-`(TXEaK7HN zg{Kwv9%I+ELoZlGAIMC81r!T|prB5~l|W_yu)H>T==VP!3huF%aN6(;A)u#y6&%a~ za#S9lpO8vJM2D;gqN2HP`qzg0^9{FQzM>dBfPn`Po&xx=dOV;TLnKBe_9WyKiZ>|8 zEK;%H85scNw2GeS=0=y@3$?_{pXh+ zGFU+V+ph=Y3XV3|AA!O~1x04}GZvX*(9`)*w)cxjB1|Hdo|Ax}jZUAi$!0kI5ZBGj z@Ybo#-0**x0^C~Kr2)J1tMU;yR~A?a&BC&qK^DMUq&ML$e_Y_2EG-9z9Atgy1q1{d z9)Gc0n;5_?LoQ`kvC%Gm*!BKPF><4TE{m0iaIJZ|JJ^!D;%KyvKYYmYKHOb;;toaqf5@I`fYhj0}J zu+an%Bz^J1h?pYCfs4?3ZQDW>j(MNIAT;YHz3_o6xNM-m^VNev`Ub4=yyMUu)Dc&L zGJoTC=f_0~4z3zxH)&YZmI5Pt&OXLXOV;MJgvilc3E)>y$GnjKR|0%>xVy#C$qG&a zNU}+R(LQts8onG`-G`PG*OMKEP#j2a@{^N5P{4&mxY;W3Cjh4BkJxkLN2U(RE`zF&3#jfdWp17@*3@eLQ5Ia}0}bl1c8&x1>41}_jx?Zk$|^BV zyJ8jdA9ULFkfa2KC+8H=01qF?M)a{sb#N=4!85l|E_;!(zC$r~ z`>HD2AULyvdR=kpt3zwVmS;d#jx8e6r=aNFw%NNQ!N8pQ_u}GL64`DFDyy#`Z8Ue5 z|61WLFi&@%?oHH4kG?Xz)03jT&htFa5bbOdi)&QVSRs21F1q){d>@h`~#X$XQm|= zPD9<>@XC?g2_mSfQBC|zspPz^P5$yg*N!LeBcE)#9tjluKwxH1TkcTBx{CTdG{rS> zpOE+l2ut;$9CX(A$JAkvh)y`d7r5$+VG*#m?X>jNK1>y1Wr6cic6~OC%|pi1p-kb^T7jvb}?tIehSQS)o}?oz4WhUez;yN1s1{p=-?+1 zgPNFaX(jmEQfMppI|EX7*+h8RnB?AbE`{UvyT+8!Ju;o1DsA5ng*O^tYPuRNr z^16cu08xSj37r<^-gylt;A*h?)pbFy(H!^?t9MqMTJ!ky5%cUl?Py9&%>J(BgeA~2 zTt5v$m6GYAB zlt^ek9^F4&Vv-%|cc6X0xSCw;h-7?ryr-VSAVV`0@8`}WISn5KpE27!poSXX^olRh zyn^>OKK@Lh^<@S=3>(>Rji^+W5+3Xf;NnX2oE98)z{U(VhX}1&ytNu4Z8||_zR8N? zdgf{~dy37%#}u3<@eu^LNNDRnx2hvkr;W^V2S@a5!c!rJ_OnR##zyAbKH||alFc|( zJt76l(j4IJ-~710z2V~R1QQJ;Uf%G2c{;Ep|Pjo9=~ zE=Jgz7`0`7SRA+{>;dSA)2HS^$>|5p%I_x-=r`pAdw9?_YFocRk^WRI^Rkwkad6r{ zEi_Bkt!5d%%lW`jkPPUW+VloQr%YZ^8!w+GmmL^mUhCoG(;o}EZrPmr*lj6~V$$kM z!PX#n;upo?+qDrd_8dFkbhiRIcq}ZXhVjJ+>1jC7DJ&>CTrFgr&&{7{H;xPQZ+(?_ zuh2n7JE&Gf(9Dm$I}q3}?tb7K?E2wz;c(p5go+d6N^D?$@)}7Y0|TX>(~V!i5;zsqM#dn8DIJA@+FBYv7o#cD(T3EIQ?-EucB`hR@cGTO9!+)vg_v=qc7= zqA*!l=n|lA+`eizE8w*0Ys-3PxJ&t>>%vD$1>&(IK+9uq$&J5DSrofFX^?7gzK{lo zpuh^?7X*DlBqFz5rQLT#b~p^Pkw32KdUJq;V9=$-7pVibHPaFpRMecVw6VDzW zx;OK4@EmJZXpYcMXCo!mdoB40m2$V-yOFO0Ny)Z#xxJyE-gNKO@n7#rdNfJ)x<1dj zK`;5nQ^e4gaqXjXpK(pt(>+Je)}8-Bp*|J#31wA!OT4TA^D~()WD`azKFB;a-&~bl zjFnmK25*AzmV-VdPZ@wHJ>r)UVM!+3>- zL0{_RF3PxDucTcpNe|rr*z@vS7_egW=S1?Q+PW*YGfx3q+-pefcs;F!a_ymWcTB6G zcZhLF>K3Wc?~vwXI#29Ioqq*3-oQe68iK&O5dyd(6@Y)e_>ihWZu$z)leKRfGh9KWBdo(76J` zo*lVGU40`ku~%EGjuOXVU4o>jO*EYLGGZg9-=vGjr>qc%-!-QKgWqcSYI&nQqJrE_ z*_EyBiJP+MXTy22dLZ_Ye)K`eM>Cj*hn^4QtverBh6jF~iKzJ4Fn|+r5cv(}{u5u% z9)bW??2^uc;gfZQx*_4z;MpZnt^B%`r7@R(aK1Bu5NwD`BZC}D8d+HC!nCd$Hx2)6e~$#)=STJ?J&^@K z%%0(ozS=Nw3F-zf?%nfwuJd;TsF2iiuhfKG*k0}01~t`6BDY5XwNianJ8#=s4@>{; zxJgwU;?6Wt&hb5y|Pp zyB0$Wc25}TfkR>1(%|*OhFEch?RvZrL4r->$eYIg8qkeNod1#kc!B$%p_jZ`fOV5g zs7-M%tayt#f*OPh!bM0B8v|rp<;QWlCDUUiF+EZZfXYLvx7D>O#id#p9brRb9ArF% zUuU%vO=|DO^Mj}w#Msihu6 zy}BHxk%1pX^|SX!B}nw;#~Xrx4pc72c>9MIvNhkPJ$f2ME#)dJoQkZpsePzR<6uLu zYIS5O7bmBbOin|hT!7Yhwv`W>T4|b$P~DdR%mX8h%i;&3Ji-IRU!0|+N(8=NAW1L^ zwkcc2O9xE!H|Sr!{mD?%hFCc;Hd$4#wg+GBO)YQQD1t9GvXLCug}NxikmBt?pVZXU z{q4c$?k1RmdpPSy=~yeUH>0uuA703{#Z}k;a8O2>Z61$uVr-EDnHxHMLm6Z@lnF!@ zcU`2zs=j{WhOf4*u~L1$S&PP^wlWQzm~q+r&JA^y*m+<1F}9zb`JJ;tLO;V}J%+_} z(Sbo~puv($xnYA-r*(E-1uT6Es1sgrw6Xf3EyCiky)0>4NU0OG$RlHmxYW_6HE8PiMf9 zTh*8_1IyJQFW9y|5nwVp_uJnzjt`RDi5(!BggAAjqnFR@u5hZ%5r6ywZxV7zjy*O0maalEAZj-4N0Ct_O-oH=+yIxwvWYQ|IXyh4tnq z37k(h&N9!Fy%q`yBb7NS?A+E^2og&tq0}?EbR@H;1w{%_GwpXwsC$wQ-AaS^ehouK z7&>p9E4Ita@T^|f9JSnj(Rg18T<%_A$3T8BeV-c_>>W$q?}$-fn~Cf<#WV?D8!a@y zaJi)33X#entRE4Bg=wi*C8hSUraoga1HW3|tK)gYH8oc@3kE(jX*K3r%F?)NkE-xL z-!7=Af8}M~bI2}X`N&Vl_6B;3M?AyoIv->nIQLSna2&+X-&G}KEF!nv6ml66%(hDq zr#oWOOW8k`wYBMiNN*z3N`0}=@2>6kkm!&ed2D_!22Ys8$xHRVQ}a8mH!)@!y`FMH zuk*pkGN`(;jM%M+XkYpJP>oVR$pBGol-FSp&?f#!mm7BOpfa<&j{cx>2O+qD}d4AKWqPn1Oo zMf@?EHAwTeFXB-GHiD%La9J)QxLh6BE{vA_knAlT2I-7Xb)kHoE_0=Ner(5W4LDeo z!;1%N8xNb-hOTHOUdRCCif`};G(l*+9w2W~C1S+#f)9u?9%F4-{}qFHY(xPUNz&ku z;j(x!@;nwqU3z7$yT~V87Fs(|0VS_?$DV_<;L7xkDI85q_!@yHNJ4F_a4(Qlhe=L4 zY2=Kp_*otCO3i7uBsp0>?lTv%YSQz74y6aq_g+j2)$$*rqF!dO5Kpz3 zKf-?-({miF+M)7w0jG3BV$iZ`mBBQ3OVj>Jk-vE}LdK7KFre~IuhYrh{l`P~S>F3Z znmcvWKFJBP@AFk8P1MRrXoG+`v?pZMjp@1r!)G3RVIH!<)u?CC%(6v{_9fv|scrkU zmr%vlBiM5kkKhPL$U8cch(TVTf@dqP7|GpFW9BQsgAm-6SN9&$bQ1|LnLtvST6nIYKH_VjWoM3L?rT*6;sjSp@VWm^uWx%;!}xP$O_a1$@cHP#n^1GQu-y_!tHmZNa; z{Jkj{ju=tME2>1*p2W9gh9!4}G3e?ESJzmAsfcv*chPuEIvQo!$IS?f~^A@-nD8D&AeA-;=mp_*s^<%t;RILj;8xZ)l@ zY4t+d{te`)cM6IR2Lg{A?kvdD-PuqQ)8OuqJ7DYtnJ8~?(`rmvOuQg&@sF2EyXAx# z+ew@>cQ+zV$S2Jzs9v{Am8UnYKW4>ZFb<_ov#$9KY~Vb)m@(MOui=6LVZlpN*3r)1 zsZ!!JbTNP_CVD&PqIwkk0dC{EsJbWL*|P`e#`Oer7Jflb{WsvMqPCH)o;dc_s-Awe zKWz&FOJp;>N=t^&ShdtlNV4fSr3H>2UPPGy62kP5EA9|58}w|0#LlFduRVm$z5>=a ztIU|Tf0plslgD4N{cx?YyluN}qpwIk0{wvkttZf=y{#`@44wkV^8hP%_=B9m2!EM# z2%QNC0Pll$K^Yy6GA=E{`P+?iDZxuJ?9jhQK2S3vz7)(&dPK}y5GLow4ttlm;d<|5$QJpfd`-&R8}NI9$$5R4PT zB;1OUuv5JVe%P$9=xrElg8PDPKURLdvc2!T>@XakAr+w4ASUCWZ$!Eetmw}Guhwbn ztAL(k<`&DQYX!_2Jt*x?1quT{KI%zhQKng=wdmmj-Uq1aY%{mDBh1XTOZq@ueBr^D!{#-wJXN?&Bjj{w zAjeeLWgB~oBzsO+M@JWS#M=W=(cgJU#8pmWBW#C;jI&ELqryR#Wa7*IraMVw9WKk0 zG~^Q#92mJ8XiSg;>UpP^OR#st!By{@#*RoyQ)g88uGkm6B%Ud{oYce0MRLLTAZJ}S zjXtq96NmcIWv7#%Ud36g5)P+)OkJee`qQ^Swcq|fCDZ?D$CiquD*Es!YQX$#=xhV=ap%jHVd#eHzBfc(%8X+ zQ+TJGPL(hN^*)tO{aU5Y)gP5e>}Li58l(IOJ%D_qEP*z&Mc06(M$U|M^RM1x%~`Wa zM6n~wqfw4`!){mQznLPt}N_Ca=B6QRf1<~Uj2>p@_`^(zcgEhTTyT&Hh}aWLS4stt2ZzdXJvMk^%w(XFi7ay*;J3FXO#pd_M zlO3y2Ujwf~dJI5aDif*Dgkz497!AJfJ9k~Y(AB!VSVrHhw=ZyF+>hf&tl*Q9TlFWp ztA&Z^JZQULa|sXU4+s}tzrRO!GWpp1tB(`bPT)wg zd3<PB1WtPr2Vm4))y?VRgam z>_ABE>Fri$uprEV^2k?Eo7U_megGLv1%OHZICtr@TOuSlAm-p1pdtpePN-Je&`jvN z18Me|DL;_|K1El!7_<^>`zt+%g`8b(4Mk#W#H@=Aa$NT!$8_kbLk{3Q+?@u3V z4e^_MUyw>8x;_0)<1o)Vz7NY3RDpA|t~II7Y=e#AkF={}w5qumeA1l_yK zN4E?Rkt6uk6Oh#@fr#_Lv6>hamdPYvb7)9HLuKpyNB>bu+ATmAlrgO~AhCX{tQ3y? zXMRqP=4*{mizTTm%g(Je-vB5Bls8X`jgckk(B_9+?$FB9SRNmnp3t2+@U8sZWO(|4 zM7yHv_$Kp=gJt>t^>qO%Ifrksuv0Lx@4sz1?)#{-!rR!0$;zyA?}X=ZpRe^!Ez(j} zp($tOZf~ZByOC3*EyML-1Iibp9Rg@rIC^g!qpNw6#(hu~w82x(N!W*nlJ3(m8;a@4 z4z;^>Ep%Xad&`}qP|`Pp59C`e399`QU$Qf=#2VL)gv&dif07_uiQ|lLJweDFH{&AB z`&D>lGJ2rIM3~~A%hzgrv3@%mwbf-p;xCJt9sa^~@3mj()pQ!tQM%9U!`D9j-;~5R zM+*}JMnpAjIrXK8GGU&eg-p=zVV55qV}R(1oLFoTqSll?|FJ|<`JTb;>)CYv6qErP z@5(vK?&zi|AF`F@n46Z{SxVTi)_%u)=4H^oFdZ8n`{uZ7j)0Et*C17A z$(Y_kMo!jmB_{TCRr<+*=EK})n$uX~A3f@!9Cxra;AJVh)AzHmkl>KB2Gv}9RB8Bi zcZsVi-wHO@)Bpa-3y@=~0>Tv%*lPS0&1}h_<>LJs$>s~BOH=f2^xm z2o-%D>bURrhs0=%PwJbGE}ZfSV=f=s%aw#TBI2!e$$#I~6}8@QiqL)`m>_=kBW+f# zlx8JeJ>6+3@K9hvR$p~}19xVh#_e~!W(iIUJHYSh{c7{JHn`_@?)IPO%+N5Vyh;7~F*jVWQ zS|XTauzQwHv|ZOQCj=sIOtcYBd^%Sep4r4Q?-EojmHxoN| zV2nSAo1lict(_>@aJyW7?R9p6aIB9 zz}gNCIAKvlsrI&lRWtRgK;W|vLGWZBhz$6lJ+<}WvSZ)*xo-dMhC5JeeiHPve=s@G#R1Z8!DZ2Yxb6R$st{egpz zVq(a_O?}mwHXyP9IgMlo>4Rlmc9-H+W?+GAuIEpI0mO)^Q)5JHGan z`5f~dFu%;O{F8y1eGVM@@_Z8`XoJEL1wm)0bytpVBCA%q3=oeXsu+g(&{i)^@!>J( z-LL_&oe;m&TU&X+3NCA)AKQq7MFHet%H57>K#0$^9c-nDONDuh~%~ zVkUZ3wOZeoa>2-xxMe_;4%fE@jMq-ZcSqYa%=4G-Oj_&f0nq}- z&b>#-NWuwOV4&l*p;Voh!F`#}x=%*0D*k*5oXlUL;jZ-D^V#*%^nZFuSR`nG&zwV7Ow*utN6oemTV$w_@@;Isy9mu0#|ReNQQZHPS9+P%JGxKVzlv znJf0EJZ`W8I1rG@m)pn#h~r6;WPLfq3wN82YP9lNAIAKQwvHr$$T|itId1yvvG0NnyMX+1L~5C z2_OQo*46x9%_}WC4!AP1R$l+KUp>vhNBiDbidDCA(0~+MMgTlbP52cpYnH`6Qt?S@xAo#&{WEX+gQ~ zGAtw6eCGZ7CRHJr=EpYQJQ(^PR>z&z(bwZ{y93k%zGf0>TNQe)}xcxB%m_ zyY$(E|2a2y5dGX-Fw`%z4wo$h{WE$ayBBO?C+<`CN-btk)C)M8 zce8QMx9BR25|-nTsSi&e%y4~r!rKTf8)Lijd>}OV2x=VRkWk5E)^(az(a3ks9fM@# zxl(A4V8^f|5$WsdkM=EJPdg zq@Vp&v969yUuNNrar$uYdwX~CYjw%EvFU|nVr^_{-oBf_u6!(M4ve^q#`y^G!~ObN zN%|Pasp?BY3?{8toW27k(<@Z8QZ9gq+0WH?M@i~Ka{bzn<}gW5gfPmR@3ikfItNe- zC*iw;HR2Ng9AhISB)rL!2^}S!_tbQ3y@;xfXi#BuLAMgZ$zn~XXFY>!t$&-Rh~+69 zUJTOs06gpp*jd^A>cR2N1MUs3U#s-Ns)r4X?1!!9U6k}Z^PMth%FakecT37hus$Zp zc{JC}R>l?EHo$C}eVCeMxTuPN3$UhchcMV}{*OO$bKf+Zi*_+w8I*JJKwZyYe$wdI zQu(lHodn^wh___cuRx`bJPTI2Odv`Y|AtAD1l)UK1C$^guuvmn1B5gCU32gZ4}TD{ z@Va+;l@4LE=uS&^9Um0xJz$C}pT_`&*U7I92|DLSJHIrY=Q^_PI;;g)>UWN=?d$uy zfAmt7l+Wg$qKRuW79zVx&XeQ)P<794JNMm%N^frClc-IbGp@W9P*wD(jKhlIlD-y? zMIccPT_6ah`|X?I{oeWf?+9=UiJXPAM-J-O^1 zHNJjKqPB9J@k+qy?NQKKFg4sL2gO}B;JF&5rr6R@lOzskjr1h%s5fhKCyzOvZzq+^ zH%g`xR^%BVVaN6*k^HvhYdd1Aq7!sWA5&k6%dK#Jxm5NG-SI9FB%rxXXMkTzT0-LS zp|1$>3pHp4RW;%R`ja%%fC|2`yf~+*t^M=f+*)q=38j84v?E2^B@w6*CLBupj z7xeHK%lAEvM8BUjnIE+H00!=h?e&n)Xw${*9`I~HAiG>6GcRx*N}%}G3y0nWkdC~q zuU~XWa0mYcyM|5Tn)x^Bc|t%l(W%ttFba_UvUK|IAmeE6I`V?Alj@O6w?B^X zR)0_r4+#-Ttw3qroY-x+^Uc9B1*O$ zsKShhXX&m_I)QcVQpXc{9~t zSxea+6K#cB-A2EkBNb+gIM)P^2)y*<^!7bJb6i|U3U<=4?sF#Ql_&8WOb>&4R01^|lbb>{k)%MW&t9685fvj?NTA87=#AbE;xHcTUTjLytYXKFS+( zCl}JGWaZ@kvBm%LWWy8fVnjd-j-6|uLLtm0UG-C3x^u3-Gz0X=JGw>3Df)cLTDPx^ zG5d1gycOV%>np?_!q3)0TtJ7d452ubRNs1E1u1DDRA{+YZ#=e-#*O$cA#eMr&6nqg zj=wl@=8OXLcHdNe{vJG}T)-qG{n;#$BqpL}+_>3tAq&07(tyvp(S^mFr?|QEtnEfm z{s~`JakNnm7{COU zbS}^EeJd|#tM&yv(X$;5&H2i#hjhq`O+u|mG@9bNsxVxk9ETE}jh2ES$4PIOlZ9J{6 zI-})jAcA7UlDtg%#=jM=eu)UR#T?+!Wu4y^u%k6K)wEf0ipw6-PSTF%ca@6=SJ_tz z387&7z^%c)UPVnjRR|hZF<*%-T=*s;Fg$C>yjO&!yrut)-5W3SX<0S&gm~iz`rIjc z{VC^`1+1Tg+kS!>carSg$8XW)I*=Z!2_iq&gA(?J4;Y(*Q+={70>KG|t53B=>}XGQ zVuQUr@t$VsGOU|^6WxUj{Q~?ssFFG*LSOoqpH(~X>Ds!wfR1BhhyP!EU3FYl+1eHa zL_+BjkWw0yR9Yz|1QA6#q(Kl+QsRswA|RoJgrtB-cZbp?(jg^EcT0coKGrz*e)k`K zjx)pAXRo#1^~MtnLRi;g#4HLadbs7P4iDV;+1$US169&~CMjG2XlzDz;{4GFYlb&o z6486e#Fbtulj{Z)Eqegv5{@?qH1!1y2!)&NAh8Zon2aB=!)%AxHTGvM*x9&9Y>jeR zSd!{XaBH?GmnptJC8NAzua@v|xqH~&BE|H`caDoHBdR(>DN5Lu2O*NTr&tXgc!# z`vvWYFbHdHUxxNnnznu{z?r*2Tz!5M*+cJ1;lMlV(f)%L=oN*9kc$+`QM8n6SS;}G45b@8~Eeje5ePUuD+rf9yasK6uBOE#4 zsDngjnF-r>YDgR{>Pq_i`dKR-Q}{otPr`30rXk9{Iu<`g*_d9%#dnD%>v-0g{%(?< z`oFyTw7Fvb(&;$&skTF-`KxH=)5nJALB9rSWHx++8Hy+$08HJaFuf{P$9?U2G}THK0>i)pSrNu?J~k^PB@aFnTa!e#>qJb= z58YHOylLfCuu@h66)QIRKzc##@afpsuUR@+cYv_R1~W#JT@oHTtOQWO=WZSo_#L~w zyri)ZTrEHp>}v>xuRyUa?J6sbUJNEi)d=+Sk?>1Fhnh$c-kgzz5VapOR>0Tj=K)1Z zr28o>EPkvPdx-ZFNEeBgDWWOZc|vvy+Fu}B@&Vs%wGldgdETFu3n~|CEW5}5NaB7n zK61{ecA!71F!Lx7ATWbr_((x5Z+WSjHI6fD2G2S{+dDEtT6~&;DAy}9k{P>%X~d`` zU7J($TSM7piE!8+Kch%ACv+xC9HiTSUaG&M%MtLvQ8y+Rx}~4?MraC1Ph8L<^qUwk zvA#~}yvKXG>Gjm8V7wr*8DuI>3QT)o*xKbd<2lIISo3?@JsR5WdbfC*jQQ6mjq5>VF>+w&hj3`fu$uz2pAYLZ>6NL9T&y9CGk>*kl6gmlb%=Y6`vSL`4x6-VelT|4()}%iY4M+J zPEd3NdgLCa!MA^$vG2=8l!qCgiJYSIZpY7^gG26xAfg!5urY0U2iTbDooD8I<9k{q z*>TlxNfi-okVW%pKds#Er?XccJrSDmeC_uN{dd+gdMglNqf~Dh-q|Q%Q^Hb)Yu*Z( zjXKUw7vDhI#;^B%QdbvHcbJ|+C1NzEn$oJwwVqharZ4t*8`@dP)~^nO2Gl}+vx>$L zP5s3ve%IeRc|Ug??%y1$BOKQ0l@xDT8elJ!1F_JrFT!kynDpgl=Wc>$?F)vO5KH2; za9<)-3|)6+JbT&lI=<}yE93ELPSo!#>0Uhad?>Hc=>C^}*?S^3D`danAAK%OEQM|{ z)J^2`PhU9>8OMUW(hI20Atc1vMxw=O!}OK@nRdHZgL;Q@cy{LaV!buwk7$DqT8u&&JoWKW^Dga_t3WPJpAh~dHF5N_$j zDLX98OV|>6yGaE5hx5b7qSYrV4p&AaA~LvgLy{h;VLd+58FGRGQ;6*PCI3|6jv+g$@}_Mc^^bXO`J>}n=kUm zuDRMhEi;Rm+A5FbcSG$jE^SYe8c}(FE7*S^FyJN!3l~n-@N^4J~Ep) zoQJIGUtj6RKYF=i!)TTh1ShlCXS(F;1L!!#!TEm4=nXBLx9kWhb4sDotO4tz>;Sss zI}1_s&bpGd6Y7%FzLMj-BnQ|()P&k@O{i88Vm*wZIbSZw_w9LR?j-MM&ZW8#pxO>F z#eE(Cw7LeM5x7Mi=g-yT(~MQZ?9>==0h5y6vz`wBBGjUv%vf&>ii;p1u=J3lWwj6q z<>`;{5xfC-u<|HcBHYFIa$5zo{rAG)Q?5do3?q}nX%a+DoN{WZbvruT=ewQko+*BW zS}Tj1%1-0uyWk|15l-e^r*_pdN-!HQIeX6R11*1fvIbOM1gC^r2{H?8hDG|=ZUgl5 z-!IDmACVSo0@yMlN;kgY+7^lA1k7^&K~Oy0D}Tf77|cX0Rh@~6;<45`46j^2BVsuC zfI0OP2398miOLvK3ggCnQMt?Na(DO|u~`gN=3myD6m!`qul5_<-hJGC&*nKDJP%Ax zsVhCjWe6ffrvIVX_!Nx*m_!oyar=^7L(TMm=Da^nM58MT#s@8tf>y2iuK92DPCTjE z6Q*Bi?gearx^QC;_UuzUM0OR{_?*V^+rpUC4p-_aL zoI_}}0akAy@hXDAe;QyHKd3aE&p$}3Ne88Tc8Lg_Kw*JygkE+^Vu(gL2 zdR~)=^LspDxP~Eqb=psV&nP-;d=rt#Y6%fr8@x5K-KIVf=l}N{L|3A!gPG-9zsL$hZUB1u(M9?B-ScXE077Et;84)!Rtgb8 zHJtVwqAdr?Nov&J!Qa1A_M2+JaN6eOA}*Z? zzij!?(()o+`bjgQ#|xU?8f>}tudw=a?X@-0*0EClm`x|LpPiN*iFR2TuRDLM`tAx6 zu{Hd69kj*{M%!PEwrTNr)UXFU^uT0KIqglrJ2I6?Rx=g4G~!Yqw$pQpUx%>k|BZD0 zH>Cn80pyeBQa%klh`F#1@TZ4IQ{*X;A*jioTeUB=!L+_uw!wwO2Pr8TU{Nlehn8!V zc;Yz0@#wTSVSY9TwjHCgWmy-aso(x>K}@u=Th6qoz|PJSv^6PibnxMOcK)ks{&f*S zcj5+C0KGT|qUKgK;;J}gQ*<%J(dZPvslSe09l7ZfAs#o7!OClpxPAsXqf(YI2u!LM z3{504P)y>vV-{L%8{@7^?`)6q`Aqg`(Mq$R$WhmSRF{2xt(mvdE^kQmL9}l8rQ!cL z-#?#$g7i&#ddTXkJ$z-vXtf6xrwJ18)^pkx7O8a{$BUIVzn}z?$Tp8Mq(|W< zS4T}UExdnzz0|+wvCn$HRgLA)Sy0R67BD2czUSpI4%XvJb(FbwZmU$l|d03dakDQyrrNu z1B~BE4iH3Vfkyl*xEWXha6W26Y>>A!a{aq0O&8p+g2U&AnTZS(i!Q7UsEx@VmR+Gg z>?0vgQ6$ia=kAm07+@cB(s=q!Y_+02ApA*{y}KjN^PB%I+@s&iZMnF&Zxs+MJzDNi z* z>H56*`GH>TbL2HQ*u_PPf*1Hb&12uEf>;R&#Yf3|v0^T2uItmm%T(om3Kac_94KR4 z>raKs2)7#QnrKc|m&>Po46dtDV6CdOEJuiTtKDturf6#0zQ%lq|_3fj; z8ow?xrt8Bu-ReF*`n?AeWqi}md0s+h6^2)ikmwM(AWyRLvA^)Y#y_DSzcR-)Y!&uj z&xF(sO&vbejx8h?K=j-k4=ByF0Ol{OOs7YX1tVD{3^7We+jgIxQ{EqGq$Df z=RCZcA(|Ny>%Nd&uU`!ni@_;WbF%_M6QtJZ%CvGN2Zb0IcAJ28q?&5=p?+?gsT$jV z-g>Y_Enw6YCTE?DZs28u;F?g&-K&Y%#0FL&KBw$;<653chLE0)A+;la`uYB($g#Pi zIImF{y{jvPJ0|72eF)^f&3FdMDlvgY^c;rkXt#?6$0*(<#PWgmq8cEwSfFRJa-wPf zzaPsWX~@Ox&GR%l&}SYbpoMnD$)Js0WTqZ1hYF3_Y)q+IIkxsqH8D<;#>|uh%ZL(J zLhk;+=_>Y`-3NaAbweP}6-K=5ES3ut8f0(LUbt7CuK36jz+nQ9tPoRer3|v)+43Jp zvlmd{rVIP=BFJ@%>V{aWm|@Ey$66hRceB2g26Ze}hoQ=2^8xV_MgQ{m>6(=IO%fPY zWeT-Z!AtIr^s+-HoJ}&7*21q`{%*H@UR8v|I)7pT&@YYx${kabvQHisl;3~Z zx!D}|uh=aD42#?Mb1|ePmR1l7t3$k^-t-1T+_;H!H2dZPnV%F7p7~TQbZHtPhxq!+ zY2^HIf|?-c_#(N-Yp11=$sakIF;H*|=LZyT{rRf;FTuv7b9_pJzAx1f(!tH#)Kg$S zIcrTlFp1eCNN*u{;IZQB&a~ZzOVa;PGOyxc)hMgl4aRFJ(jO`h6xWfqSN;4sNnTeZJ*k$KmSsgkD%t{%fgQg7^Y@Jm|Jp0(kvovs_Y*oa=7Kd< z)P7;;kCVcZ{qT!Tg!2;VIHfc#c3YRqEzC~(yj!D!5-TWY zxY<}_s}~%RxxO*T)*@<04y`cVLbyBr^x|5BMYgdaE>z!19Q5!y=5tik{}iAz_Jf78 z9=O_zmZc0r%w%M``F3eAts^$f9P#mWwlY{JHVDgPbg+#sta0hECo!sWj%}67iuLdr z_9yWjhC9{38dH;802_O1Eo~SWs^7dBaWfzI#q)(i_or3a+1MIYO^`eql2rtW9no!O z^CKZe$I-^KD3Bp-fhmafK{lHPLVFW~NCZdg&ui0y2&^F0TsPd+biWWc@lw>>du@0t zSt`x~Ob%6ECtp_Z#s6BHCP`XzE>eggOAa&J$Q#rX`IYpiXNr@h@i)@|9J^|5n_*c9=Zej z=74Eu$qr>z?S;Vy;{SP^r}|i7eVu90yW0MH{6~z2koeEA9KLNeF&EfQQT3Tb-ouc_ z)EE!zIrErL$)M)tt5=^Qv`(1%-aS~-sld&inEjCVecu5}(X?s` zIr!+eb8q*NXDBvjk#8|x?Y+q)L%C!rhKraID$l4!Xd>);OjcW&V^u!H`Tq`OAB|zn zmCqXg6o&bp4lulxGBHU?*Dbq+6tLhne}1Wp2eD%hNy{u%0A>Fm2UobBY+5wYHMUd` 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∋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 diff --git a/docs/system-context-diagram.md b/docs/system-context-diagram.md index 0bef4d543..cc4bcfd58 100644 --- a/docs/system-context-diagram.md +++ b/docs/system-context-diagram.md @@ -8,6 +8,6 @@ Detail isn't important here as this is your zoomed out view showing a big pictur This is an example System Context diagram for a fictional Internet Banking System. It shows the people who use it, and the other software systems that the Internet Banking System has a relationship with. Personal Customers of the bank use the Internet Banking System to view information about their bank accounts, and to make payments. The Internet Banking System itself uses the bank's existing Mainframe Banking System to do this, and uses the bank's existing E-mail System to send e-mails to customers. -![An example System Context diagram](images/system-context-diagram-1.png) +![An example System Context diagram](https://static.structurizr.com/workspace/36141/diagrams/SystemContext.png) See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#SystemContext](https://structurizr.com/share/36141/diagrams#SystemContext) for the diagram. \ No newline at end of file diff --git a/docs/system-landscape-diagram.md b/docs/system-landscape-diagram.md index 7e7b0d599..457998bf7 100644 --- a/docs/system-landscape-diagram.md +++ b/docs/system-landscape-diagram.md @@ -8,6 +8,6 @@ Essentially this is a high-level map of the software systems at the enterprise l As an example, a System Landscape diagram for a simplified, fictional bank might look something like this. -![An example System Landscape diagram](images/system-landscape-diagram-1.png) +![An example System Landscape diagram](https://static.structurizr.com/workspace/28201/diagrams/SystemLandscape.png) See [SystemLandscape.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/SystemLandscape.java) for the code, and [https://structurizr.com/share/28201/diagrams#SystemLandscape](https://structurizr.com/share/28201/diagrams#SystemLandscape) for the diagram. \ No newline at end of file From 0dbfce7ec3a623cf0e1e132efbb629bcd59c210d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 15 Aug 2022 18:24:11 +0100 Subject: [PATCH 025/418] Some readme tweaks. --- README.md | 11 +++++------ docs/images/documentation-1.png | Bin 614701 -> 0 bytes docs/images/documentation-2.png | Bin 542540 -> 0 bytes docs/images/documentation-3.png | Bin 413890 -> 0 bytes docs/images/readme-1.png | Bin 84348 -> 0 bytes 5 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 docs/images/documentation-1.png delete mode 100644 docs/images/documentation-2.png delete mode 100644 docs/images/documentation-3.png delete mode 100644 docs/images/readme-1.png diff --git a/README.md b/README.md index 49a6872b3..df19c8b72 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,12 @@ # Structurizr for Java -This GitHub repository is an official client library for the [Structurizr](https://structurizr.com) cloud service and on-premises installation, both of which are web-based publishing platforms for software architecture models based upon the [C4 model](https://c4model.com). The component finder, adr-tools importer, and alternative diagram export formats (e.g. PlantUML) can be found at [Structurizr for Java extensions](https://github.com/structurizr/java-extensions). +This GitHub repository is (1) a client library for the [Structurizr](https://structurizr.com) cloud service and on-premises installation +and (2) a way to create a Structurizr workspace using Java code. Looking for the [Structurizr DSL](https://github.com/structurizr/dsl) instead? ## A quick example -As an example, the following Java code can be used to create a software architecture __model__ and an associated __view__ that describes a user using a software system. +As an example, the following Java code can be used to create a software architecture __model__ and an associated __view__ that describes a user using a software system, based upon the [C4 model](https://c4model.com). ```java public static void main(String[] args) throws Exception { @@ -24,10 +25,8 @@ public static void main(String[] args) throws Exception { } ``` -The view can then be exported to be visualised using the [Structurizr cloud service/on-premises installation](https://structurizr.com), -or other formats including PlantUML, Mermaid, and WebSequenceDiagrams via the [structurizr-export library](https://github.com/structurizr/export). - -![Views can be exported and visualised via a number of tools](docs/images/readme-1.png) +The view can then be exported to be visualised using the [Structurizr cloud service/on-premises installation/Lite](https://structurizr.com), +or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via the [structurizr-export library](https://github.com/structurizr/export). ## Table of contents diff --git a/docs/images/documentation-1.png b/docs/images/documentation-1.png deleted file mode 100644 index 4b354c86826e609dec949ecc1c08fa7a556b2afa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 614701 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

xLiY8k7I?yJ5O< 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ö zMtz6DCpX1wWlp4+tSJ2;dwyidK~Kv(gvF{}n9Zw*LgUyV@*l#N5Ue>Be?Uk5V1Pea zNvG2JH=_Ps3_*re5>;@%z)1T2yQAIdKw)1Q+#))jfUn^c9~_-G$uk&vMfr@$q1Bmg zGO$h@_)US{MHiK;6-5XK7fu(qjp0U^&Ag6bAg2Q&Q`HKFKTzHpa0+eXQN6+*u}~^R zdbrd4`bi&**Mx3tM&R1(UvdgLEimI(}9gp#hk!mpQ9TI+>t?S?Z5@a zjQb=fI|;}`#U+BAL7E(lF<)U-m>GSMhesY!g^tg={FXm}Vd8{+o6hdDbx6*nOXi2S z^(B)nkDFt%q`nL}t@@l#a+Cey&VHYhYp~IV@Jpc4U5V4}`02X-`#l0_O3Yu#X*-9~ z{6FHCu?=3LcQqQ&hhJj-ED5>xZtEveoKz{Y;Z@ zm2{-SB7uoYx3`fM((pGOw~)gnfeokxfC|Xg?IMdrH^mTyrq6;$+5pRnHIu(ck!v{7p0_+y-jM2;BrMGj=&I_qfs8$X5ls=*LSV6M=|K?dr7& zrFQ10EaOBGFNs_VE+`#G`|qt|JYzCptOW=OyafRVt&KpZPu4YS3Zm8fKIHQvCw?i2 ztEp$&i|yD8nmWI!0G<={jTR_LbzdVWF&XJ3WD`tfHb)3`Lyh!o84h6sh65-Dc43&} zoAw+O)Spc_@G358O6>Zo@S~ZN<)X%J0|2PBkyM1&ph zW0G0g-7PRZgi!rFv2_DG?^z)sR1F*J5Sw&lh9+-MGJ{U z93XKIst;F$NSvhZi z&QS^Z6x*?F<9Zw6{U7-6E{Ex27FRR{qq&}oexQj8!#BP=&5)67?YU8ZhjY;Zev4qe zHuu{mfVl2nV-&WPVcZQP;wo-i4*fVMT8^{w5`DbkYn8^pGK%Avj0w5`5Ai$qtQOMA{6`%J>}w%S{e9!K-b=c1O_5H}(Y>jE-QwGtZSbUK`EL=H zd+5F->!{b5zFW&W>ZC%k{ish$H{t(w;eVBevzq>RcE>%$zTm$-`xKRZa!7g^o{VCF zJUk~g^wb%lY?reAXRtsZOEQSb^6Y{h)$3fiS8_!h|LnB^GFP<*$wLI9CEH+o%M#Ob zV9=jEX5z45-<61Q-=MKIJ~L!6NB2$y%AIFI4i+f);AO%=F=6yLl~v2@)GXy-yoDu!YeLE z6g=7Hbw#o2LivmIwOya(kkj+C9=i@b$j_*JoU@u4Wf}dC-ID@(S?29MO$jXOsC%(% zA-8ot{`o6Jj$8fTH9jte@HM`g607hmoPx#Rg3DOP4!H^D z<(^)yNPW`aE*8Qt#72{kD z+&m{BypT0^=3PB=rqPOVi8Rz!G;ieV>4s?QjJ^UqmFu$n<1p6=TO9&dBr({Dc)P-_ zFBdB1_@fpW(0E{2f(g&NxaPm-ljiRvp}d9aHSe?kASV+E@q7s&x4$ z*>a6?Q=S<%2Xn+tBH#iHP*d5cNzD$wavh0gbf8;t&n64c&XwFRWr8*<{Z~*=z---fT)=bUgCLK zr%4mKaFme`7X~ug=OoR$De`AqM%Wa{dCNA<7y#=D)x`{g6<^LC-n%a-t8R-ckIbdK zwN5`E#Mvi9!shYDT6q+wN;n7*_{A8GYYr+EUxn|cDd(se8U6zr0U2mvxZ8{7Z_=wQn_uyf_uAt zqef2a{=oy2&-%iFldM0r3XPB4p9N7@QjaSig1##Bw?am*Q9yTiwh$Pb3s9(Oh_VU_ z_?A^98Xe6y6PX=S!Bp9j(xmj{N%)EY{BG*>IX%-Gi8gQtka(pcGsR2}PRQ@d7>_{E z(fiW01BQ)&{dX)oZq(IG9rH=vu=K@ttnDIXeio_XGvs{IEaROY3~o=WO3{v%$%%fj zyrNh$kcz-BITKEm^tC2~^yJq#whQhV>zBZ2v21~Vk!h6J6q5U@vQ|7HBZ!NP6^vkz zimXbuxbBMgSTUSX0e+Eyk?oL&52FtqOUkaHuma1vnE&>s>DJLGvod zdneVs)a6;)n~6iJth@FL>iTS-7#q51yYQ=%rs>rIdMz+$y^ToF z={k$6z3aAdw(M=KgO|$L6nE*m-gl-Fqj)>obui6MJ6-^Wsx9g4LxGeN@ygg~g(hXx zwPq?B>Yn*d9xFu6T+Q{1#;2d5u9$gDF?qNp$A30Ama!DE$ zm59%efP|P3$~t<+Z-W4sw{}cou8uHf!yEjluc(z#!JkbUPEp334VL2Afo|om@|75CXlI#+^)j&{Y zmrk(5tQoE82h1!ABxn&tt%8W%>k3qXw?{-HsqCX9Wp6gp1Lu~M&G+mqw=`pNi_iuZ zLIOOf*7Zg)OJ!4JMJu$Mg&w}d0O610#e0Ax)W47dh=HJfYl z#*OIST*2gRxG0%P(ctS3#qJM3_0%Lkd?N^6zB4L#f*JfhD#;T*Gv(S5X;qa$teEc8 z*LZo!!mLzJRw}XGMpQ?MO-#Y;fFQGkXnF8zKgGv(DDU|V*$gpfTJ^Br%;uxL+iArH z3|7jg!?LNe$%zM@5An-gCG-!b-pB>E9t+r=VC$X7%B<&X+4U|6vC8SOwdUjEgEg)i z|0RQu`b6&|cEC~-OLAtKW};nJo75t_uAJOm%wgUB=ykj0;;&CdQOTgY=NmdmvbJ-W z$~zv1o);*SXq%H%Y4eSvHg;uF;4|AZLZOq@!vJnt3t~_`ou9xeK>~f0U9Gp3mN(4V z`LT9P%m?kMVT0q`-^c9pnY{A=t3bSfxpVx{G1Ai5VNJ!$CtBazu zL^!ixn=;Gk5@N)nUB1n_fH1F6^ss)NjB~iP=>VsXdJ(&B! zmc(qO_1FDts4G8K#Bs~&^+Cr3(G_Dx z!Vt#Q8zT;A)!-Ajr@PUGkMU*?FL4Uvew2%g=}?%K*a+R-F-8wku5=X;I;2IF@(p7^ zRe$vyP>i0tD{80=q)E8aMKkzn(&5UG^uu#&fj655aRktBlp)gh>0!K?sE>O|9eVUp z&5b4#D{fsUn58K|rI|^jH0Pys9xAMF6Rb-sSE`Lo+ExcYq`(4g5y>0SA1VbAZ*)MV zST$OzLiwyXIuLU1v6FMq$6WZQwOl!(C9dyv38KAz4JTRPl{UNKPo(WDkZFw|CC0)q zkac@PQ<5c4#*@+%y&W+GicKgzeKjZw_r%QZjE1<)+&9FSE~N*eKyaI z-r|B|U&##{;3&n6W%Bl3`hCQw*lQV1|>TR6gOvhy{dsx~Mkav2+4lrlf|5Mohv{k*5)z3$@vH2xae9b<#YH;Yy zPIpC$=EnMv(b5?R+@#?}Wh<)r!v7QRI_qgbDE=kVV$eHBlYoNgk@@ZZ@HZ+0D@2Xw zsFI^6i5}WD18;doG*priB~hZ)KqI9+U{vsECSL`ET_rPlfBj-CrBp?e-K0HWUl0eF zh6pPnfWDGROSwfjRIri;&~$dz{@Wg(KD2TudV1FC_PED{VX^9mI+xG zU*Uo@uci(1s8vubU#pmVdr297Xw*5rXW7>6lqBUVckPyVCLo;h@qi(+43VZTJ6Uk6 z!sPP_4QJ=hk6}B@2o;Flzf{XPH}z(3j6ew|LpaXli0vfP-m7@H039#xGBBapw~!7+ zX!Z{@;<%RX;O^eD$9#b|#Dvb;3Yw{CD)J%jh<)jow%#9C%@!2wo5@0>%-^QEY)sn#-r?KuTsWFnQM4IV*nXx?&=?NRCZg+ z;FHg#)1Fun>Hf$2TN!StW)!(2x9YlK*b1YXDR+b$Q$QEkx5h~A@G9$B8)HFgw=S@AeKg@C`+`|jSmYz13DwG_+ zkrKbqSz*8srLIVjal2d*m1*AlnA2v#H(lq5_U2a~_tS@5<yx3U)D$9r70Y_bP1? zJg(uQD=u&|P+ZLLp4dIysmzs&g+E)*o!*kMogj|WLg8g>Zj&<*5Bo@^w!e0IQzbi8 zXPiiBVAQqUTO4mYHpY2p8gJ6oWehOsuMfEHW1xol}`%QLi*)FG+ z9Hl#4_HNv%R*2VPy3F43C#Yf@tWc-l4FZd9e9d<}o^cIVl*8rAfPGM6bIgd5;R7nE zdSTN)ba@nLjgF6%XCB7UxO_d85=>`&`hr#!;2?=dcO2kyFR?8mdwNP7zOefXk8+a* zhl#t0gE>j?dSydA5b=a6=K7l4%6pvgZh3hVS4S@3`@LWrysSkLtCVSA=nyuSryS zm#pJF0!^y1C&9rgpZ)a}vZkJ5hY$3qdBJhdXH+-3^=KgjcDd|A4V2igWeZNRj+zyG z!R!G0pP=USH$VOw6sPek>bqp~jy*8O;pplqQcud#bN5{XXNN;O)(SHL5yigb))YrC z%Xq3{+H%ov(*z4&t^fp?ZGe=dgk_h%pDDx_)~#{A*a z4TxuN$-Y@b{4|-(roUqn_X_<47oR}v(>kijwM*w?6#ts3Y-ar#vugh8H~GeFl>&)l zw^tgD?0YOK*j#&Hi{#*imAom!@FiB3-?J?Z!qp(b{=7rlXA+0XvIe3QHE_a{e7?2p zH`wWqp|bGc=GptK;kGnU(rQGm@gJaJN_B=O+oVWu8gPCjNZl!uim-(3NOM;IR0xVg>wnH*`5)xe z-+x4JbCcBno%SP|B1*1Uf8eL4{ZYujMU>U!1cn(!hdK7g^$c48%K2%9778`!gq+EM z1Y6<8ex02QGvfSC^w(#ZP&7G-qpzgP;=!5li%(|IQo`Koq--$QT*jSx8tPJL8aJdB zcgjW&{#k61;YW}W!J(Q`0;63o0-O)4?E}A7j5DiT|Ee$Chh$wxabj`L_z&L>nnQyw z3WfSa#3WZ-PQvShq!}A8(isY2XG)uDPRLQrrImO|$OuDrMOi=S+XY}1o}@wagsMrs zs#dGG2&7_(21wl=XFJ(LTyHl{uh3FY!-cc?=VVp5jP+?JCoa8jX4tgcGy2vTX?xR~ zNdx1J`G5pyjWW3x?(hlXK{MU<(869;q|N#@&V8l>bF33lTeivl)A4&D^Eq+1O{WZv z&yH_&cg7uDg#8~#bW>ClT~^(QTvCtJPq}GM&j#OM^NB>zrrg>)5fEf#oOC`Xye)Nf zA|<_&)_6C)Lsu|WYnrrWvytm1dJj{ zb3txk9;O>+E_uSyx(DT_PEFVk>^o{KvCwG7l&G$mBY4*|6lBvbW0bMtQN*Acx$(l= z=o)PK&w1{3pyTe?xI;Erw7B~>telFV>|6ML`g`0>JZloSAShilatJeJz#@`AdawRo z7j9X>!CK9YiV-HoZ@&DGyQhH{=faXRTEXh6?>g|){)PgA3`D9$J-!DT(ZmSEG@odh zzZ2O+%`OG$eE80QqnzB&L44lJ@wN9>lKhT)X4`p!{B%V+uq((1RrqGf^2*Plp43bN2 z-UNoTk1pS@Usv7zIqq&;NE6`g6ZDW#(RRPw@$ymy8H%#p*vfm#{273BH{cpK0;?hX zy`y+!wzOH{nrBe~j%Rv*Vr=T>uy$VISCS8IvGwxP8r{GXMTKi0^%cX)H_}z#=~woK zmh@?L7&ZYo*o$DUL?LMdz)*!RvDi9Ou;ga%YtHkYB z>&Ndq!T|Tp658MP&xJ=<0aSLuAns3*r zKI>d)i$s<_>2Ad7_0CIgtp`eI;E>|o!F5WY^m3blR4=Z5TJ2knrD-+S#Y+^2qDFLzoy&c2Nyb#sl5^MzlR28nO z)vq_l&yvDe`+4W0X)*)1N$9hgk7;6y44DR`Rs;m|Xdz{CUx|g-vT5Ar<)~LB$ibcFvRieyX$*d8WGKcTx z$64KsTQ;WsChVBW=2E4A3<(}nP!@m9RVVxGi|Seiiipd)00-!fDXnN zlBkc(?$qt;CHZcNUJ<*;it0ohr7K?@!ofTxkL}NEf{ZwB!x8;ek?)4vK3V^ms5=`( zoaHll?={|5Bg50UZhb$Y_}gu4Am(6e8DIg%(HG#iv5kqlw{tF#TbO@!cSG;;GyY}w zZT+$$8)NY?V_>;(?{R*z@RQ&G^GMl5(&R`PB4;VnF}=PNId@xT- zS;(-521pp|Q<;~XrU5*Ne~XL%p8gR&(L^$3-pDH~{x%(_s?va2q%o6dSYn!+eZ@ge z2{cL+_kij1kwdziF|eeOfMt9Vk=9Lb5nef4UZ3@RXWvg`_CuJ#zt4__ zg2saK+l9;WjZY)e^@T$E*B2bp;1E-4m?7Vk&UIyG8CF!F4&`@$<1RW>VFVY?odzFn_dYO_Zg zPOqU+H4$}|lUdy+Pz28heZ8FQT%cs$+{6+}pSq%Qu4@91BKd7WZh{uaxLHO50H6MK zypdq8r}V-hm6;2~?dZmod2CJgyAk z7F3E5WD~zYaOSb3Gp$urm&d`Ze~{~tS)HhSD9K3ROlnHnGh8s3t4#a7FF)!|xT-^k zG{J?ewbjI(H6c zA7{lNm4or+=Qn|KwH1yYn@=)PyPg^^%t^A}v&K5IrCNa`LTL32JsQN{;Uf#?XtWFJ z>JOg@S19WvHPC7z`@J%TaJ+=AM5j{Vtw4M0K}9!yIu~ zKF{PnKSS8W8dSBwRX)Z*#`^`^%)d(3%&Cgx=>XNsQ~#+s;gL1!65%X&nOeYp-;eS%-w0<`13@lJpMMcsSXEuz8e8h=g`zPU&plGKN&=M4i9M%}2&?gY?;?YL%rU#VxW zBOt=a5n!Eec2{d#++KC}Io|3lZmdf3XIgpaX@nuD<#Va%=1%AB<&}9CN6|u1)VMkW zc3>&{sVLC#P$%VkEUrjf4+?K^dg(aga|=%O7Y_3R#_x2;Cob;}wNl5Je&B5B9c@?~ z;&7iI%2zB`2WS! zS4Fk8u3hgc1xjhr;suHoEfySFTml7(TXBbCg#aO?6e#X4!KJuMffR>e#XY!tkU)U& zXYcc!{hu+`#k$JH8snXrb3SuEuYF1z{HPMPKi66L{AMJ;rJz%~#ZP^KZ;!m1<9mgF zg|X>?7T~s*ZfcFfEMJjsx`5IIt-eb6Q0lM$KCfc`B8g@=S!Vh_ocsEf=ALkhB_I7y zHLgIm@XOAn<`I@)qtEeSD)_BvIrSwuBAEPP-GYsLi>}uil|;9w%-b%H0;BjVnHsL8 z5UWSp>B&#D(mzBB5HH_!1*MQVEPu%$wR%hwm=Z0^)-UexO?0_#aVota-i#2-tpKs`7wd@f zDcZ&PRhP2QhQrx+1Fs8zz2$J<4I{}W`(&sVKt&Cta;lRh6^+zDRQhrtZ64!4R~;m} zF6maEG{-lL3S@i#v2#>VDY@|nF{j>v4expvpS;smE1OB)vi^BX(qSd9YMK*4H?Gi1 z1u!MSSgEPx=sg)A@0W#VWd5&IcFpU8cw$i1Mry-Mtd2c5e?fv0+I~-F*#qv(fg*IIJKSWN$X!&2d4UeX47B zNd*XPmXQ#4GR~*p5YuMv09AZoNdqTNo20}_2YNN|Dq}?dqI+(J7_IrJKr-0l$`kg(j9=)LjPA%}$8BtUair2htE7lhP4o;cqS2-%egOFS*!UrF~0V zs~HY5h*7y#tNU!W`wUa0x3+4Pz!_|EiAABp{nIet9{&vMl1~rbYPWY-hN|IgXs?IF zy9UEQX_5zvOqqP#Miff(ukNj(VcXmLh5jZe^`xbayt@E&U3t;cKyY@rM{xY6JgVrA zc*i`2J>>lgZ>J!SEv3aAONAV80CwZBI!sCWzVb6J*{Lu*tPxYRV3_740sO3Sker`z zXh)I(p)A7Qw3by2J$y)vDH%CQ>2*_5blX&cRvsRr_#j9zf-}FD^<7u>+^5v>wNB^)K-ZS_Q|ssv^Q!)) z&czq(hm=C*ab|>H3FF|A{=m1dH!K>Y8I`f5=EGU z?;Nv(DLaX@Iq@?YGdr+fQ?Svo!s7Ex+MP9Kwq=D!-gEe%z7WVwAZR1l{< zG;HfHYMV~r%AIq$U8cIzm7VG*vy|$QTbP~ZgwLwR+tQ*+Pg<&?$2@a4I5TIn`+qr1 zu0q5Cv`8-M2)im8Q3`%#)t_etmXM0(&khn+J>|8zcx; zZ_6IDgQ~N0)^p!&P2=!=<$lBYKN;HAvx9{xopB%qFyF-^y6;HW_?)w66(%+~GcV^o zBtCq)siV~|m8(t&8m1}iR&$yhA6v-mujgBQIXOt8_U>OS`s<+zmUda>e6xP_*Zu!I zxVTu_wI&m@nJeK>WydwYrP};fW!NsxEime^lHtQPQhIW0%qUZ#WnJZUfu7+i0bk3E z~(ohLGFWFfmMAD*W)o%0Jbu-Pr#c%Ml^}l?z9r}2OC6ypESfW}{@`?aT+7ssc zf=Xb|bSEs!+qYgfq+(ffwPrD%CH_z)?QifAH{`>k=&h`#NLm{7>y-rVSCI8i#qrg; zk2;(W5g4T{5W0@4rr=i4zl9%=eX%4&1S+KeYa~b6% z5aX7kH6u&X4KZDp-s23#c!SYws(m@IVBL|OCFH2x~9qOV)-?9U>Xr+II(Dt7Mm^vkb zv^@LR)5rQv5t^M!$UtQE7$po&*^3dH|Mka(R>;6dT<5Wv zlYS_ba`-8~>I#*+*)D+ZV@S8VC+f`+_^qxR)$9XC^g2hpxpX`T8HDWW;cSQ!erTHd zUBny&mvsJv4g#ocT$0+h%dr~_aUW5v`>>duo5XBsT)4TnxhP%KWvy*V1UjfYmT{|U zkV!)d(PbRtw&O$O4hI?ogtN6F`X=lFE-F(8<~xT%NB4?~D_Ngob-4xWq15v-3{aH( zxk5lAjy`3|e5a7wgilKFKH6+7f^Eq%O>j;u&-5wx06v|>@il!axKp%+FFBF-nMJmP zl{E3vzfunS@}2nWeGMV1mU~#O_s>(w{EEdxg;=v$$91*uFkW+qSxA^2AbH751l`S^ zJ_5z$V+R;K1sHr(!$Sd7pmnr%A#7XhD+!kw&uQ^IcCIhCxlxmn+L zPikq{^whLcP$5UBo3Gj7?0q2Ez%4~Yb>C1GG4JS*=p<*eAqSgbT*p1JNcYs`Ih7#Ou_F zatTLUY>+t&aCw69gwNO>m)~@H zO-9tuh{LDPU3 z7`Fg(6bWv3IUbh4M@}MX$pOC{gh_y~>S5JiI|}u6L6~GVo7)r*kqPQhu@}66ESwi0 zXf0Rd8OQpDAaomZ&6IlQ9;*E`4OLS9%uU=293Dx7cfq!?!s}qWE@(grQomE3=clc_ zNj9QNW+mV~ieuO3y?cBOZFg6AU4uvOB=P#?_lxhAEzyN1*XO;q=yXpwcJx~%k5XOLj-R3D=EUEh(w%GC4?cFWwH)F}eCnNXqgeN>=yQ4X%U{(; zyw3dA!_)ONh!$Z5uugi6oVl-sh=JL^5CkdzYa=LMKori%rl4NcCa zSj>^fuW>~uUyC_E{S~@!4}TV&EvMD4*L02KIrpT=B@19`sBD~_;)LI-#_wF#zXG)> z63H{N0JDvB=lMnc%nwCJ-H#I8zUf`#|3hIK4q8D~d++y8I^CJlsYY};84<)tP=}@OEi=um=nyNca8+sZiPPg#YTRRopJ1$|G=PnoYvt>OvoCtn9$(CMWPPjus`H zTBGMa!R!+d2WtPUv{W%p*QQsHfU{StK9^Gl4PeP^Dt}Lw#FSf!>oiRcsxm5waMsx- zMBPcT-yJh1&`MfRtg~Q#T?$>n5~t0O2v_3J&*nd1UfcmSE%?aLjbcurS^bL`47P<= z(EBFtY#e#rO~vn7ZA8fCRk4A(>3)HBYT?wuo*G{AxcMQiJCM^}&_z+3&W*^=EaXN=5tK-?-K65)GNu zoQM-KrFy25FCXxLK27`022oi>*%%Ulvbjwoka!`MbJu~O+S`?Ct?R%mL>HJv zX#I}Ro-Q8g29%0>1{$e5MVOxST(djOo)=_?*Kh6I?A#N<97U=dOcdxc z#k1T_)~9LL{*YTNSl;wvS+(u& zaoNKN8hZBeMz0Mu&W4BO!^&F}xx%j&dmeD^$7AA!j?NlE!K^%1Ct+isMJB|EK@8ao zDHGioq5MX!yV`fCLri2(g^1NZ-82Wpjie$U_?)y!PR3`7vz6z>Q#8IE>FYX|j!m%yN`}imWQCEyhn4 zVgNQ+Vx>5@G?RRgnX)DD9rohj>pJc6!tT3IHw6j(G2EBWbCS$^oT$fC`-mo|-55)- zo*^`y4?nZ*2uxyNY*cKVTrxAjYfB3 zL)3R5kLOh{o7KQU0`yS*DCC8XVn4SNW*tN+q@HEY-8;utqwVw&bEJ|YICy%=*zn+% z{zZE8XgrA{(#};IRPQQ4<1FAx4*#_t!?kpM8|^UYn0e)G!XCbOb zS#qTE^yf(J&7W|=xBUA*$g9Kq8s&xXA^G`%hCa2TgF*H@`s)w0d%!rQd6b1V8dl@9 zcA1pU`8TmuqxU4k-58gGuZXS;m8sq==H`ksmJ`nw#%lLbTQUg4#s(6tdl#x=%P&P- ztRwKg$XOaue7^U8BIs`^6djKBX<|=0rvH^ja`#l8foN}}tlWke;VP2DRT>>JM zP=cQRvfV=RY5?1tItkaOjq_ctyZM5%R2xpG7*0wU&(0gYu8@!53bpk~n(nv!=-XNq zb`#anV^WsHo{IZ~xvM0Xx<1iXNQvPgc)~qOY}D;fb|8%()xz@g-Wnva^JkG2$IsP6 zCSxYQBGR&5tyBH_2fVe)`)^yGp&U1$=c>THrt^Wpk`TRonG6sR5wIARTEm4{oI5yx4i*5L77vV^Q#XnffYrM z6yjJ`7vgO;%V<+0KJ0n~5dK>JvZN0h-;zi2s=IMq2Bss8K`n}5(8%$1$E_|Glix3r zdYVPb5xiH{YjW4j+dy`pZ<9h`(kfu^(PC<5*GAyu1&cXE4YjClBudu!EmMS7J>N!S zt(dAfxg~VLZv4r>8@OYi?mI`JnMP5y&LpewwDFJer!2kOH$H)%OTOqg@g~}~5334_ zUgh^zIo(ggoMN}{$^2G&XX_aAcqGzC<-WG2MPu=y#%H&Vkch(5>>WT!b1v5O&0sTN z=0`tHuO*A_DsH8BWVTFrcK#!;pEzZz+bXLcDlwaraL3#0_F_&MFwz zz-}#(oW&^7O^`zN0&jw%)<0w{iw%ri2S>{Ay4e*R$a_=xlbX!J*ZU5UPnfPm3 z56-OxsZItQ@+${wG?MF0Fh8z`#e&AJhiTqUX<3L=KB95zjiTX=4fRc)oY8v3V2Ky$ z4E)yBG^(ZmbAb#i^((LMsgOqN+wRY)OnN&^{$w`@7E!p9x$B*mMSf%v7B?&9;5RSV zI3YcWWHkV$D68o)qry2{kf&C&_ni|x^r`0C8V6CoW?-YMYZKo z0K~JhW#DUoXFsxa%eY!`dL+hg=3F`cAp`fz9+y?HjKN-aPkmIp*b=()N$V6cV8}26 zq4TV<-L=@It5*vp6_8&g70BgTrTMQbiycEraol5l)y>d>^DhFN zJd9Pd%1=3qaARclEqU+xp*(}|A}mVkJgkgB`|Q$HaF$uqaZFH%jhv`4LG+eHYouc8 zf_eke%@R%LwDKtX!Niz%w6-KQimXYRxn=F8H~j>55{eKqcpo!QZGR+U%=zI^N4}^` zmQ#@?Yif!Y2;O$ouC;pc$%Fik7|hjbIG~xx_91eCb%#CWo0i-D;auZO zt#*Ylj$tkP_4P8P5V~%Y(GzoJgjVdrQ?O?vjh3)_ls7C#8Vz^fYvoEUg*uUuVb2Mf z0`4A8$nE6JV={jg`rgLTQz()FWryP^`4(j5b>S5@H!}1`x50Yjjo-c8O|{ias&7%= zuh*<A)m|^);GCW$5(SddN$FeoAR9NT9c{ zKp*w#hq16)O%h&qnoLJDTFNaRaig#NS7gqyEW=5I>) zF(D1W5#1=;@bIO}0QG4>r&?!sZadVMDa%2p^TM~FcXut`&MVBP7YO3K*$uCybyC8R zD|mF0Na}}39Z8zJRe|COol6^kX2e-Tlhq5^CW5mB0f0lGr3WZxQ!*td;rT`%W&od` zVGi9)M~Rc={Y7CpV9ExuyiIc&f8mS=4kIOAaGe-XdTaikEvQ;0 z*!yAZX2{^48E*B8sE6I%bLiZWq-qyzv=3KGUxA(y?VdtdRD|NI8_FtUv9{S+ecM}n z(hwuaqe$6@(FMl$T(9%q&Bgb>=4EHgU%0qcAQT+%@>e5+nLvm%)O?nL4Q~Zv#!$MG&cRRRyszwK zW@BA6xcL4tR2)+l_^P}5xKDrH-lzzGGa$7*KiAWEzI^^h4AY+(`iZJOGU!8^F?vpU zq%9}6M>vAHOFchRkcLL&Gu6|s(D+%^9v1cMV{8-1Dn`a94x*(4=B{m5-c(*;{D3nVIBg0V(XhUA;e)5x(?Sk#nd3@GmDnad_HvD?yXp zi^jgfiF0Jn`P9#`sPG8gaJ-b`DGFKWOS=zWMGUh-U=%sNhfkcT6IV)p<>SlS*0zDV zND8y?PviLGP%ciZh%}}UKgt2kw0Hq7T8eMcm;%GWP2&9|u8rC1Xzs5;<7y*E03ldS zqR9fZhu%}Ynb_7Gj7Nckwg`AEs@62se!no%ZbhH-r;T6GhB>{P0w}E0%o{qh#P1UB zM(hQ9|DfV~2G}+|vbJ&Geh-OeUWX&1x&1wpVfuLhl#)5Zr0^JOHRuu90uk*Ig zU%mlUH+TBzJMl14VeV%KC(VGrt&{#zuF)HUczSU&+oP+l;V~3DKg2QCi9%IBV;M*o zNKQ3m-v9kSU?T)@Ig0D%?4Rx*kQwr9j{?Xd8YK@hq%an7a-!E_`8Hq1M!}guvt~+% z`BPc3BnCq-t?XlmH=mjt(e#xJ7;;mHYS5;f( za;73V8ta~PsK&FTs+0RX;r;an0#GxuE-<`9OSVZfX6WBayYj^@DM@WygW%AMh53uZ zl6wU<4~X;MW!)StTn;>flZRMr9*tixw(_w1UdkiHZy13TC1}!R>rplL?|&A8BJMIq zfH3qzMwwYK9v;>HXI?0uY|Eh*JY<*LHL+xaam*93x?8^&e;F-Tlb(eU}xsEG=zF!o6@1-%j zjv)Bm#zyfFcyjTMq<$;R0aEk}TR)9?u*B^lo z<8DchIOmUt(gI4CH_uKwEEF@H_Zo8`wKtz8C3>51Thzvx(cQxmhYT5|)3o{2#l37m zTf*Cxj`IAQFOJ~!v__O^wTvWp#n7>Ck2=CL{%Hf2b>APFB6K}z@PRiFaLE@gxPEMK zKeNb~4#iolGjn;VSo_2a|9|l0&tF((Bk&iY>c68P>ihjHnPV#Go+j+lr56!~CfS%E zz>uiR)9Xn=m4|{PfsJ9}xWIX#PPqKIC#Z8U`}y5vRk8v>%5kJQtM9a$i@2DKXWJXO zWDd1VLK&|%sjA&lq-ZYq2S9=B z0o7}9ohnyb+gLrLoU1gTzRwA-Q36Cad+8bBc&t6{SJ-O3TLfV3aNPvmlE|!vW!Y$= zsj%z_;yBQV-g+2H$ARn*ntJ2Ni}-Z|;jl?>imekna@g^Bx`4LkN)9#K6LA5|ENcKl zwGZ@DHq|^vNep`3F=ySXTcGNt#+FY=&K{uVoKGAzqNWsn(gtTYK9ep_HH;L_z?p6S zRs&>WeaPJ?`7*nH>zi{r%g$bFDj}SxuOorMV1M4VW!>4c5dzc^LAh^^> zJH=hPD5jU6ixuciJ{24c=eN$cAGaMwq+X1kYa8cbt&UGLYQ6sGp_aoxa4D-bv!Nv# z2S!i{C15}6>o{{D-!E+)l8|Ev4jp! z)cq1p+4{%nPpjB_72(b-n(0(m5~(3{B|@dP@+8qh7|9d=*DNZ+higj?LoG|`SWoDpQ1mTw~yW!ClgI!CmkoI z#Lj4R8Fi%NVvfulm8FPYo4PAYNG@99xK|s%VsbjxT@b!=^{N)}^h~r-yro9&QHn~Y zg6IcC>+k)FvrEAmXl`EY3TQ1rC?t1NB4M5%)Lm{8E4I|rosTviAk4lRJkm?2Dg!!T zr49I+b*!I1cq6L9hjaYZm7&y7?q+RpdGrf%4Zk$+AIhQ0cTbx8B^-ag__HY!R9;QG zmIR7CjEM2R`4;?k8|_(lAJPN-wU7nB*9a(>6yxI+5B0!IVuo+>FlT?+Vb=K$-Oi+7Iz6_uG|~HiW+I(zP5H;tq)il zp(+jZ!XP)96FaLUNCfFqzyE4eAQ7c^EN)!sr}krF4y=n9GA+mY1BoofTRAna3rQvt z_1=|LQS|9%QWEcO5?6o%fD0)*&81cQ1RqlWj+zjk^pw?IMi>~NSc*Tln?o|yAT-Ar zm^W(GH9E8;w!;JE{+j*|@L!X$REn>9=gsRxZCln1>wkAlktgUU@rs{WlA?BuFEt~H zt_{>!jd(r)J=YA#XR^AN8^qvp^Wb4FU034lSGSa{0DSIyI5$!T;kJ@$?10A%1K|Bt z^P1h*g1owr5R8f>Y3ls2bI-OyfKs+Mt2%1uRKHnlW$CvAT;A-I3HnpGmn<;Vxm(@j zJIn5!S4bIdmt${2n&FSrOWrM)!pJ{>(QduG(#l7bE%x^~WOnK)JTl;~QehpG_o2LE zeT7f2NhP|ji=L99xvp=<(=R0W@3)ahB&~rHbbQ|u{&FxDp3*yI&c)_P!HGx-OIRa; z_0FC+#oGB^r$a(}O%TZpM_vMZnqTSPaG;>b33zzYXdlmMCz95SPi}3qzy4O_GJa2? z-rMz9_^?Ty^~;WFLEGO_gq`ML|tB=f?ttoYTw^Hv9Fe^T=Q-T_9nrPl> zs$s2;nzeOvc5#`qOjWz4_LFjdwB`~1+-KKsTCfu6vq}%Dqh(y!B z_xg3EupJn~9oD2o+U5b8+A0(X+kjVeL2L)sjsFG!qkGF*^@-PduJ%oT2SHY+!@Vnn z!C|it^xbyryDZx~{2jp+2Ck)VGQ=$HcUfBM&BEtH5z+M>?@1UkA5d%xKljYD*6pn< zH~9F~Cr5>)A0VI%KF2RW05d(&hK<`L-R*cAOOP;kWJcA z0wCht&F*htF5?ESltRXYdbSIxeeaMh56V3UU7bp(}Rus_?gz*qLqrS4wPaxiEd0xgYY!lbsBvjBM#s3>$=CD5adEk?N@wu_^8$s{RfY97>8KNI?xH5fw zZ$WfKP^(PGE`<4HAu36PQ}X2WGqOLIX5v5LTg8``PcYq z3v|$iC7Vu5+89!Zz293vf9A2kj`NylwMr#~jEwy(;(il$R_7_Qg~vF9lpCX3`fS4S zzDZ@N(YCagW0zCsudR+A;2^+zDd?jw<*q@`)+2e}BfwllW{^77#{{T%`aZIfCF1Vq zsB23FGE{PvetPIlFWL$hk}|$`%m5`3ai3V~M}8-5A)fTfq?=?%xC;+X(b^55%NEED zIemyNwD~Vls~`S~)8eCBWS1C6O+r;82lhDlgZj5TL{%aQRF; z@!Xl{QQlYpXcq*5Z;{XA^QAK^g}Z<7$4*xHv|lV)O*(}oTf1pq#c+nU97Js8?uu4b zMFB<-!spx*Zr;^g3hcnU%vJ$^6%UP-0V&|W=D<9*s-j__v7j0|yX^pIH$v;I>(K`BxD}m`mPQ%q%yzJBV z%{i0y{QG@lhi2-8@f4KQ)dsKZf?sXFEkW$L0Y!_x%J^3{9h&ax-W#|8CYOA4Y2X?W znGWg|rS_WRZPKv_Nic!#ZB)})`sbxm1l;ZG-^qoU@a>An-0@;FspvP}kve{9T8m^V z-96BmX;k~r)ZO)(i-b*5ym&=F<6Q6>#~@Qk6gkU)zcA&t|E;D9b6@)Ynr`TadyMBnx8P z8s!;>H61pdTNbS%g*kq6LYvqt~F zBtaS*5u11}A@uma(T$=Pf0(}!6z1;at>-l_$~~e2pf?}wQfKE!UC#uedit z$(*9T8@PIP5f3e5LaYMr#}){UdYh$^b0O(to^bgFIY!1l%>~GB7rsTAG)XfWy>IP| z+v;bAQX{r9smqOLIJUiL`%3un#RZ)XjYO0fJzFc0V zhZAuHMtJim2coQRqQ=->fk*J!51HAGf52bguD^EI>MPQ`xD5DR?35UQpD=# z9UNqg-|xJCw7V4`*1kWRUzFic1~JJ=Y4EnWu*NBTr`Qk5_fy77Y3rGWCnT9svVOVC zwwr#=afU1x(`GMNpSML&fr+2?x@@9|-8s(X#8W&wc5zMW!!#!JUa0+;PNSkm~+Pnsn zP(1xejJh^L*NdYWtW*ys>kLEu`LCC_o^6O3NJ=bbWIh0J~>%mJ%y-rK{Qa*-O?? zNa~o$KoRv`)$|cc%Xl;pu!tk5D_|UXPJrrSfpDGlL!E|xlf9EOL800SLJ=N%=6m7; z?kr9KywV91gDHSP7PaqfyuEno@#uYE3x!c|FTBU4@=8vPj0)hI`vCgs&IL)lfBso> z(O^Q}6f@VQ={Xfk+(f70H_wDX{y!R*mi#3 z`v-Dtm5dx7zsXFbbK^dGG%yu^`dcLmVn{v?dp<=f%6hj;R_1H4^pNtGUSfA`r8n7p zQme2EScjVGKG|A#>Gir8bI1WClw=ec(XFczSvF|?lJm#!vXASbGg4HacYOX}!3Ra| zC|HsJ(a%qb3U%KzfIjubo~j@KA8j1rB#ps1yCl7xEv{s2$5BxS8Dr2)^REVB0W-1U z^9K{Y<3!G64R`j#GXg+FrAscI;h%xRj3|#_)LU)I4N$H<1}$VH;o${u7C>+}wWoq{ zXOGj*4fA7l`Y1Is`Jl?qSt^IPdK~LXR|)Vy9`n+z773GyNwdaPYnxf(C^~PL*#=YZ<49w8Uq2q^1JDaFr zm8*Aoq)~_nydsHU?+mka@>9qN9w`X={t=zW@5Bw3Z+kIVRf75=S@;;ZRgx(vf-1iG zGu`S9x^?7mxQ&OIm=2_L`VFMe+yCWX7ubhZRW!RQ+z4*;)jpr>0Zy$Y56;U=+#GO9 z+zc5p@>AzAK2N-?Y@}T>b-Cd!;ATp{c1j(mMc;qYs$z^P_9Ykf8x(W2d%=^S&SGbZ zBew0vHjLVBFb(_aH}aX_f4qt3idcdY<8@5*-@yFu$gk19op{~NNW%&W0}ta3h} zU31?{Sb!$lo)43da{G|+P-|$1-5c&5cGeu@)}1P8uS1P5uyn?czjQ)mWVb2TM9NGq zRpOfC^(jyI*apd|QWLXqj>QpiVisRTTn`v@eJeB;H25WNa|pPdV6Juv6g+afYUZ(2 z7qlOJaTP(gF|0B^cnxAJ`O&lpG=n&j`JZKRE-w5U#PeymjWj6svP@OmwJ!{qD+pBr zh56n1MUXkGj&h2Oqf5~#xqGb)sMmFmMUS^#^~}1;!bJ#>+CRv|#rQ z&QI%p@?S7bw{!Q4=x$}MOMs1)<5>0=`XWl}X4Xek$7U*`G)t_8s*rgq`E+z+aGla3 zkDBh5(ckwKvQ#D(&mjEroz9yU!hS97=L>95^;D& zTZc+mqLy%C3=iS}%O=C$9h(+x2=SD^WxcL^;xCJO&eXK+U6DDWA7q16@sF&d%O>l* zlyYpw9O!9(a|JlPRCSwzC)iCFIFEe5bRUa|>DN7(~ z%~l$;dq`-7t()}l-qZQtuWgqfLiM06yV-*<(=~1uhjVs|`j=t-eikz?d+5Hc=u7%; ztv}^7;%7a1ncf_Q?GJc7%A1NCujTQLr_!lN*Qp;UHMmr(J-HRRz0IJ|cOgA7!oAE& z!L29Cj`FuZVqZGN-7PFE%B_K-F*#wb2kA=&2HROv!`fbct{aD95r$10pg|q=G&uX* zV+*Q$lNI45e)IiQW@p_d(~Z*H()5ix_T!e(WC`akcvcuR{J15?n5A&A@cb`X3S63o z>-_muX&1aUX_p^_V{dg!2EEEP6kl0Hu#fml6e6?zo6h9m>&DUDN|@!ARx`od4V?Jt z%^~)5I=~c0O8M@H+vd_p*tro~0_F4@Dng}VLo)}d@OwV|T@ zDg(ab;h5++Jd6oA95He`5>ts`5si2J)YpX49tPUb3IF^iVP%TN?#oYT90-)kQN?Vc{x%Un}w8Gbb`i&ZSY}0QN8Q}PXsrdx6h^}OhBE#y9+BQraPg;h!}g(DEo6(r6(W<3 zZ6BZ9U_B{%|Bzl@!r2M1p~fm{v{3k~fgbD)7T8i1&tNdSaIa^hdPwzQ@6@dPtjMov zJK=JUXLp|^y1D9%^TG-9TfHmK-n0x;Z9yGIH>09#z|veY^T3X)n^Tj- zY1>A79-eoJdxwS3>R*l_ty@?>Unly!$yh3G77U~jqE`jaWENsfZol1%$`}xm*^CNz z>$INU1*-mdOb!#yFG4+Z&e<4}NPx7+OSbX9ukkhesRXd}u${1etMF)abXK_u0*-#> zlCDkE)6Z=-QKvhI7ZG;WJ~gztsHhzpLnRWgRyHU&qGXpKU#f6&`dED7IPuwp&-nKi z$4rhIfgCQV0K*1dutH#5?~a-tQ+6OGgcOkI8%A2koelw-iXL@9(qjBviS!8#HX_PR zTqx%Kx~{;9+O;9*Kg9lfv^#}8leBIav2j^>FXAHozmi*Nl7lvh%M$g32P?gPit#wC ze~3C|LGR3jIpzu8FZInRb$Ela_z?YJqbBJzdQRc&7{;a1D_l=G#Yz75r&BIQakpZF zA^OSK6oJ}}TIP1HfxgJkmbAU$Q)VT^=c~1-+LHt66wjtXuBGiHxL_jw%lZx7E|yVX ztjGrt$7xG(x!|J;?&ztum6k!W>1Prn1!gmIQ)Ry}amlxB-y1@JCCb%G_MFbex*Lt$ ze@>huKLT8nAXb4C<$D%;1BGL@aM^odbo*+}-hW24j0;EXZIR#0sLC6-QDa3PDogth z&Wh`49mjGbJiJ-qOa@|2lJdgiJysM4zcU4bzeZ$Xj_}qU* zp5D(YS1s9HG@-W1mL9ny=B)H!nT30sQjO~m81y)caogz#Iytndx4A5e{Z?|pw1~Qb zQ_v>3wm$B93qJEL%OcBRLI8>e6eTf}Vyf$Y)Vu9-KRA1K%pVgA>>4*O>CsAO=@{Dw z$;9a0tp_dHlLOfWL-5h_%t34TQlySv%-M+OcHc^(K4gdLklQw`9Js>TPy$6-71>wE zZ>gQR5`Ig-&lJn+hHO74Gtafulwe*^FmN+#y3%e)*7Kyk8h^(~7o`cYZti3cH@3GN z`I)Iqq9#5OzW1#6nmIho-q>2Fo?3kk60pI5h`7lKNFXZk)6CKaf|jm-D(*yEsEmd; z&xB=NDGeMUT5puze2)5H?Un8ox>9&FKi0knQ+VP{e!-XF*6Y2{C3Ohybzg$;Z ze2g{v05=(Q5aD+vCTYzxcdh&KK~bhRPT!_2o6n8Ofz83Ym1cW$FPSK1?ROIMFcYNF z-;mvsJFWb^cswhKbxbT!NU$)h!NqUu(4rDza#tS5>0E8uR!Lh}TlvX+Wt!aYwwl6R zhr;?-i|L+KKv`IZYgw4^iRs1)AB}IP`9ZJPR9l8`>A@w*IXnDE!hNEVBnV(6nD01S z=Mef1l(u9ktW*4rs9Jxp#7ZOyp^#!R>uE%0a79-|dhTQ^YgFaA`FL^$yoPB891^6k z&+iC-%_u?@DPm2*f1vv34%JCiX|FGx)rTd~wjHwZpCjfZH36zYil0KoVNz1uCY-H>~5Ufn1c+%!}XJPKYx<8eo71TDU)(2b~NjN}HZ?`PoaL z7lRyLjT?S+#!n*K4!F?L8SSBFi5pnv181`o4SsUv!-nH`)1I1$Jm+WdX#fr> z*6Wbu%vg4{qF2EzJOx6hR9E;K`SgIizS@xZE#h+)MzskYI&4Nl6-+m$(7%=Nvs!Pn z`bzQRZ0RGIA2|B4B<}o72bJf@;xewQ_M!4+QlE+OLk%)(YPM2j=oh$qURt3+?Iqm^ zmFly|9}_F5wKtPr>lNWp&q1XmcrU-Dcm^M=6iyu3=6=x1>MZL!)N5`5`88dZv0@4{FJR=n`uX0}9Uy@W_vW zQ#rUN()Akqt6N8ksDG(PHq8J)Ow6?CJvnU6gYNLdG2P*Bs%Rar2d-Y?q_vI0?eL7i zBr$UDR+j5&(p2jK56$;Y>QMSz8sWhuBLEzPg#{=Xe(W_jOG8l@PQ1B{ArC1OAU^^F0;0T8cppv`Eqax zI!9?NZ7`vf%}xG*?Hg^fFt3No+EIVxn~1k|0&2kT?B`PPdwIWJ2wez{XuGqxvp07n z_%+H&vO7n-)RJklxo!#%=_Qx~ z!J$EtF>x}HB~{$oGfIk%#D@QumWPnK=A^DwV%XKnYlP41-%}0XpUuNE@an|!S`L>r zBfSxa`D=HFFY9{Jb4o%i8Y8U!pzXLbBmpvkCiNk>bsh5f{3iuF4;>>r7eSQP`A^n@ z4m*Y4WL|$&Gqfn-`&clmX~dta${aPDr;Bk3EZ`y-&-oGx5_pd;2F)APcI}9GPc|g4 zgn6Q+ZyTYJq+{ z_Wa>yZ%P3jG`*WeT`OUgx<@P0ZSW)!yWWmn%MkrFGMGc3GSII@QQ2g8jATpLZLU`1 zF5l(N1$8bNComjyB5f&di#aN=y=bd?9mAT|;x(;l(xMsaLCn%|dcy3Cru)TX$pJOh zzSm)KCfRV2q?SbG*E+JOOAcT&A&?dYIj*De3{H*(S+6%fmp8P*>+jRx1KpJIodt7jAJOvYO%!VzRYp z9shJpE(O(-3-mh7o~RI>w|S&TwoXv)n9L|cLTredLe?0UT=EQvb}eNmo@M)g?7d}Jl-n9Nj)at`w9+a_mvkf2NP~0;1|c9_ z4#Vgcm6jHe9J*T?0}znz?k?$ucMXI4?6c3_9`||Q5C0GU>+%E6JkNUW{N1tcXH9up zp4uvQEU|XhCCc8q^s0^t(hY8?Jstc+)5jvaT(*|0(qU;j!cC?~^TgIS+1JsN;-t1A zS)v$pUVZ1*ZogZc5iPOKE}IVa_7ORW@xbr1tiMOi*K0mMkkB*hlb4);OXS$P)y}Xp zAB`Wbc=BzH4UfD&i;R%xac20|hrZAV)}EESoMK+Ip?h6QR>-yW3 zQWU-0=ZoaB9fUjNQmN;1fQk6)cFE-Tj@@(bDmYuWU)RIn3D^^MP9M2C;YLrbwd$gj zuBEU_nWWmAuhh1XyzRQhyNDPY*_}2oRE|k*eOt(ucKCI}&4xazccZ?&lC2=Afh{(v z!Md2>>E4F}!R-XtKG!$-S5%Lf^)zfOp>G5Y8&r?A>KS%$^!#$)iw&`LF5HSuJ)S0D z&D$EAtKR3Ir=+6CsO#!Tf^ucK1y2fmC=IcH+6W0V)jGNIBEa>2lPTeNSiQ#BVYov} z$st;+<+0<}ibs%4kuRpp#b|V7Vgu@3cPC)?QPGh!@EGk3e zKJTp&D;G8ca#NjVbqw2$;0rTbonM1oS~4XTg`LH&@;c{C)NSm{o_xO2>o~sQBZSy` zek|E~i9CI0A%-`_27kDIGe?Y(Ge2{1p&jSMTn)mG#?sEwYaa95Q4%u2|EzJjZI#sg zcwac{N|r*8*wiz@P3yP&BNIINsRDAx!UiXg_!=G71+K(jp)9DIq2WE+pDpb=Nd3(G zfZVM2an{R*#;?L9f{RhkB#tXeAv-sx4_FQajy>LR%|>(&J!Yl+;5!_XGT&4%@a*<7 zYj0|o`-(80?Me1BO29#Y;N0Cb%Y=@hGSg zgp7z2FKKaPUQ@J>Wudk+)+OSsK>M+ygxHssLF`L^`1t#Lia61^7zeha5`v7+6k{i^ zFN%)Jzq(k09>SMU@pa!|PxVc)$4G)zSkiPUj%G~0J}ZkBZWD1qT#!6TZCs(0o^o;8 zGz#W-dd#$~HqgVD6OoeB6APVFeJATG1C{7nmmn{?Iot=AO4esmo##vm@$=o{4?PhJ zySY^j=P7M*+M$?>c~X|&z(owFrvoxUoymtAH`rug>G z(by>xib)0DFG28)?)9I1!)Vycm@$21Q||iJmfnLoI!1q1U|KzCHeUKj;~aOB(Y0p@ zQDoIJ8Y7}LTbC4eCA-)3?`gR9zjC~tAjpBwxM!P%XLk#W>wwD|`r!$@x!!-ZIa7g5 zY0-+UTA!g&xluz^Dq``aRSO?0yY@DABb}~}-fTsW#BvTL4>OYhP0X}WikRxij9b#n zCW_A*ju(1g&L0qk)$lLNpHxT@Tkn+UToi1Rw>q(3FK%p5*cw4=8kMlB&?#p;VsA5P5YKf;#-&BMaecE!JPq}4f zpR$7zaQ~v6P+Q%R*YGhRxxG%rL_IEd*VQVFhbzjxdSL23oa)1z-ul&clx;&h`hZfYdJQF@zH{g5 zF8ZwclS=2r7>QRtg`3@%ZB7afn+oynhMo7oe!uB*ug>WB!(Ptnnpf4^L4PLQ?N^s} zKM!+M?^(=TK5T=igYm>^UrpIXnF>33Z}M%i(`WGANE%ua83$>sb3pORkKe6;68 zi1ATF8nXrAn!Xk|7_#V#e|2+i6)DV|-lGI*PB zTJ_U@3$}X+^nICFf$iPojQnHSY;_7MlCN z{!aX3;{_ZO5+{IY%28zh+lPNRKIH7RTjN&zvEYQ!iQoiiHg=d0pNxHqy_1@#PKv~A zoIBIv-eOax=jL6T8wHckbHt5v@W%*40@kq`TZ|$w?#+sLIEQLEK)>pi% zgyIXN-b?J{4w4kbL?3kE zVT)4yr3NNu8scHaQ5A6u54&sVmu2r5A8p(wt5^tGf83X#M8@ylq4+Y4ZQ6V@`-Z;% z40xgWu?~+=_OZ5yRt{0BOK0AdZuw56m`A|Sd6>o)xvt~q`jP+%hBapy4LS(scJF3=voFMXuUT?gy8cc@4K^X{JFuA%44yf@qMliZ*8u#rX! z9j<7i!a7$T_ocVyRWdEFG;DPeP>OIac#YbIWl2?9n%A~EzQjF!BlAmrA6qW6KXv2z&O+5FJ^$$H?q)H9I6yiX+IaRAmKj zXz{D$sxA1{gCJrIqvr{6G7nY_ks~E#PN;Ljda2~OMJgrEiJ|WwL|4-rEnc%Bw-+1M zA;0!Sto0)2GgKf(Sfy;7nr@Pba(mI3yhhwho0}%}?CzXw&}X;uLhEs#Os`cDZcsEI zdieWSjtx!r(s_FL`=BC(`fvYosS#QdgA+VJ|4-fg;ghew)tnB-KNz0X!o)M6fKTQ) zstm^8aTpUwSYEbb5*vZP8qMxFoaHK9VPNSt08`q@$6C72JjTFYsqtfM4YSE(94nDg z`7Cw{coy^!t@#=Mq>iZCr|1ZHkwU%mJ(k~kP+&M&Bhe|-SymSDww=&!oe2daGLl(pL#*2nm9!i1OF~HRS?OzSmO%x2c zNHqUtyg#7Lzv~{GftjWL0-zx~}Iv}l2Ti$C6_>Kxxkb))2KvU!t&^DCh5z_r$ z9``r2AIk+|!x%8&pGvQi2|+G2%DfBoqW?D=`SBpX%ix@p8=GS!KNX8HHDRjCd_A7q z2i1hE`V)!4P^$@Osy@hb%;S5){Z$T{C>`Kg52@?%6JFF5$i_*KU%F>j0J5TKtMsR( zRss&c9f$Y&s|qzP7DSA_COp%P@Bw;Eh}qUh!O&w@pZXibE29)}qr)htMoJOsxe?)m zhJY(b5dNd&JZn6z0aYFasRa`Ll;;0b?%#jn1OuhL(>cNCKX;fjc>9A3{p}~PBMFdS z9?&JxoZ{}kaquVG{N?{Hody$B2XyQoTK=KMzf|q7e0cB@FutuIe_QFNfc`5S|G1_P z0|BoXb6CF*RQ$c%fA^lB|FMmW@Rsn9=h45y(=T0zeu_iv3!5lBuNcSH97oqE_2j2o%p*!_iX{cqgYumKS1U=x=K{3X%< ziqYm8AsDZQ6HWcU^GE|46b!h5r$5c_>e;4(HRs5rt$yaQ5UG&L%294xNcbW$klZqui$b8ucE&KF0`)Tm{-unIgkJbMtXvn7pG_sg~Vn0*TZX%$uc4wXY*vby2G?bfZ2o#Msn zzbrn=2e{h1f9w|gEzbWyy+2*h7mM&Ru}(a-v+!R-b;DRiTyW?%Wnt|JeorX1XRX@~=bc_)`iTvgW5Cy)H@X}jG10hWm&f|g z4$Py3&bLDc;oaapvMoHZ62r0$W8Z=8GKtnicZWiEI9c_?)73bu@hCwXQQ&|KIMz&Tu&VvIxSy%zl4arU9)I)54s-Wq;+*`TM%5QV z@}-g)qt*l>qtc~`GS59MZo?~wbui$!nyb2{xWwE$oO0<>8+LHJwkvkNRSvLq&eRFA z8jz=SYuz+o8IC@%$T2*f{!fZrPT$D%uWpLoCH2c{RLc;gX z;m9gV;^q8~AZLR4bu%{pa|PwHjV=3Yx>p3_$ex}C=hjGUHswRS02MH1Z-i85kUl9&Km6^!AQbbWO49~s zzNlf+M?UXDL+4;o4~p%{TbeOc51O=y(}kS_b&X!DS=#^Rd!p(3;J|#j9Idr}_R{P@ zV<*mR5{ZTRoBNm9QLBEJ)Ia_M2DyXCd~T-EFq|RL(nC~pqQKX`bzGV@TREnfj@hX% zkl4v4GGC$AdGk8d*uPRpt~Q(Z1arl4y8S#WWKAftw0KH~#BOxGcAR3XN+(XDw>nAR zT~ASi^)p$_kW6LyuI=p$iqZTN8?5{J>qo5j#3LQV$VyvqWOHb> z%=!=Ag6gchrusZOZcl=fD`b5WG+Enj4VQ7>bv}Mq+QD0Nm@XT&Snx&rXuI>lC9K@?7Vu(R2zO)A6IK{q3)5wq))b zSd4pN*5AlBWMv+U;T^hp;>bYuGQN$D#-zB0O~zAQoK4v{8An@KwBW11eL}VKzlYY` z0AT&*)bv!6m`JpORo;nn#gRoEb~ja06yX$IetRQju*giyY`XJ0o{|01vRMvZ$xY}J z;y`egRZ7*RBOKn!!lzJQf2>-@J8@Mq@&Y%ZCS8z@lQjZAhnpowg zPYzMoH)|fH9TrN|5`NC!y576d=5z5LMO4aA?31>d#gymXg7;SjT07VM&UGwb4TT!l z#yd;19yWAxGe4g;+U!a`yshbBOqb(qA#a#PpJKj}#kwFMvT_369<_bsH8UzEt!KHR z!M~`JcFQA^su&D17lA=$wL_dOjQ{9Y`~$yK1_wrV#3)f={g_uccquN30ry*c;jlm` zW6Pk-SM@I0DgNuVqvkx+j_71hjWRJg5mIaOI9+ zd8*<}toGWZNB4>6>W;82l;fUh5IeXBdl{`J5^F68H?u0MEA`gdf-^CMhvDtL`(_QU zM+;b4&%iGPzI}YOaSk+k`Zmpz8p;(M4g=TWlEE_WU#ahG6&8ueEWgs5Yi}jre0rzg zb=PUF=@9tv>SLY5be%@4IiuMtARIG4;9)*0RFlcsr7Zt(7J$2=z~Zp<{;Endz<~{+-_4tVIZ7`*oghF2?*d={2#jFwhfW{b1b=F3r1k!5y1 z=c~2jkHaCWnl8$`X1N<7mQrahR`0w#t`g>~GvPT9zno4SQQ&f@n!QpatQGJD%s)c} zB@+1{@^?VX_pn=~CP*y;H7GPz^bsX}o!Pqn$9hl=JI~H_(Y3A*uVbc zse*r#MEo7}x_E))c>b#VT}h0ykmwZPsX?TMBMaJ|L`in}9`+IEb6j{m?Ph2C%2btn zo^SHSdBPdXqN}UGnHOe_9FYO{OH^@WK{}+``%W&llcG77pfNAKn`U&9HatLQdhL%@f7uBg7;ug*dcWZ-0tl59THoCW*g%~+G5gS_S1 z?elsrg44P#f>6jia4;C(1 zfBW|J+mdIAW8VQfJV7M{bSkN}%*TT{BP&-+j@9E{ViG!h$#a;L*4`&q$!;eKl)l^J zqFreBH5hO40Xboy_stc`+-?|4^o%ER1*+jZh+ zd6_pW%+&IMvl0as^YwQq#af!by13IZl{JQdxVZ4N_zU8&N+UxgJ z$SU`_u5pp)#%_PLOwQM%ctXSI`Du>4{}_bTqq0noLLKR)4C!8p-d$r3**e-!cmCH381~o)xLHirrsR_!WkWcY?86J>*X*c=<4gGlO+-TxINvA0JbE{O zy_xle^1qG*{Y_WfoNl2t$1_BXOM>!*L$1JanqZm@`tVJ1xo%>`*O_5nJ@5VMmG;%+ zPJUJ^^h%9;J|z7XZ?i8v(3rq4Q+vWQhpLBHwz?}IsbDG9VXC1U1CQ~MjzoH7W9QpD zpG@oTtn$U92E|s+$^~V(V9*-eW*l>uco*+UMDrT+e@?SnIY5s}+GV2lRKFE+Ln+WJHc@krNlq+Em6}el&Ed}4`4Kp7L zk0fYIbk~@piqCRudl^7tHqm?|hGtrmY>uhp^M3FUI0{y1-hCA!r#H&pzFVuRyVt+d z=G@=tZD0x&klhQvqsAV8tM&9O$a&cr+^_q$%*JI)Jkc>|EhBb+G%@~ht%HJ&3aVft z@#p`NY6G^c)HOFp-+Q8qcnph~hKwp}$%)tR5UB)Vqq7|0X^N+pX9$nLr-g74Z!Kxz z%eeL5CN%n{GX7A}^_7^ny13f$6gz9%CU!xG{{{M{a-6JZf|8$mpWz5fSAFwlrSKCX z@xMrFzL#;$&P2CNNJ%7|Wbt`X$L26yBei!>fkc#LQ{ng6&)+(|%A;(cO}f#e@c%#hhd4_KI*zc77H0uJbmX5Vj8t<=i#T zD^j;ZXmHrz0~S^oq`6sRs70(67#cNrZQfblS+z!*3$5v5=a`8z(1}%Om{1!NWG|y) zVe1hP+<@dx#Zi!(n~y z9NbpDG!SVVeP!adDrSS{;19Iog@RF_l*RuvnvWmD+OuPe?o#Dw?8LZzbZ zmkwE3{jq4?uPsm89v0{oKNHI@bkm{OFDodC7Mb$ZKAxUTA^J`rKJY0`@*l7sk?x7o zp@A{1jJ$^ywH6lg7>6dQmlVSyh$JjJ6y0rv<5nBsIpQ4rcxvYU?Sw4F`UT5YMW_C~ zkxnCJZqJaR;^Sogwa}D0gU?hP^Y7gRg%T2NrN5a`_4M(2XXI2e%pYhKFwazuf4W`0 zB&m8W4+15)SGJNCj7gDC+jIEHHGj2By%G}l<4jOS$QBmUp|4v)4RPx?CW?=0; zXtMN_wh|YXKXY5Wf`hbrCiBjuZ@j1G`cY6PyArCQPyGkGXDRCh;$^F4HeY5>79;0m zCrs)qwkmBW+HXK7C&>dce#zy}U_nR?c)rYnJMSq}q>{lV;@Y?{1A|~W22=_GBCNP| zk`$LokEY0N`l!=Q&!}L_W?9&jasUe%xSQ~;L_{8joF)4j$gb-hYX4I zua-S&m><3CbY7QRgYuw2tUrG4JF5^8o&onZoBEwmX?noBqmxtl8 z__(i@Q5{D$Nx#@WHrUrd`+N-M(iBV@Zf_*yIw=mF%z1yXl(H>fJ=!m_ow!}g75EB8 zZT$FQ-jv_tOXPB2v>~iz;xB(Wa9ySeyaVuzLdlyAA0b^>BO$|=q*Z+a(!-hg7hLn>k{)6w6CjGhRs&~YKIU4L=eWE?JWycXJdMd|`RL}O@8ENA1n5}01RlGzr zVX62yt8J*_v#sBfY}>tA71>3PCO+&c`jkYwcy+X0lx=<5pPHjAdXmq-)%P;7CBV_7 zmM*0hJN7D7GnwV9vEpC82^OKnE!%BP<<+|dp$4{fWbKjU?A}SMdkbMX z-Ybeu;%+BF{5xeSRaBOkvE6*^J~q!5$!+HyZ#d2LcbKR}k}8aPSVTvozanWF$*=DC zuCP*mSjfL}n1Uvh=ww3X@leeWsvf=BsQ&7uWsJZOSH;Uu`v(&P%we@jS`cb$7d z?~CV~%GvybM{VsWb_VD;ZThTlCuWnonr!9sZ#sT<8fCPjbY1J*@;@-?pAN6(V=Lue zadTkqrA?Wo3|g`d*)*RY2bOk^r*2EmeRMa)equw*kB3iTGCwv}s}N1CN1`?I!^|LQ zEr!wVNJP0fojY&&d?07WpbHF0^nI^p~o{x>w<| zA|mZW(VLKw!-$g5nl3I8*QpgDdg$5!|JL`sVkWZkB#DmLqJ~)6eXTk7GUa71laZlP zPSd*L=K_g?%nq6U-Itssm2V}_)V&USpfuA;C1bRYoA+pILA$)_M&xXZPgo0_-H){) zxOl5{9g~Lr57IOg*aFdI@$?o~9;P!1hG7U=dlLqT@~pTG7>^9Hdp}$i$PAP6 zeg6;06gS2J$gEyt&wQp!ngCQj$n+QfErmO4iLW=N$zZ3%PwH=Bwx2ulmlNQT{s$@MALx%O1BFGprKq3RKhg2Oo+U1a zfgUK|n8x`}yyOp%)J;d@sbR2T&izxZ{Jo&%nZW&ON`9wd^$+X*;&Q2Mz>F3ml@tGw z@%>AFDr(^VAUg}+|5195*jO~0z0b(-{yFIgB49x{STvy_>ZZTQ?!O2}npnDmI7(XC zOsCmDiPL|0R)_|;A0AD1GR6!K zHg_ij>g)W-Egz3bBR~ExjL^)YcBUu?ymuaP%?+236^LaSMuLC^yM0kHLvAw&Sr?0zmdw`jYjBI_#3i3;!q0)_dbab>@_Ct41{x6|;GUu1sW4#+#8+^rvvdj`J)KS5v|=n@%2(LB*!T6nu!F=Ojfk3o zB1_ZHUU+YhOGGwinh7P+bN4z8PSqMn51?MS*{kZ*TVXf^fQJSuf{n${f}dD z7Ams|eg%Y5@nE6MS{E-mlnguhIPlSy({$HWkkcV`Qv`IACRK!F%|e_8j~eg71<8G{ z5I=Ihy5|_U32A9*=cTP*|B|IKDa2J*i&43Z!9E%U&a}KI-`d)ekPYQ|>85bXvE4Nq z&k!H;He`;gqJwIV!g%!RO?QTtjFFL1-%)D*jIzzIBv2L#G{YEa!02XLW2y1*@K9xo zc#foz50SqP^g|%+hm_j@zU*h2JLlk-)X$zi9Rt^Ko&1u;skkDD6Cq*I4#WstSXf}P zp>ey5RNE2<&c>-=#NEQ^_}*SqQ!{o^_L9hE+ZYXcAd@}YR^%^4mN;H?FH~9g zrM!A$l(ed<5jNQ(&Mj%V)5pl)1Z>=r$3qtI1DCJh&;V)HrfX)L$7J=?Ah)*(`4ydn zGI9`!4?uQ?6M?i4lu?~gwj_(-B5k!F$ptFNtC*g4IQ&XI6PFAwws70Wi#g>n1hmQ1 zXno0$IZe?GK(~;+75rDI>jnm)LAhZ@bb7I|3`j4QWg)G|B%7E4dTcs z#{fdp9D~o_`jwRwLB-v@xk)a3dNIQrL^6McN8;4tEM$Nlon{7Pf5ijS?|_R1XWAJ~ z*{Sgdb?Cs`{TRj5m6%k(m#FlL;lHwyg)X=_Ic0PV$&MT-5ad6l;-7+`iV$G62>e3S zuLKDdN^o&EXMptWQzD$;pt0@WwetkI5)6Dv%x_+HH|_#z6y|x8xDvke`v;~QGq6;J z6D=d^Ek#Au(LQZ2dW)c+c`mF0YjbClcF_&;HhZnMt<2E$4mUTCavln9zvfK`ELB7^H3a4Zkn`uWQzHF3L1PO>JHExY4>NVtaAOB<)Te9(m%=Sk~f2z(uMN}Bs<-YGVXnDuS4hE~R2L`Jj z=d_EM_Hkb>-&(ILE{@1MSmCq}z);RwT&{eYTVX)8at$3TNaDiL>t=yqJ!=2NnyfR7dCh5BS%NCT51guFRoMx#b~?QP?c$Rz1mzh7g{cXejA3W zeiIq1newwJCxYZ4-RDmPo_S*&33|+joe@CbP9CpLuby)&b+KNbSw*qp@-pXa{{h{i zTf@Le0-EXehq|UTtl=nk+Z)dw|eia_ue7B!&pO0GhQc+9HQCHcBJ!yS* ztGh=R4|hoB36Ljf-J!}R*>>TaE|mHZAfIjw#1x5q!Iv+KN1{jSVLf>alc*0~`>x1W z`?<0?HD+|Fz6<+Z?l~Y_of({dKgtVZ?%A==XR&x<>K5%Z1 zO3pYMHDG8zN_(rx=mMLH??%|iI8{>uUW-f{*<*O@R_g~W&XMBS`7-X;UExRb0aXv2 zlh?e09({|YE~h>Y>YIFhBf)Vy`mtNc>dzo`N_y-~=uX|8>Rs2&vbFH`-fo_3kEHm7 zk35xLtd{QcrOUSPad;K%fe5?7;G%Nx?mi}(!lbLyhVNCtm5*saI0b&e_K>A;$p>&=K$9-J9(JN27 zLVoj$B#YTEs3f6_$um_pYK!_U&@C9l=VL2eIL35`l#3SeFCI|f!cI66d#3@t^7?#G}n*`-r6VJHB>?;VUs8R&+b+Ld;tdV9fTETy+@ zW{P^J#F6{v{lu`3HL8gzMq+X=#W zcZ`0*f`xWmPPx!Z^~}PDlayY`9YWeunE^6+urgvPkN8{=hf=?{N#AMRj~lOwjz zXXAckH_%f4#ZZQt5I4Y8s*BFl@iI*=1mXDs1u4k9)`A4U!c0F8FEY5C2M%{D&LXG= z9bNEVj(4b78B<3|qSd0=T@rd-vd-E!bwibSR(eMxJ3#~o507`O zw5oiN>$>j?5Kl9&H6vUaa?1nr9O^Wpcaf9RgVY2oBK!HoLVg3OiFlw1Y)&32V^q}l zmOWo8wT?NcKCGk+R%8_AnLo8nNb?>g3Og3*dhhji-!0LGyj-k#z`nK5dERpAbz!rS zFZ)lNLY^^KK&QHFwvn3F(p}4%%O#{rHkStyZybtg(-DsAM8D`}&R}jS1AJg@>4AYw ztQK{7kwP@RcpN$~9y;+ZB&ej?0tu6yoVy}=N^(RhNG>F6^_9W-&1en&V$j`V;bAU{G>eX4B>aHwkIZNoX-Q=KyVM42fSB^)f+l#E1Fud!yT{bsSYeT3s zF!(O9Y18@;&nMLO_=_01X|U{X@JTj()6pyc;(BUvf7A0KB+)LP$ktvH3$HJ8KjxUJ z+-guj2f0Hr*|YdD*+5ux6y_aSgN8E0{ZVRP$E9a&_UC)I9daOh`>wwm{^=VV9# zNpGXHI3RsFEia4p;;v7%Z>5pP-8DappPBYehBhsRF9s(NogwHlkLid`IC{W9=a_1K z@hk(0?8)E@?%%q)I|)te1Fvt3IGI!IhSy8c`d>svPiCX>{y`#uA#)n@skc?OD*`qimP?Rt z%+el7N`G(7;>LGjFj|I^}K%$5^Z7az+Ms+qpxvjB7J2$>V!C|9;<3u24=$4GEd&gOLrTd=fC%giUc$Ed$C!JDCE@O{+?bAhTNH*>VQ#?#cGqu z8v(uq=68EG2BP#5O5^W$IV<@Z#kKYFU6GVWle-b1&Xa33G~Qvt)4uax?8qPNSRF7!s-dx)_cNW!nsl5 zKoBt#=yeLOxE40QC-)iM?#emf`bWU zjo2cp9z4yQ94m>r>`r~;omXY&j{>s-Kx~CpuXW~Q9}tw+PF+8v9Dx+FpXgy}*bbR) z4Aw^jwRz*m2QoZe9T6*TT?AWlSPiOfu$8I@d~k_tyaDZ-FdJb};34x@M)`k|W~%t0 zqRC;+6a%#F&atN8Ijr0Fisdg(wWUt!X8QVSn@$PA)0z+cKk- zzq&AgU8+p`Et{^d@V4-Js`g;-zRzE+fSR#QJoE)%rg($^HsA7d3o`?0;D11 zU!3q6;6B6+*0S@XHlG9L%SD(Czptp9%uwNa#oj}9aF9Izshm!=gNYeJ4Tg@la{S01 zs^3{x`$+q&Yb@z`;*;X~H&rm$$IEK_0_!dx%az{jy{a^uGIvBp_X1MH`>b*XQa2Hx zz{tyY)*AzJ=uAfNVNh8ly&NR<6yuJX&C!DSxR5Q zy6v{tenUBfkmh?KP;4yfS>V__gx&zhR0th(un zuCive5A|s~7(T4Wh96KT`4EfTAYonz;_Ii}j;RhXh$(lecWep%Nov!`mOJ4dUQd0z zbC4L*34#i6_{0{Wyh(K4txZWSo7U8e6uWhkrlY3e{(`F@kHJTk^}s+fTB89ppUgDp zacWm8rU0XOc*nhj9>SuImvIplAW4zBkrpWz=sx6C#m`m9i=y*x6D5?FIBPtiCN&YD|FY4A~K~Z4FKQEHkfw5k*c$-=3gX z94;Q8!!C#eJjJ<&{P-63wMwThS9+|jeDHME)~Y;YT<-vw8Cs{xjP#O605UBerQYyAz9HHCp z7fu;YH35=iiqXG=-0+wN5tHdvI&X&JSdULN!KI^#S*g#6{8Y-@A@7r_0IES{r5sk8 zO4qp8%#6$0fpUPr!HS-P;ZLT`;_}f~DlK#lkA=cX9NoZAAS^xW(qMn4fgZdeuMC9} zNGyRy)y%$C>YY&vUNe~;Ap+7eAP^)hB z7q+W;_r%G?>&ul0!=Qa^kYuL4~XB zmv_|uJ^cmD_9||Qzf>F<;i8gLxzL22bnbBm9^Tn*Qw=A2R8%BWh8C3QhR#cZ(>~m* z#~lYTggl{e7>e?4N&87VeM;GQqeh(Nq-ziK{0_VY`6`p>bjcAnX%vfV+1NZ;U?jpb z8G&Ke`i!2y66}E-fm@`7JsQ+T%Fz`#YU5^l&RZ@QMCvO#a;(uNo1V-D_q$l_^>sR^99j*QB4_$ib||Z zE1ip$cf^99gLA*;KI}V)(5m}rV6cAsT4Gt@zI$hDtNic;QLMBZ7b{e=YQ%ZI*l2l^EX2Up~tOJg*Qoc{WR&E2$m^<#Fb>SAlrV&WjqZvNOh<6Q7 z?9pl^hrjS*^v?&P>}YaVE;-k?vhSuO#BlR)f644A24e1}4?O0(xF}PhpPOAJYUtKd z53&k(jZsg(*MEL7K_3Wf(!>rQ$u|e+xO3Bv7xZB83O|MFLr3Vnudt{Q*=j(N%5(UD z!AVW(?|N(1*^eCd{9K(vN;C$ORV$O!e*T)59==b_$nfcx+fBWOoxq=N6lW7B*5gcE zt^YLTXglCW%dYX(r4cOb3xt=&$-Lsv#@p`715j3bVvX!I7a@Ah<@PpHwWv)e^TBIh zDt8{xF9vli6+8J5y_2;f>OfH=lO*$?a9bFywPttuh%l4Cw>BZ!v69jKN1$5mmQs#w z?;tx(^)%A;;>!74OO2J+nlN_Umt$)a#GrQ^FAdFM{Ss|@=qptd(212*qr7QJwaAeU zf-FBpfCCnH#@=MN%u!$YdVY}C*}7tBa#qT8J?iOCymg5mCS!f1ICNMGWnB>0Q&+!h zS?sU=o7#;~e+g%z{e5rggP_h<-_iqiCczYmI9-5o>5h;5d`=Dci5RfJ<;Tl%$Oy{> z+#}tD49^4yb#YbiD&Mp9iUg95F;y~gyRKw$8z}V2+zJ!fp&`4wtMJa3YQ%znU)SXx zq4UyP@{1jP)ybRBtIvOh!1tx%?aXWyJ=A&VYG9TAQ<^;r!5;=3pRSCmiP&50?zDV* zN{?|lqGlp}bP2iN*?j|iDPnI<8%(%cD+U$p$<}^5tPe6L`yeUrvZ+qha@6-=P_=(h z9sPSk=Ll&z#o^G;ZEsL!tfh(V4d~&e&)qN39ZX1zj|WgZNrDECwPnW?T@nndb%h& zBcps;(z=ps>Ljj}9k}_NKUudIyV~hIm^BxT1G51BkzRb2hw2oga*u${i>piNk=q>J zh<@hWeCHk`)~59UQSYL(TV4&o8#a)-f?2ipp>L;8jPa*> zRxyMbd91{mHotd%bjciSF9=vI>lnlrV|1LXjK7(hevZ~K+s1j)_IUzhRn#$cAy`}! z0d3QTWJIS}(w7a8WiWK`^QjYxPlL)x^O%C{;}p8b&azD{MC$}TH}ifVMG9hI&@K=< zK+@M2T)~><+%(uh7F04vxsisO@NnkafETCR-$AlxjS~Stcd-Bf^PveT&36r%`Tz{K z-8Pm*!00fx`L1o>Xe>!No5u(O4$=Xlq@^c!8lRF>a}n$(4_sTKLT-&RX@M{8W|dm$ z*}sE0R_924Vy24L8DuP{r*7XaK#V%C*U?dulmykfNa;?!;myCxN`FBU@{<9Q5Y@4P z0x^mxDhRE+gaUzZmf5A>pD zKiCB`+Q%!Ol^IX7?Y|CaA=<{^H^se$*el_w-Jl`^+41EojDcPO9hW=4Pr5?w)mXIc zrcQ;12(=c;9pYy~12#Btf0GJ1%E6=#zPvT7m;;3NMP%p8x-v9B6}E|IJ|ERVfnIzw zaOHWI7)KzavR)zV%Z9R`KnC+qFfZhuF%5RGKBP`edc1+Yz-urv(gcKUARZ32xI~rL z5=J&oZu#Oxt8xkelHWmVN2g99nVu}emjii?zm;pYxP26N9oyg#%xy!yben=VOU zxig`Pja2~4mo>0Tg9B1J_T6-C@K|tLc;O*J_^CJmgwrFWUmzpwEP(_`YUZePg9_t+5wRgbW*PyO}E$apzTv$E4|CPVs> zfyx%bR9ZlRd?YIbIn%KRvXsWziaFv_^1fyP)U!^Ju5YCI9+l%EaPMfHRHw;FtdW7J z?ixoAo2*RiNm@j4Q}*PCLAsxW*-7FLtC6u^R*y$Z@|zYD*qkhE0tCycAKt{#Bb^(K zUV7f8Q-RxYjlP=83A(0G>=A5(r4a`njiD6DhO8B%fr_V}NH`$dR8t0En|zfE4oxEH z+w4oUUDlyyvS?Y6k)mLlysSOi{chy*cp@=D)s~hm}Q&wqOft|U=P@WCM z2_Pgl4xdxgLH9U$uP`~15%$oE??+G+Hx@=cZJcpBaC4V@$q;kC&&|zu>HRu#Na@t6 z{+x7jV8CA4lnPkybySlU<*FYPrj{J2zJ1eJ40^obIHGSsrn$Q|0TmVjn{{IdA7w&J zfFz_S&mt|K4a9ppPCUW09aR|wu+7LT)3fIa?A1?GSFTbgl-M_kaVXoEzv-z$e z%O1NvlY!0^%)sGO9T1ild;18vnJr`p9HG7yMsCwd%BK6M`801*12L4zab`2>R0mT)RE%Qw@$|^N={k@jlS!n5 zoi}Rr?kuj!vUhp@y@AL`Q(b#|1SsLd9Ci{NNa5r4_>Z4)I1SjG{5)F++Zesh)r33x zd!LuGlfAqmx(C9#iLvf#7(=ukZ{l5eP(pduZF&U!Hk1VzT|+_2|Do)wqpEthwG}}S z6p@rh0TB=o6p%)c?v_SE8l*cmiUHCkjdZ7U8nkqGNjGe|zIU({t`Wi~)PF z_FOZc`ONvQwT^H)a^Q3apHMH(GT=d=iCG~fDFpZSGJ~;AaA`8Dqr@(D*$q{dSq~~bL}a9grJgSn7_3AEwWiV;f4vBiz3Rm~D%d+!;|yt(m|Q(GhL;S6cMrl_W!v3XJZ~DF$OfY~D1w zKt{!641B@H&mKn(1tWKuZf73;WoGorv(Ef%k8kEHac;+&r~@w;9g-lY(goH$f9Upg zva?{loA1Mwv#l}+@!~=4N`XS!48nNqz98OXyMW$)Bzml(QNsWId_3I$@4@H}%Njr0>#8(vm1cdUJ_5~Hj&GC&*C``| z=Cn(0p}i)yWI5Lg?iAm$gpN$71oS0uC|!%tmpi1dM+D9Y&`$>~CfrRGqoJXFwTE|x zCyR4tv65?*UUrKr)WIKgqW2bRO9F(#K zeMIQ;VplmWx`xLojqdl8J;NNy|8d$(q-9R_`noyW=IM-?`t_s6PY#_nZ!>w%ul2vP zEJEalI~$c}dpS^<(pO1^Xw|sa{29n`lJ@*dV)g<=oVy-l@r~fY3>D{Qr3GdU>PhWE zm{kQbYZPWIqLDd(HmT1@7N>q8SM=xjW_29%ceNoC`O1Q_C%?XKKxOcfz-X1rjOAk?CWa-M807d{oO*A)DEZFmD<&~#`O)hC867D2<6 zgOu9RXS;x)Gn)XGls}ypy-7o}F}X!NU6Q96Gd9}Wi3K{NLI57+(QrZbu%oh+Am)6@ z_#8{b?bnQ;Qv4rd^D^k1Os*F!2z=qebqn z6&&z?p6)nn$V;44WEhd?Wd3IenW;4ii$8h1w%;&NWaV6-Bm7xb7q+7r+r|Dcu=wzs zLp)IjCp;g-QbISB+szx`5s1Vu0}{W7O`-@w#sSE#ab&U4UUhxs+&mTixov8q4t(MS z^d!jQ4Edic>79>`ie74Ol4>K_aFxr?b2?6*an$z&h)`3<4;1QTGCI2Gv3 zeFl3!QlPEv8wp`8+hfjq|9Zk&0BQt`!FPwT=^Y|=vA1Y|$JcUnQJ&|@KvQ664Vi#; zDfxu$&BmFioAIi)dsW6gzDsn-;?z2y6BUDp5zhDz0-4BxOdmpkkxRU*`?cg(z1NP` zgXg}r-qr!-=d}1*JJ}=Cj_T=@dDq8K*?tU_?P%tc3xEiKf6GM70)i=Og4W)1(?sU) z(dI8TXg65aY>^L*Ew+m+Vv1rS&0^gymt}TY_qO__Iv+YCH~iWxy1$kK-*!|daNG}% zo`D8V_AmmBm$>K#*p7)=j}B3<7WQjz!5-ETM;yX0$wS>L2n)#orMb5IY9Y9U zQP6|T+MC##p#uc*Gfi)GjJ^>6u+(T2@XuKa zNF}R;3XlocLIwj70xbt3Nm+09Rnyq?>FIY4GAj- zdG}-WLqyNL1=49~hf^2gDJg;KKzF0ke*VC_O~;1I4uAF(RW2`q;(zo73Rx1Q*lNH7 z^@OliZ-nK67Z5KpfE4jZh4JfrsbeMQrsN#RZLaw=)=%aK{QVQ`eyGOp0=C*dY`X5a zI7HqI6-9{b4t^=2jVL_4z?-%>n4A$)cxzD3Lq{^Y3IIqwt(>U$i$9B4S!)0gZtNVw zg$hDM|01KPU}Jj+#}iDC^`m!xEjz+!Z~}Cz*@;*+Tl?`Zi$ZpINGbTY2dg;x?JQcN z%#Dmc@Ew%;{`dYBjI^iiB6Ym0KGC#hVQ%KL*vv=7g%+xiV5vjV?_l>bz-Yp0{>fZO zeEVN>RXvgn?|x(18V-4@G#eEcd7GF$zctqV(z;hzQ5u3Ir@XU>T^CPxmvkVY_p4MwTX!3@vv88D-AP|03T>K%Q`KA6~4& zA5#|^mZSJ&koFm;Ma{wli~k%@{18I%5&%1^z;D0}+sLgBQT5VJ_BtSA3PA|xr8wUO z!hwv*#qNwab88KGU8`|d1})m?R*FXe)N7f(5SB3g@|X=mrL)-2BdTc4bq1rm{SSIk zx_B+M%E{6Yhl>6Q=kN<*jP8Yy*RL}k*t5OuPfZ1@1Hn)jNhBn7k^+(;`(Qr)xP5S@ zp6T4t{!7`QNzLYu7MCLYm(-SGrsCt_a}=@`FgZ(cu5H1Bo}xYaJ-uR?KN7X+qCoGJ zK<^8g+3SE;Ov=fC*q8eeb*XUzwmZ8o+sHQvEI%5aHJhHh86d+lLnFIo5NS*sgyM0# zUmPNhX+YV-TFur39ZAJw1HPbE$!7XOTHWPw?*POw%wjthR&qHXmR8ZOdZ!(mb1!?p z3tzSxMih>JO0tSy>Y$2I07}P8y-x`G9V++b$YAS_2%s+fnrWrY81`C&|BIi|PuPfq zvjzG$ewxSQ+u7M6u6RBsE1i`x?%unFf~c;)ll{Mom~I3k=G$Z1+>fno zZFpEz%n(IP1S!b+8erWMlR9HrYma;}0|dtp)}W*$58()dRCov?u>um2w$>)e@cfDt z5fF}u!H@1Jp67?p?IDP)cQl#hso41JIkg&(D*PysOk`R6^6iT&{`B4b}V1@9ZV2FA8xy}-B zfzDE7dxf=-yHR{>KkF_SrDp|z+5|z;7P`){5n|9A4L#a*9_rd0#hHM*(m&(D613xe z@(BWO8g_k}ZUT%{rrWk4oACp-K6`!XC)p#ty|EbsaxQiV?%+ajCzAFAfmIHmhnuXA zE+BRwno>YDWlR%FyGPquu=II$+X6g|%g0Kku~_ITw-ws<^Y= zkiU}@_%MUBjkpEk5{$M{Do;fk&o3yV8D>Z)VwV;_yttga8|&~vkM}^vz$NbOwNU8D zw?^SbF#Z3B!xAn|X^BcB|DM+sjz7M4Ie7?46x3iR6m&4k)_Z=*7+M#9SG7Gh>z0mK zApS=$!=rmOI}wL{Qq7~fJ}(_jw#(P@1h?h>!+a;3?B&r=p3mlKvD0|*r}O^@$FDm) zTJt65b+WK`&~p<6p3w>=f7*vEo{!J+3Fn(%qMC$#Mg2_U00X>LQ5zLSa*Y!osI?=u0HO^pfI?NZa{FGkbn4OgP`%w zx}(Za{PN{Ledi^ZIBwY%4|vPBj7+~B7qq@>fDiuJWW?B_YPu($c3Xi}CIJow347mN zgdD6u4mI!+WkftF06sG6Zo-HSbtgC_1UQd*eK!+PB+-jmfi=4A-cAFUFS-2iq84e` zKAlSj=KNzBp~x4S%?C>pqVR?@dc}Kkoh%`POeMd1*^Zf-%aX`;OA5u&I_068_JeWd}uvuT^LYJ1oIb1gS7DhwE67NRIKL4?jypdBCYM7FKFokazd?(mwP z!nI^eQHhDrISO!M2(hz5ZvuauhEt^Gv~!+LN)tzG)3v1!2D!$XA1 zpv_FsJesMz!Tf zw+P_XONdJJk2z82Q{9$KEP+h6Mr3Sc-!Q(^jT?v>2{w|ao7dK|4>Sq*m@toy7?>)4 z*2q!>+5j!Z$ONt-U;sMMQl_U^H)50KU+1es1tv2+_A3`wmwrZpjV*Bpfpf@+7-1GQ zJ~+*){Q#}N1ONE5pAFvK$YOHkC99m-UHNf72Qh*gq!#MH+{F}A7r{m{kp~~o=y^Q- zDBi!=A+Jdd(7^v=iGCU`%N;-kw~;&bi|B$gZ4kivbFyH7n!>TN`FCg0u|}2_{=2Er z&;vy01%b%w(Ya~q51ml??_JFcV6($n=%kfCgQJ2P;80$|iesbio^aVMI9`zFM1N&U zZ{ORf>&W%vs@c>Yii9|FFdRr2MUt_15RviF-u*-nw^h2CO+W_miHbg;w5I$Fbw@O+ z#}L$Xuz({T(DDoHdecVqBq}Iifg=$UetgFeDhG5`ilu}N*8Md8LkD9bej-ky;EB9r>8Z?l#MguKL<6&-Y_99{BIvLZ^U^xi1e=NG^woQ1h(~}hNqu9{n370BZec&pV zIj@t{`Sv{xgWTDhDjuiWNlsA{;}5ax(IKcF_@P@0kyZ6TjFQOs=OSX18`7VZGTfLB zX?g8H@c6hVz9q^iJ1<}e8sEeCIes(3QSSX1TDO_FcX<9P8EB$!j-U7sa+}dal>;=v zci+oLh(n~Ok-koPfSSGY8v9!qgkjDKLaSCG`AM(}UW?3F8?bgHR!i~&L zAbL=op);~dG5?WMIT#e|bwPBW^Eaps_&xptCp7?$4g6gq5yt!-3}#zM>?h0K&{wr7r? z?K2g<41PQ&KoFc8imNs(KrBv)52No@o4u>GB6Pf`HEZpSan~2+X>vr1E^V}rm$gt1 zMg*VNahF%N2?oB&{qXMY{!Z@_+s_hF<}8uTaF2GX?VsF_I;cYQU$V5$cd>Lhg>T1f z^M-6W&u+N2Tl(QROUrbu7J&XACafL(1rbLkW zT}H-glD>>2I>f>F1s(jte+ib22R<$q6#Ra8Cx2(iP=9$np4Ft&Qmn^PLQjx_6p8d} z0?B3xkrEd-*+K7~hf(OzQ|L11*ZQX)`S|-rGr~^WUynY9U`9*5brk=|!iAQsMzLG( zWn-y%_jRM6#ExG2P1r{?;Ld1|ZXzy6k%t1}Y-gjJ195G6CXlFhQhT3lAffgyiJ11$ ziB9=_@X1GxU&PwTU{7YvU{CQb^$AI9idn9`#klq15)vAP=bwHA^4Coa-(_+=F-+YvD;MVAzPj07bL9$@(zY_MM49;2m##UGE0kfG@YUXfk?;xSlEvaINLHZ?4`!XmSjET%xHVC9ns2>+2?OkDPF|ke zL|pb4C~V2xx1UuYG{Aokvb=n>p#fOhsn(3ET|)1nw7VpR8Z-!Qm|ssl`iled$0g~C zg>4q&QzS^Ae-n?t11ZmAPnu)v^r*qxz@ikFD@Hhy6#0;Ta;H?nH%buGk}|C$u&4GV zW{r2p<_G>}P0rwBRlaAB z^U0C8wA_To-Hr!@Z%9{aUL|mW5))jcjsQ(GB(E(!(e{@p$Gm@~ANcEE#cRGsGFC3A zJWCzw!+^i{9%*DF)sj3}F0mw?dgc@IgGJo$640qqpuN~*q49yo!M zyh4nl_U1x)!0;_gQ=hF)a5u%*Y-Bp%(R*e)61!bg|kUzNLZ5*C-cdtFNAm=m=j&bT?fJ|kx zpX%N1$8!uU@Hb*^8U8MR%PBA~g2Cg;wB*zBu(z*U*8;bubrHfDZ8%e7tvs}dx84+p zbUfvT{#s54qEto1-<}c?r94oS8a^<(hH!3KwOl> zeA}m%ax47H5(Z=FQioP&G+i0~-^waKh45rCS1siYt`cGq2@OVCtBP~?RZd75qtU9J zC|fO4NNpz6u5E|XJtO2-vt3E67j6fZy^>dMqpHCWZr2WlbmZs6&nH;V_Z9+XzZY8) zu>w~i{Kd!ya(pK-j6Ywnn!3%jVm5}WmOh12_!G!qe=Yk9v30vDTETv2M`czaV=qHK z$6-TOtj2rNJAC)B))&n(P0PMqu`S~U!ND_%G^p6aH@X}Vo>mBXn)3B4eu(d&0H^*b zX4{{4b=P2!I6Az}J{iA~ivBh%)F%f~x0}%(AGvnpzptV;{{bgtIWpCjnN!aFJ1$ue zuAAt-%cSApm;U3um6DTHLD|ZAY&4@UaPaVy5{zpY7+9ZPnYalFI1dSUjMaT%lB*hE zebn>jqAXm!>R!=MhGsW{e1Zq6r07@GZ2==G^Z~48~m}+4OnA zU5^Zf$aTs8YTtwaq-BrIM?*WqCNmZhV~n%)WRL=z(H}cjW24#Mj);G_u3)`Y>-rTk za4uxvomzb-9u*t1GiB$J zWkmn3{Y-dwP|~j$t~IEz|1BY-!DS|!O7>=VD%T$?vHY01^{(yQkJpfgxj2&xfS)|; zSYABfVME>m<30JBk5nfJMd^Si*-+ew;gPw>Ff@=_f8}>A@S@r7pKNvS42V-KMf^-* zEOm@&=k0A^@TN;p;p@(#@Ghm*-dBlq96$Jv#g-P!aR%lxJDDmaEM@Ud@M^g)l)>Rt zorC@NR!J|<6wb)RGdw!mMu*gSKmpPQxo{9K;(j0IgRFA3KR)du$^1qNulSJJ_)bE5 z`D(k{@}dk=YlZmZ;2Uow?rbw8u-qCSW#JV1@ZN%Pthx)szm1jRlag zjIsYvNK}4W2%&*G@wz-^K2D+?U2!+5)#+$`#~uBwR*tv)?NC7-`kXX%5xZb*J~^8Z zoyZFjyB1LF`dKB0W!y3{H42O%(6-F{s1ztkf?ILxz2-^!VG)7o4hn(24Qkroa`9#4 zL%NM&&55A5t0R6Rswm9~I%%k|!M*_hXlI`os~o4evUhY({P?Azulw*KG3Y{?Vn`Y? zGNN|~X?7t;t6}~uw1_td|8dq*5~2N=OqZT6_yu(eq_J#dwZ0{h9Jws=Ax*}EUt32) zd0syd$l?3~|L`Nw8Fq7^hIX0kq#$GZ4q1R2O7pq18Nuv}59RcJE{SluCJTz1q5T3o zhRLqPa)+-Gvdlp+X*`DJ0wDK5sLy%d$dw#40Br^53YFUVU+nCHP99dRKn+%wJ9j1 zYS?TR$?EqcYUBjiFlG4Lw~huPzbcqCW}o72u5 zaY}XhP z{Tpt&jLbsSIo?a!wG<8w)LvD`c1(g0u-pR3`z?;agu9iiNV+8qg(%8UW1 zHgKH6ht7eW2yp#_Cz?AYj=rz1_N$|GjIO33NmvSzk-TX; z@i!!jIt-VNLz#}VB)QjJ|6{KciTC{I1a-FVt;5H6_!-rA;;;*fy8T|ri*mtq-htAe z%BT{@EFygraQ67zz>quxK`Gi71_r0L<>#`Uun`7!fObQWNGIug$P^EkA&o};` zY+P4&ZN+g1&uMQ{JDNx~QH$~Uw^kIXdk0!Z#(N(I*4gd0ds`VJGC7LRt!xNcnY#jh zUB(QChqLc#9Vt1BEcF? z(#?VZ{&5J{9oe?fv=`SM&uL>G% zKMI^)doCrf*yk=OebVCbOWSbq*58b1_&9Hde`}o#|gKbk+$tN+N8T#3o((!xtX^%WO#by4M|6ttM zPd3)BsvcFNO7d6_uCV<04h(zWNfC{V3qNXL8SSc6HRpEpMDS;t7#*m&5&-E#5iLoQ z9Fev}A-)2BD9?2fA`tnr{x%ddg@1BY#N&ZWe~ocw#*f)x?6g*(!8MG9=4+(9-642g zBa!UD!XG5%pGD^f{Hf$Zbyq*S?#RPcdfa8HH1%aUqKhI;3M5DJGttV^tk!tw9KD+& z#IlBCC<2-Qm=~zMiVp>)L@wlmP|TMud1i!s2T(5LYYfxck)gP~(Yov~K`p{JXq+jU z=asA?sM~t;^2uI2COL}kcp%%%MD1O@_G3qP+hDD~;&&&;{L}e>=G+18M?2j!w&EGB z=A6QJN>QTRc#Rdd%eqG1$i&eo5NKh$y>~-K2djkgPj9ftUeq%NVAJkz;wcx=3#s#% zRNc9s50-j!uSq*%jN!CjSwU#FzJ9upOS19)uT;-I;%;e;4b@23sd?)n!7oJTzVe43 z_hn(Mtr5+ibJQiPnXJl_e=M>9vZFJW=Cw5%9f}063?c+)SLnt=}#)UBOj0NGV;(v-SklTI8a3^iF05#L}j@ zAF%w#^gP7WY5(i}v66S78Xu>QNwyY>Sl-(o;gv`^r}oC?mh5&q8-rHzmHYRs(j!C) z@5nX*WuZI47caam1HxHEA|{B$k^mtEyq~f?UA*Wk&L^y@XWNaE4g?wU9CxTf#2E3i zzll&vN8T%{b=PFHdtHKvsXz5)&(2w>{!y0zu` z>a*YI%Qv5TDeW6AT%&;)_dE8i2mcAjS0a&{58t=seWk5|igu?yNdM$=)<%%lrct}y zMBp79nwM@z#uP}AP_@5i5O+aMxI_7GeKW=3BL9KLTfRjsE=5_gVr;bhF-2} z>~4x;?BG@a_R<1lk(8&BwwU#PeKfVia&U68XNyus6rMk+4lb@UQDG;)w;^b?7n&K- z<@Ed-Z96sb%`cf@0cv{6ymW;_y;b8o@>W}+W@aqr0y_K zrmmP@t_AFrl?;!?5-rQJ&l&c+C|O+ue*6=I*5moDQft;3=10Ja+Xb)KD(GBr*b`qAn9p_z6oiW5Y(J+Zn;#B5#>126ZT z_P3#My8Ny&N!g$1E~wH6(Ph|wan(cWNlSCQiLNAUMJ64+MjDHlNlw?xMwygMeBH?Bk+v-Ex?qda3A0*V|70H*tYRrm{zVBJ~3ayM3E_Pq&Ga zbu$w_W$lz&{(cq!5{xB6-RGJ~GCxZ>9cQmYg{pS|T)lg+n||^EgVNLFuO{8Pq671j zvJb_`i60+2(vm;ASA8SF{?G2UB|14dxA((kLDopC<8kc(>%c(QICwr>TYIi!Z;Vz( z)QDbdjHlL)C~6?|Ew3v5oVZCvyP1erV&;h-#QC5TYVmM@uh{(pFugcEQy8L%$zlNw zpzD5Gq(W#Q0|#h;vlW(`FQ?_g$0E zr1gj$VckKLLU~V8&Pdn|mg?BwW3R9G%0@fK1vDHmb;1e&L`zT5Ibz7w` z8;XhPcMG5d+eRXQ`9FCaOaRAdytL!f0N4I_s=%_l^o~m2n`D(|x)Rm=S+$yKs$KLu z`Lx+qKWKZNp$5TyuS(GQOT1`P48sKBvM8G(eG$hJK)M%x?9~Y26DFYOk@~cWuC}`# zVGiCqiQ1n>upac?&Amn|NCZ{yc|=;rn{f84N)*Cp1;ak1(mVZ+39{~}PQj->VUjME zCaF3tua|0`ypYJ@cbHh$j|U=U9u7vzJ2k}UTTXHQ*^jK{6TYuU?49k$tf-et6FICe z*^t9O#Q3FsIK>1PTc06_@NXO>`?sSzux4$*PguRJ=o}r^i}8n?Ie#N zvdYDR`*L@vNQHGl#EUeQY7TQ{tME+c*t*%QwrNgL$j=)-#6{{k<6T4}lX${duyZr^ zv6z(dgTSC3!yj>pAO8rd`P(rTII0ilRm5Es6aw73wnaEM68UzxwdM`yTakBa+&78T zosn(#)9dF$N<1gFE>laZ7+G{|QK7ovc5X6OPY0=ZZ)TV17I!N~s#*CkxFJm?#P%J> z#dY*Ix(suFe+xW(rkyt}O@br-1#k<(tMgVD@b)uEo7^Od6&C|KPN2IK1$I_D)IO%l zWu*<#vupSE>&^DXoOQn$IHdF0cU`6`f#G%5N?PueszkrPojCc$-(wp_MpLU6o2e=Fd?E0;Ob9+G+c%}2U*{^|lwwF!`?G(r(Bv-{_kDJ% z4!?3#>6RYX(YfvHocsPD(ISJzS{^k}t#>pojxS(rg$_4W`{9E96I~g(HDUhfrE;FE z^1GzjJ=@No?5}+Ktibsuc^(cQjdQ=H?=X^(Nf!yvkdc@rw5h24XhR5j!BaCb?{k~* z$3Zkh4=hFf0);`rQlYP)u)Z6y6WtYuqpg=@;C*Or$Z3Wf7t>jrZqUkj`2<6qNA(t3 zL*CnD^-Pb9+}EPW4He|A ze>;|uaO(T?EaDZ(KZX4H-@u~E;E}SXk=FV`JL$EN^w+{wk`M26c*b{b%R;?E20@Oh zx$`l%39ooZM&`N^87xw`FCk38y;Tfsx^rY^0Y{ zoLE0CSt01$zRmeQbfMmn^I7Wr&hzpX?r>)m(i4HN&%WaE+{}C}G918ggaY5=I}UDo zmP5+y_Eu7!={5=5;t!y1?%#VE7ofot0%EuIE8ZjAfDZD0`A4jFCntt{VPhU{V}&f$ z--6t;!cV=~)Yq<2;nZ|KP1a?t!Q8>v7Z`^#71{0ImjglaAT)C_Nb9K&!yYpHM+lkT z$yL^@Hcc(YH1}D)UtsyKYDg1DdqSs){L#e*jY^=8Wixr-v^*$lZ=nD_X{ z)Yl@;w1;IyBu%Waxbf4JBZc=k_?%|F{j5(vpIL~1VSXAUdT-Bv4_@?ewfl?`q!pSy zPGR3aa;95aIwwr-_bRp2&4*-8zHtDufxaexwuGOB>2@y@e(zH%o;B~n#qz3+Dy>8U zbQ9ms4ljw52e_L>fy)zqo>8rRFmBQ(MZ@=8x40uKHn6i4NF@HNd(-AYoft{}CUQn; ze&dD}-sa0MPlBsB5)43!jO`-P45hfN!!CzALy#zs#!xS&hH@MY{ zlOMoJlFbvd1BOn`{ks+OoNrPEpIGd~HtmGv!&`B8V&Y`lR_#i`w|W>}Y9)*s__(XX zcZO_>mD4R|MwHSX2!C5h1KMy$ z+TGsWi*uGAKbzUMDqhh3iD|t}KHT{A*8yx}6Gx7WpM%`&WY_a3)jhJod+}13Vv0oQgrFpC$!LUtQMYumrCfhvC9hlsf*#KxPk=S zngNWli^^yg6Tp49dLKlUH5e|!wu%@-XVR2>D@X6t+_d*y-ItT^9lk%VIWjw`&Llb$ zXF}+(sIeLPgnEIC?nWLrBkHbx$s=c8nLMQAc~<9PffrP)dGoe|^^vReluG?AKRlxD zVnzrKtMi6x2JqZDQo59pmQ!Y`70_4f9Ndz(4Y+gkI|tKV#%wziTrrsfqK)yqT(z7r z-qm+JL%m-Mgdm!xCT0-!9Bev|2@u+Z0jT`=A>}K{69H$iSr$~iJnm!4|DlJPno(uc z3KQ4g^Hj(@nhz`Li)iYAvw(z*@`7yecZ*h^FfZH5`w2Dk?tKJ2mqR4e#rKr*j+Ity z-^V5E&}&>iaWn1@!E5}9qh`P3Fq09X$(Ur=PNzZQ?EfbS6leu&t91c$L3P4CjT8B= zEO@+hK3pL~S7wWC3>F5jO+T>nk>cT8*p`^wa5o+=k;AkYiXCczFB{t!ESVFuB)s@A z|G~?JT})yTmr#*xhvJNPsAivWh}`bJLZvg~_W{sG8SIm(V8s7^qxnO!yCk|{ChE_ zi`Y$v$_=Swg&!hzwL!VrQOfn$tKMk}&|g=#vkF_d=A+9h_w}tOekWi2VU96xv9)aB zWiq$;Q-sCrNpB9c+&{k=V9*k*49F8xq*I2LbUcxEJO-TNXki z>acEyaoF4PbcfjB%qCKs`aWjPyDNRAV>I6@u8}W}xT%-;#H1^W_+U1_*T&2(d%85R zv*GzT9(ufshT)haJpQA%5xFwt+=5}IZ<7$!1U-%>X)^um&_PbqCm z?h%Qsz#lDA>u2=nw*Ba(da)hkDjSxnwIvk!YK>+FboT#?87hQQD4E+K!W;R?K?>cu zoD*Ma=6+Zze|Bt>ssIg%?$bDyGlC8yEW_VyPsstZ~nA#iTzB)la@9=1pgU#*m>4_CaeClN-KSoBaeO4 z$FezHPTKAVZBF#reaqvmzIJupvqa(X7e7A+WAG0hFT)sxytmKp<|#Ux70pNx*E zF6a>d3G=wTz>B~w)nLuHSH0j=^X%)A(iWJGM$=hkVl_|{Zv~xFvk)USqb)oP*7`P{ zcSQ_eVu(hekzXTeVlY)uM4FZPGv5%FsB)c2Vd8Xhh(Y@e-;f-ycd z_l!+!-y6bjTcMVUkB#w#(Mr@Z&Cbe2I6Fw3wCy#0O&1(0Bu1yq;NYo^>H%2J*`2p& z#PHXs2vczIhTv=RmczVpy2@_`J@e((y8$&+268WACq+CW*w^=s{k{DYxr8p!s!`$` zIiut^l(6+Afz_?8f@q7NLb*Aeh{e0;;356pNQ7Lv|s@GcF(PL=H#iGLKc)2zs||H6{s%xxLk3Us}}r z9!q1e?U@G%v(@RH(r62Eu00$Ux2{@frRR@@5%8U>3=CTX>*KU-N4t@kbWFR~HzfKq z4VK1KS}6iDRE6)PE0bR{w@*tqj%u2lpxN z-;_z=>7#_X?PJofouhH%1D8{5E$|{91cB96U+!`%+_yF#h8F;^4q4t6BL zbd)s?e)Xa%0*3xE))F@k;#U$-Bts*=ZbSR36K)Y-SV@?9Ugbw6-|l2jj#Lx8qm7JY z;!SmuCMc!0&=(AM=cHkRkDRU?1{@j`2%`G;ri~ja<+&aR8_4j%pLxgSzaN)AJ+5uK z7Jq09zq9*C+>EslQ8&EKcOR9lN-s01cNRb3k%{r`o}r((!VlE$;uvBEczmjr!>rl= z*h~B(h>nosOoPzsP?45&E~qX%43Pa8n|t2=imji#ZIWGA?z1+|1nS71kkuq9xgI&R zPVwSLMLx6)55?q6bym2$TN1l<_?qu#+d3iM$5_gcju^(*^z;I8Q^lp6 zy|;_k1|M*F9N4mdEwo~pAE%aQ9!!^7-8YzMA*fH2+#!iix=+DS>miMqaq>!?nnYh= z0GV$%`7p$BHoJQmh2^lXX?~81%hpLY>80w$l zkF~e2&m^?SB@z%7Ew1ZtyBj~>_F7RRSk26ndMV*>DI=>oN#sLM4w}HFC9eMD>-NWj zn3?MeiR-TEJ0bHe$we>U8$Nehn16Owh1}Osq;k;57k&Tvz=?xN;;D27-ZxITLrbXy z8~3pzi!5zzs#5jye5Goght63sZf|4k4^+Usd_o2U_6hQ@-!v^27&3rH{yY#9zyo9g zo$LGNea|Y6E3F-~4hV|Q*WJI>`mNdYjlSoXM>-rceo87XZz+dCwtSt++x=8Ywf4Ex z$s@%8Ui=}S-XE{u$60jZU>$mJDkkskq>ZKWT_X~z8k8vYJhdm=DS0bk*VPeaSMSlh zpvv0yZV)ke`KL6HAOqg$b>?U0v18Cv&FPqYMf6IgDhjQluVR8h$MlGSidRFJbndOA za8BbdPqdvD?c_O3y_|}ZKP?}-us7!#1T?`u&9K-6{nS##xkm9pWtaE(W2n|4le|`( z4yGb%iu>97-aH>Pmi)+N!&6a7TB+DozV@I(uA%KC`m4n# zV)ka@QXXt);UOBPpv~e($!2BfR(6#KDVwo;&DtLO$;MiiaKpS%oDMFj9R1$aw$CA` zDjG=|eHI$(ear^(d1f~olodE+Qnl=Bt`qgQUFmgX&sGIH0$MmOrIHl~ycxF%%h}6j zl!i*oy(GC++{kBg*SIIo5~SHCY7>w6Wk#!&`Piq{v}%O)s4MpNq8eFdx~fMWr>8kbf6iP}N@ zEs$U5h@Ppr;mmE+w%@=(6l^9h4tnC6Fog_Z82<1kRnw{#VdEyg2B#V*q06W_;3UMy zyR^8=iEPCoSwLDg_UkL`;=_&gZJAv-0)$+pE_k0kFyMhtCoH!QFG#8 zQmDci%4((i$4SZgqe@(ZLWQj#(_y6|akaFdt^DYlRmplviXEV4MQ88p-mqW4*PPz6 z;G>tv7IB5HPGN8IQ7r0AoN-OR26|;?lJZ{tu85|x^SA+v-P2=-E)C1%UvBeas%M{^ zSmQsikI_&H(@fO9GXGj_s=vyz-C?vJvy)c8CzONKNBFX+hBER}cbREK^TMV{Us^I7 zUc0(`$jWupKt)H>hGL9f9}}^$2jg}vmO`Q0qfe;!V#U{tGisO8qXxDeY(LS(ZO*pp z|7>grJ5inNz({U3GZ#|_toL9<4+Ii@?es3I4uh) z`FjBb2GlkgVpYM;r1Wi6nE2{hkY@J zlX*S$@t(*rY`N6COs1EKWZ{`+hY!llyScEpC|7^;7n(>MAz6&CFVeljg>NSW%1&e) z^8t27d7(1NoJ0j}hHM*-iTC~L!N5Lo;cM$3(Py3)vzG7`@=LqCiYvqEH?;f4bVFu> zB3ebLeTUUE`6^3yhp64yit0x>k1L{K1QKo2S<3RV7b&+?}>cM@HYOQO+)f%0tT8#V6275lHoe8#l zyz9#}o}qh!lBkGEE?Nnii!V6amo8!mVv?O3x0xhr7^KMGY8y~ZcZT4`1;cqmhrvI< zkgzTes_fX#&1#3u7{uyx#~B_*s({HRS-YDQqimD1!P%6mA4j#pBM7W%9& z!U!+E30<0wzeoecDDjAZn`$JlmZ$Jrh|om#YZ9I>LVJCQjBbM_+m@=dmORH^R&-V9 z@|^?e%2LCOQkC`+%WJ}$F)S*=?b$x+H(HNM8x2nKl#s-7@@=~76%;!&HCh@m*_SX@ zk*mA6P8qM|h$Nww8RuuG#)ySC{GxUAy*&lnS~{hUrmOG(oTOk5l`P1<`Y!egWvRJUb4Kjm`$(0E7Zb%*tK-z zG+D2bGNf`hu1iUtH$zQ4pvNx&-Encbqf%||!F%o4)=C3V#*}kb9`EW#zWO>^<{qu= zUPzR-Q1WcG(LF6u+Q3!?4184te8!=*V9A4TwZi1OwKqXAKJ;w3SVo}G0RUZpI^p+* z1`jCOOJeEk^A5+0N=|$1wJb^x6{X}C*i#D5r&e(7i`AX<^UZ|fU+0G;|JWRY^C4yv zjCo@_q&+F;B|lMtF13e{={X8pwjlEuyZIY6F=O2oaq(%Ttd`NpC~BC0_}r225z5`W}^T7rVlQUsQ#q!~%mFDcgDB#@o6A&**F&HueHU zPcp}XhI{w@qLmoA;{D#V+#9ovM<0#6JR>Nu{p-sS!F#SlK6=?1ooxG;tkRk?tD9VS z#+nU#*;O7EhTt)0{_#dbc*4^mt}f(H^E_l5@s^l>~RXEIiG{obgOVu zC}Y6J(UC9CU;SR9i~eh;#RaQ)M;`39+)o(s1RM+1Er$D83C%fVBN11tUrQ}&3$akX z{61*z(zD6eeew<-lAVnvDd7tb#l5he&9s>6_egZ{+9(;#PJikYmz)&Q99LMNlur^$ zpvu&oCD5QzXphAz*H}fducENKDU3vqLoh)@Ntbj(cUZ5hbm5YC^48i2c11ggcW$;X zI2k>MKgho{sD7*?C0JyduOp#~*^8 zw+fZPLA_k_me`4itxuPt_blmq(WPRi?q;nQJ+=sAw{U4v?0PRGp+EhT?9jM^vP6ej z_~bf;YA$Zy&Y;c=FNa2{+ETn9^7vhbOgTObUtM>6xQ{fw!Fq0gXPHmsv*dE6w{b3? zuX8Xiab+q$AFe4g4P(r*lE1mw=Rnt;{&9^|ZrhpE#GR?i@1X4PXlkN{gmG&Kye?JL zjeiCok;Jhcf^cQAlD%knVQ~;)MAcimz0+~>l1YNQR>Cf}6*z-ffGQ&vj zNfp6Uc-CtU>&$;Uanu=G_f;zTmmlgHtET)KBYwp97o2RJC|O|&huBZv-<(*u^o;5@ z_ToHJ-Z`L|lM@P>y7afUxw1c`=yRJHxA7{mQ^v_JwglIgLy@l6UTWqLYIkn_8FGgS24 zJB@_q+dpNk1-9U{N{J-B20oNrh8NR2*6hI~8Q?=lwRhrMap!|!f6L=yY{iU&ca#`co7azfwW#FrumJjsK6mw+yRtUE4+xK?DU4r!#NrMnq43~3m2H|%>TzH5JLy{=>KWB>hrI43xngXg*K zE6%IV`z)8ybT6gp&TBJPdaw$Z3Q*{!L{mi*g>$*=Uz}BwRL7kj`OSZj>k1TTysRJn zZ!R+e)oP+v6Q|}N-n#qOc()=?6BlO?uYR6-Ix5+M9r{6Da7oAg4UwWO!}d6pmR#w9 z!(#Q4*AR1d?e%dCxf zv7}oY5i))o!Oc%RMc^~_{7Ghv@rI^;6|KokKGig*nzJ_X>U73J6O!D`%V@mT7OQJT z&flj@p>!rnS8B%Z`;{%>^tq!$FPYffkmmjdLb0iEEztNT-ma_wO1cPit% zc&-y$ry|vH=&zX-ioMIy5D5V<%-80k*u0#}*Uw-7zbx3@V)%BD%j38bOdgH1vKfp+ zX*jN2vw4?b>7JowgjLxf+*WP^!MfnrnTD_eRptOz=E?p-1m)=FuJVo-f39Yj?R6@k z=)dHyhsqxz6dg4sLKv&4spFpTnV3Y& zB(+4SR_EQ?l>$dTJwMFC#*)5d^2v`0P8H|BFo9y@t}2LmwmVmYHB?hu+PMVZM`7|hjfFVScPqnHxK+|! zmRS`9#z=2|&fean8U40<+vN71Yq^fAjI{?X4T*h=+)p_Q$|k-RDNB-`@&g+H_epMv*ic* zl+{m~ynW7i=Skt|ebG2vmGIL8Di^WUug5iyHK@}pced^l@QmLg^88Kkg^%c%P{c`R0<8&>g6xL6-Vw~ zUXdopwQ`xU9zB0M$9cL>6?>CWzV?>EulY#vY{6{xE18ho5BO8&cHW&Qb+}FIY;u-f zJv)D;_W-+I&MW6MR%eI0c=fmBe7N)?!=GwB4e}~C-3#-ZRgsgQj@6>SS47*E?q6;i ztrY(nN2PUoZ@alaA;$C@mfI4{=JPj+e#+}By-jrlrsy%yU@>)mKx(ZkuVapqX%{^B zIOn#ve6>1zE6J?_=zYvm(vTstSc3y!;~C|3%t<|1*-y$*x3jfEc!r`uWAZ~5>xIpX z2d12*jLOg^Z9J{_kPL~Ew;Ls8?cEtGqHcyh(ovRfc3)Ji5+(=s!U-k2W^7V4q3@ME z7M4j2cCs>>GhszDDr9$vqEJFUK}IChOWQwM@uw<3^qpi1Q^SpCC@7Muv5yZcBiA5v zww5ker_pluQTogo=*NLLt)aaxoGY)JL7S}n(>IenZ)AY0X)?$a!}d_zJ1Dt~n-lmt zSsre%|MWXA_L2HiEYm<*+4h@1L!Nq|71!jun}7OM!zv=;ELev(vV2lu_0!oND6rH` z{xYp0RBsIp>U$+v{g*q=YC4^N&iX%U;b6c1@TA9$zVhO@LO1bO+~crPW+?hFMzSl} zVCEjwC1-z)T9P3C~U;MIWE0!k@-{rX58ZKIs z{VZ2ScQ9Y*c;~!guvSPAhY^Y+q>A?Ov!H0|xzaV@;PRE>L?rBDE0gqA6Q?idbotlR zK6<=x(a;VmBVT8uf40HGZBqC7kibf>K}3QV%HF;`sy%AICR6xeVm&%iyeoMuiXuo{ z6NOG*3pcx>)K*zpph#Jh(QGb}iMDSjG{Q~iW0*Hd|6b;C)uQ`)h*lC$wn6_)zJ!RO zbStA#XYO;MzAI^)cp(?sA2vQ+klMh==xqtdU(cs0hmT#HvKHFN7#|upo`O0?IV<(~ zq@5f0X(M(aO!bjjo^-Wi-XUjR1V88w-gqkWyEe9W=7*@<>-naFBd~QfQ)i&O0%D6R z+NneRK#nf*Y6rRR2YAT6uS-0I(AvIXnzbtgH&@4K;yLBgU37K7w;G2aZS%tn%O7KW z1dNdqoA&bje}JWrkZ?(O0Q>sb!|VvAr;~hD#gC^ImeX$la$nG%-PO2kC1x0RMw3Sp zU7Qmj_o@FkJn`%AbUg|4FDd{HikBbDuMLdJN5=C3^H^;1jM!OcQN1shauxUb^9`y>;|rB7zo1&qV)0o*YM)n(SSx3ksb8O7ihtrECh3^jps#Q(f8O zBn?Tgt)<`V8HSP#ke8UECrCBo#ZfNvknD2!!Z+ka@6ztEpJ7w9@s;$$-W`VLxp=2q z`(b6=%nvDSpN?oCB@~oYiZnI)W+u>--p^434eS_a82mx=G(8Pq{DJPc-n83i(~&=> zMT3PJnhH?J<*1GdHV=5R!uTb~l;L;B)Er@P-*W5GZ%XPEch5DP8q>d*mOR=C= zD%FyZG<`kE3zZ6Z3(5v0c32=AM#Q-Ibk-COZOeNGf{cG!nNESBpuGK~pUo#R9zp}Z z3Y{`qAM5@|!;=NY=@c4*=U7&TDWV;t@N!noIacI4uEcZe%aQ2o)W})AppST$vVR43 z!rCO$tXdEWidF$ln6%5#?X(;qQ@~*_Q|ZpT+9pA4!a{ zhF#SM_wJP@QxPdEn1&TOew|5PG3ZxQz7k}qPDbJ@&p_Ixt*TG-PFzXqS0Z&08ayE3 zO~FDgn5F8FBAu1%Tg>iL{quaUaBDq*ID7PSty{FZNE>|`OxUnsNP^dEB{w8AV(MOP z`^!UH&V(&+KWua|LSf+Hq-ArvutX`0t%0Ouy<4;*4h7w=1qEc{keixtKu_?bBH9zP zrp{Jl=pX$+1tSNfwiUmJPX&FE6K?my5Wl6-Y7ZoxNisbgIr&fwvEq-_F=0tp>Qo(S zjsBYxCiCtuIt#ePbgtc&3`>>M+wZNke>Z~dDXWY8s*8GZY|OF-%J&oHJ9>57H}WDv zyK`j)1o4#^D74%oNuM}9R>*rNO`A!fm@J}`%oP3u14{3LuU1WC+g*$$Jq#NmynN*;`bGt<;h8TiPK<%D z)r7OtPsiucLcE}#4r!X{eC+1kfx8l8bef*Wd@ESdu6<$aEzSALlO`$*Peu60+p}%Y zLBT*9sx-eRx)=AzeJ{D8Px49 zYT&QM2B8EuU{Jt`U~ z0?ARl|98~OQeXuk=<-(M8ejGdDF7R}!cB2n#ol`n+H{f6E;Mupm(g}psC^aZ-PK-J^GkIS^3Sl>Znp4j z7&qML$1vz|1mP1s_>KG9*V=!UH$tDMM};JXw&)%^U4vz!h4!0-HKCaQxBD@9y!TKR+Rv&IgkM7b5^g8tSj*?P&|Jm*p~3TZ{K!D3)ISM3o_553YTDR4SqS9k8Iu zXV|E=Z_XGefF|OpC)j% zN*SGIZ2v-dl-kTJD|6V(w`f5klbEo2S#YFJCOgeqE-0ZWdX#ke^VK1e zNB38~tiW{F)=hFICW2SR%jemQz27v5m0=Iz7wx7*+^)XFBzrdYPbl&zaWFYZBhn9s zcO7$tvD-Y|(E`ar8*&${+L5#<>vivK68A>?UI41Wr#QvG4c1c=IXTdgA~ycpVDAC? zPVr2k{F6Rh1NgWVNqG4Ga7!W2ljT9;MYY`{qvV;@H2aJ)&ie_`*EaG07PmZe?mAp} zEzVcNMU~~ON7odzJN?j$oO(mdH7^IFM6}kz`G=>dE0Y6WY{*s}2@pLkWM#X!}8=XWf;{?{Z*|AFL@NMn5rJ zMo|jn&Mdwtx&3?768~q~u4!nzRJHgdGjzAb>i33D4k;{uCpeNnU&pGP%DdCbl{+L! z^SNs)9K8MOj<%rNjhu4(XwtF5rX_>u4#p-ehRAyuqiqtNzFzbmN+h6+)lo+a8ocaV zlSGco{w(Xe^aEJpzZ!0ZEwjuE*HOXBXCe&Fl)JVjdX{3;9TLNk59&m=U|})Snf$WL z>2Wy@Gjlm|bn6o+GU0pmwM@Q`g@G*c+>_+3HH=o+87Sl}U%{EPp#Ix&jsXyuf zbtLc*{D}UpC7^A3=HeB^x3IrA9ds;CDODeiLF)Zv86YXDHAEvAV<#; z&a^oDjjPBV(;e(-pZ^f?A7QIK__o9B9)ewaoYof)Hci?me(_l%&ujsmczi6i`oSwc zhar{xus-&$fW`0*^4!fEE;om7i6i+t-pXqJUb{g-)dE^_d;w9lf%Ct8yCwNcP|8MIZ6~S@g~6+qwftKs1>yQ#SByG2^cN3kM{)@yGc( zVVD=Y5}%hOHuk2n~Tv`Smj?LZk zi*4WHKR?mUz3e9Vn5hz5PO6f*h{$E4;|W+yj>Z}ZuiwLsKey6}C?HQT`!Bq#KybBZ zKb;FN;`T&aIwqTD-f|(i_K*s8GJe9GgbG5t?Z2<^d zjT~QoZ#AKK&Vf$N!^OtS`D-xA3j)D~j@L6ykj>wz-VWBj;`6JD{puBZm0N9bvDj4D zezX`l$;h=W+rLKMD})Y5%_OO7t+!LigaLzyR@X)WCEyV3Z-d<-(^wfPgs49!P?9*B zyQ2sp>q1Q2A4saAZ&d$OEIAu7=?D|!33c-neDVl;N_kdj9-=>&^RVn6TFL?B_1V$` z$GwQy&yuSm97O7R=V^u3A-_9^pD~LwQ50;HP)q<*Xgi(GsX}vn&-=gl_bX_HazE#r z=sgB+VNxs;-aruEETjyo67=s>tr6$zZY8^ns%bO*8V3E#DO9xe&O)_l0%J^-%6!x( zJM^EG7+BY9U+|T_5rchoPcYYTi6s5R)5wI%kJ*S3lm(u`M6S$Q8>qsJ*&pX|h8*VA z5nBH}HkSAU*+#Safl0B~6Ih8HhCnKgWaQ($zxmH{2B3HLElkep+7AYlP%@R!gWdsO z*hEQo0%9Jyi^8vQ<6JC?%pbwW?u!eyzKo>3nD;~Y4a3PW9!hE zB&-H7MLw!HMv?K4*Ah5jYzA>caa#~kNoN0NY!)n+P}hL#gMU4LF9Xo>>KWh4C=>-! zObj4b&m1I`13`n}gTDybGY8qef;0rD_+w%OoxXi_gk8u;FO82w;id8dvHA)$;&w&A z=BlJE4&Ap|2#BW9q-)NcU3pciM$GXH9H2r;VNH*u0C~Z}PfqJGSakc(T$SJf@SGI~ z(~mE0t`Ru}bYa78?)^oNDZGLB=X+lnS|~oalKOjaF8Cwy zmDJz;jbJ=etBJi1gI(1?H2v;yFg{1~gqWPZ~uQ0i<)!~bNDUbqF~vwAV4 zrB`_>%AMzA@Te1i;^gDt2zWk z3<-i)PBG3Y$6P3=r<2+0+TlW#LsO=oIc4xSG z-+%IJCQulKF1bsa8L2?13=0aV;!i2?FQG-^Fp!LsOf&ZHbHgtMVmSAI2L5qm&4~Wvo8 zgRaT>kPWRICr3BT@*dZkgr0+z*`2c$fvTE>7ZdUj3z^cRHG#MbrRAS;r5qZPpI^Tz z{s=!tLBULycp;>8aN0b%tB3h`g4>^7?jrk47|ODWU}1>D<@eG!$`$0+xJzi??*S#xBSzp(tF~x8l;X*& zkp}Dy{OhuA+gr`Q$o6L&@Bq7W0h6;f^F`MwCW{Gza2!zz5=?kvokg~_+&-=)*H+VAU>R)6tdMH@|0v8T}aM18GWveUpjC9@M>uJ3)?$JLx_h_ACT(Sj$ysomB$F*Z?|2eJ%Sq9d)DK=E zc3*`hES>F3l0Tpa<<*V{Yp=CVMjzcYcN1nV<{ak~OCjIrgKo5oz->D51@*3 zW_z7YVR%(s-pjsP|LND&1#SD82(KKsMNGBZg6&lD+dv1}I7baIP{l*>{}B5nz5ws_ zu)gn-ZtMt98)sGpBlSclY05Lg2zKqAmz|oH5`4_8?7quDepGe!io{4nt(zc&y7~5x zqRD3iN!hfRl#b%QcX#4_Mm|yLeDvKlQTSFO58o+19HJ3QmTP65EjRZy1b&bHYGB@_ zll~KF{(LLw1?CB|dOgo%t;`ls6s9|@@lO_wE4CwdvxLm1c(T&4+mu3J-d34e^yPO$ za=B88(Ie6p@6Hld*dD2F7@(Nw{R}%6KuDC>To^Wpv=NKV>fk==Ke5Li#}Zp#_!x#? zoxZbi-k00$Vx%JWwrBBL(48_ygWY&q0|Wkd`K!Yf*#1-=VGXUrg-D(07W-!!6H>@f z_^h<&&7+pS7kpU-b!J#lH@&{p78{M~jSNKOLt5Oz@y1@FJy!=R4WY~o!6f74*PqO` zWh~+wcqp~Toev9KW%*PjtpmU>?Yd3veTjxHtBC*7HF|M?aM7_B9#n>_p#^>U-n`*2 zabuOrLe_?lFHt_W=d(jzc`s?Qv3(YoTY*$04^ScptFKJ<2lE^Q{W{g@iyV0!IfY?U zai2cg?u_VSkDH2xdM3wKAD_Nd(Y^?G4$QK4^w-^>HtV}s8)_LHU{TML3!6!q)QkWuJ*%Oyg zWP>yIn_d4dwoW-zxh<8hBQtJ9^J99f&kYM%la?4hT`j zYub!ff1Y~?=r>GU^WnuJ=g*javmPLJQmwn_mTPLUtN9_tx#d}J?fr&tVT)Ac*?K@7 zmeBlo>!z!3(C}6f?<7m3m}uW*<@{Su`M0z3Yeq*mx-Cfm?d{9;B;|F1)Mj$Emyd?> zcdYE^*5kJ);}{rto-Hij9y!e#D$Q3ZAr;{{c>QL}?0B>kIz*K5C^H@47kYew_55TBa8JsJB>$} zn1Y?x$yFyh3}2n4!FhexG+WmUSJYgM-PQKQxQ4!wXR@zt5&6fsdlTVrHlO#r=)=4V z?hJdfY6!o2CAAQf`_JM2#%=#{2`{a|@1VC7C$Gcql0lJ(op>3h)4 zutF#15Mo_cXUb6Q(iZK7gYaBU+m1155*GSWaPqTh9`V+RT*zi~QFyzR7z!Qy1M%hi zZ-OMfErBriH*SeS0=jl2BYpUyN=6xYf4hXUCsSvh*PI8~>xLj%?v5!4bz(@kMHv0V zdSGJWD(R&X*c#)-kB+1&D!J|Pk9IW|FrZ}$SeB+LlOe=s%n(De8OlknIBwliwJiLJ z-n`?49EG)z#5nionAtL){Z07p^q+YOOjg{B>3`t3`ZZFOgjP!n!kTpPh0kz<(55VN z_$bUPY(0KeVxtW6J?xoI{*RCisT)}k2s`EC{x?xMDBT^q6T%bo0UO>i{8L@6EUWq{ zE#>g2m#6SEVs5*78*Hz@=>u)rz<#|@D&}!0CmO`!eV8~ExBlDpsi!GdyqwRx8n+WL zNs@19-Q{5569x85!~Dqc{5JMjszjqkB;BypWL^bKCQHrSDw>s?|68qt(gn7A+7jVc zqi;-2-&>=`U-bB@l)i4;PSkpj?VQVxJ4uGsV_J1%VuCM^%7ZNsBSGVDTPo>D8^gW{ z6F^&dmSQQ;8MV{ls|p6RP=LP6qJjLfS5C~KkQAbleiA}2XTu>xP|y4tr0}jQm-F& zCbYi$^r~6aC};eAtZwu47R#b{OPQO)y$zH2qL$Fj4{h&tJzKcU5?t#9UKLam@o_E6 z*vXb@M^H9cPwA@8JdVUDq_7*V?T0yu7SDCMf5t8@ua^ zm|1!7d^bq#aDLd_a{t%b6ah}=t^e79e~bU^JYSga{_xMAiQRF$#^mgd+jFFW>Ek6* zk;Tq?^cXV%@*>A^eJODV=5x$X9AVcW!x7(cK4+F$ln0Q%S!0RC?y-0Y@vlt4-&pQF zR~Zi>i)EiJQ4S|45Af6J^)bv}N;ZouiRM;b;OM&-pM%Xl<^dvn=o8RQJvk)y2yUL z;090448dMS#n5BTchx$L`KpO*Db02csM1e1IPMb0HFT18Fe))k_umm1W;wxN zhGv;{Y51zTM&z;gV0R3{r_viVc=%?O`6^y0F0vbul5f-|`njL@%9h6$ToWMi6CoI_ zgv4#6x8ypks3i>6~U_lcFAn}PV#${Cd z0MHs%U0ae5iUN+fqg~t0SOYf)D{LbCRi6Y-@`ZdE*QkW!O*r(5n!x7$?JrQd496yO zg36Cr)E1PNuS%-Zyr<}iPm=jkjWI(SC< z$RW=^W*>29lKsVgE4O2@ly}1TVH$c6{e2CS`(FlmBUuNVVOlG;2FCOrwsFlgwh<0b z!Ua}-I0o1G>LbK*_03D z9>#WrG4|&WFro!nUSDV>`2fJGnJh}?8{U8yzaymQ2T+YzN4wOaRw>{}v^9z1Fewa! zG!Ftge*|!PC}|JpI4^WTpn}kZbAA%2gNDjBu>$#dB@>-glzxUydJUwLU7^)TiEt>t zKhvNsrnA6G6Gg&EfL0npZZGn+q6t2HNJl_99636N!$QC_{B}p?^*|SA(){su9^_D7 znB+Ksyb!=Q-AqqQXr8*Ud&3uK`U-n#miEzA@fxC3__ZACis0NQKrp&I#eZuxKTrgh zOSEbpknoxv*u+Q86x?a**?_c|X;`hASo-vRZi*41zwpIZ@$r>Dq+2?gXM76S>!m>E zsKf=*`1&=jKSNu0K*;=#cZp{yn89y#v^KN_2tw%XB=5U=8g6XU=Tt2+;;F;StLK}~ z%8P!WD9qjo$nwQDQh*J7sxI# zOL$t)Jb2uW-}HIX_k&iFO|BzCM5dy)-)818jVTU(;wivLmc6|dYI`tDlUY3RoTV<) zaO!8+q98mr3ELIpCXrywR}Agge1$+`>EQiI1V5hqGXGwMnPkd(PA=O+M4IQ-Sm-y# z676BDFDfW>{b{bu=DRXMW#z!Q+C_W>{vG39!k{0mASY!j96DL($)0>neV^o63{mJb9#7tLu8CIq?ldMx{dqkXlUyJh-f@P=JDf5AUDVq-|tni%><> z7?6ok4_7~UWX>{|*k>8%Vj1VeM|M?Ulj(`a;O;iA(li4-J})dOaO$;U?aCKeAk#~U z9=kZ+peZ7@gfFP#Tc4OtH=TK)Y}`X(BBt6C$uqmX`^pBxJka&k(z5J$&D@XP&5q7! zXsYQkg0OpXiS%R!^tO4kGV^IXI`e4`bE&cyo6x;4QaTEb9WIc`(UgG=XsSI<0ef+h zl}8e)IF#l^@dR3l%azpZwQ;CHhxYKRJ>m=1qT~%r`*#+c^;~Qo_$veR`>qw=EF!6( z{>_LOr$CyaVW_V80&;#6gSz71apWIF`2bL3_Fva{lMb4sWxzU9>mz|x9|i%I&D5&zEIfFE?G)wfWJ0>3loHJSB^%lAY^9 z{v0a!eJWpF6iWssV7_`4RTkwH`)E2=dXm#Nx$3X$rOkC*N{ECRQ6&_I%x z)=zN#zE1Bya>`rZ<3J8K*`3`naZB*0GSX@f+d*3K&lM?TC2{?U?5oz4A6VkB^uk^P zcSB0J2(~s)rWCKvu!+jvgKVi@mycDl(Sp5q@vfU5#2aK3yc?^2VBj8Ek^XLxrwCG{ z9y?D*KgX-ZXPR}gI+`@gYm@J0fPI@Se{kCOlhP zMYy-*dyFvWz%c2p09G;W$!b}#skAZqM|$`D0fXR=WQNNq_V4=eH zNP!wQy-PXZJ+^GQyhDT{2vl(zVqqkfv8u-hfGM6b083{=n|r1-}@t3!WPbP0?|9c z{X+QJ8b6>hC<<2NZ^?RCJAvU{gD9be?^!d^u6#U+!HP563uTabnJuuwt$_VLoqiyH zxHR)wiTKed!LZzRzg8js%pGjkH*pzoaJw`hE%-(z|_ArbG2#jle>!^f_3 z;Nq$<^hPb~`Jn&P?!Bp9Sx@^Tq$>CLP5o5}Kn>XLC`WT6g+PN80s*)&{T~bAN5cnC z4#r_?9j;5`Csb^+xbCX1ydFyS)kb_=o_*oE+zf28fbKQZF zRcv_=E}ABOB^C&WO1DTb{swu*Fm8QGdV8H37?(A; zcK%oGLQot0#P&@45+KfbKUZOoh8{#!>7MKt8Ft+(>@zGXx{H{%fMrJ$D5e%r%u&X! z^g{XU{K8=T+KIG4?Wxx@3=0rW9o8CNIYT=Fii0;(DE%Kca)zQpEVV|8H25(pCdtz1 za$`!qkK(Bw6{3pI2j;ScyPqaK&b&Cw_ysH?ch*uiziE1)EJ;NDA-h(?jrP-X;4P3J-eC6sgxQs6^d=3c=Y zu~Ef2Z$~CwVvoxj(f@cJPYuAe+s|0>Mw0``6FF*vrKA1NX+3k;6y^DJ0=FQmZxaN_#L<##6jbu>!0Bcdd5hgGV)?(E zj0_%TK9QPnUDpjuYB3$$Es;`+zn#HSmwhg&HJyl#mP z4ik&5#(DAdA_Y-;D#yDL?n)vH)FJ?UF5ser!qOH&eg>x(OJh$a^M#;3eWAT?OP3SA zq1gs2EJXV)L~8q5@=V{SYfO74FBjAyIfcj8MEW9d;JoZ}UcvTA0IDdJ<+K9;nPjEy z_1_F~4M3V@J5`YdplD^tix}!v!u~mASx1p}doN<^c6MMp2I)$l*$Ov}301rOdda#7 zBj}@maz%w%T)?Wd54WD8GMuMSUt%gfn$NE7BEl0trN;3u+*S z@k~=YUuE$OX2}@(ie?I|%n%dlk702NDQWH%3=|-*%A!@t2A5zi3pJGLx$;!bJS;EN z)2$w`V9a&;xn&gYl+)i8t?^XL;=(7uTtdlSvJ4~otHzlI@a#)J$1C|N^INzQ*blyh zasI{jkntHbH(-)`7jkTiJu(L%Q-{dw%!3Tk`HBYI3A4OO5%WZ%;OWin&E3vpNRmaVOZ z+}2TCw_0JJ&V1I#uamE0U8)$p^P&g3T;ENeNz2W=akv+A8mA1AVLNzH`n2=P`^ZY@ z(c(B%FR;=n#5uM8bK1o4geyns4w8vHfe_i*{7+>A-OgZOOeWMhEH*DtJT;J4O-wV0 z{=B*dMmVl=;A++fye&w4XOK&BRu{7_v5>Q_AwlMs%{CX zCUR`<6~J@!r=lQ?|08(ve_!GsftUY#68~47{>^#*|1)fKEG21^T~Z?Mv2X{S;S671 zewpLW;yq{kN#%{xyT{+9f^EuR_|ct&NT72=Z~^~s4h$I$m+xvkVFFaRqQ8qb(f<$$ z>55UJvTCqrm#B&4>8Gq&@D!&B|LSVU_+&pUC-S3n$`WpYPq;XH1*1Nm;U^orFx5!E z;(Y5mND1-Z3FG~>i~!(^f3KJE*ma60IpAx88a@DQ%VxzdU8FcD@Xg3xdNa=|WIEmy z-Upug8YVB+?I=A|alc#?1?l%^?-dI0&*qiMMu(ExvNiasbG1c8 zeAc%ZCJ{$%D?H9@P=}OEmfhm@L5Z_t7>>VAi68;Qh+@aL)M`jq(J}M5;45T&BBP&0 zF`&(<34@@r|7&*bSnez@@12v*+IgG1nbU!8oRf0zeeIx2jpG{pkZ796+PTh%G%5F! zi>$c=kP4VGtRX~)1PgZJcn-z_`F2Y8RTx28G{PD!=+D7SHSI{hlK+yGK1~Y#DsEA%4@ElF^O6n|)$KVR_17{S8-`SF+^y`c+B{9hl z?}(LiArK2H={e$N^X9=y!}5S}qV!*7#KMrMYx)oAnIsNKxxT3xV;IH$43xNmUhTwF zya8$zY3a}snl4hpK`F$>(AMHGw~dLaVP44)l~Ohf{vW= z_;SkXKU(__gu&~zsu~-Cnrm4E5+w}#{eLiHZuB7O)}=Vxc}h_JS!iM`MBwdhOl34| z*kzs;0OcFT!f6_Ge^mwwv_VGQ!=Jr&l$1^F#CS?JyvjO7i_&Akdyr z=&CnnPcjVAntZ)E!UU}{$*ZeU5TZA8-jw#1wJ=f4U)C9}P?$BQ$vbK>3Qi%3Ox?dnw|syUy%arKlXmG9Hzj9c3n#@L=MccJZe+7U{{MPz%uS zs+Bs4hc6M+xlV!xZ){MwR&tvqt;d}1j7&VTCW4l7dC}Z|xb-Kfz#n37h%hDWe4vre z4^HOiIh$>aoOA6{(cC#4$jV_Hc(p+ZwHQ4SNr>N}45E9cOOd;?n zq-A_f1X-f`r#T}ZZw@Y2u5;%ZefccTfkr8#r;=bRnq5Hb57~UFCd~Ek``obwpO`L+ zWwbV+(OQf#1#ZFZIa#7tT@f4p4^btF!+aSac5=}spMH18!No~bl|z{}#$~LjCGus5 zB6v(Ju%H~3$64ojI+=?boZ|!mcX-p3#7%y-;*hzF6WA54yt|8aNosh(ufPmREB(t<-SSWR(3p_&f9~aqY&O|@kvlfMAe6+h!d|agt_p{;H(JZyxu6X18 zb!$s+3*eLTss5o)ni-wGNbGeIf0}My{1w;gvmg$bNh_8s2$azYh5O=0z3eRz=BepA zdqYV^-)@P(rHDR=m8MvJy>J^ZUM(}mWN+d9>Mny_x^RoO6yK7h*T-uD6XvKQ%UL2m zQ<-KG5KT#ppyZ9RXnHZSWuY!*QxFvIM^M6V-o!y5e$dL+_Rr`|LXIJeXX0>>UG#6D zEc~rKY=AX^b%mN1MW5+eVS`KXg!2@0NXHkL|M-A?E_`SHU3f}I?FdT$b38#)FeU22 zyjE#lbd#KJZ-RbmAn7b0ClaOKd`IMi+=$MLdz^I{XALy>2rMPc)PG=6XmQl)WU2&X z?)$UX>Tp*p^n_^XoJS2cX}sL&xzfDOkXD#WSvwgo&xSR+SLY*f2zp-wp4ar4#y zjDH;37AD_*Do`OLa!?#=)h>npLBTWCT%W`AG}H#N=YOJP8mY`O{dKM(Vz43K^C}1( z3pXfWjJ@v}7P1=mD8I7?0<%U5DRVC^&6*QVALK65nirpxL!on-$E5H&>HV`W_U|hC z`Gu!0xgblAYD^bv#LV1`OJ9#T*krHe+VCsIy>Mmg*&iy+s!}56D+OZ$mbe?TE8jr& zj$cE44j7U=@Ym-pA9~CUy&}0xGO>^i&QHWG$lMa3t#<%zX~Adk1hEcw++T{L(D|t_ zfiw9TU|%e{^HW!1yRLx={M);1bcbmeaH1ww48e$>4IDR@TaxJ%$fosFWo!-5cd2YZ zP2e|32FP-A#Uhuc9sOx?2fr3|X!mm$F=({5BI0xXQ9{x}&j6Wsr9bRB3f+Xa^54>5 zYOI?FC<=Ru%%P>KKelTuti(GrtqfKU)oh|DG<`uoCW9h&@2fPWiAB5~Iu#{&n9U~Ab+Wur zy$4!RNx~MDs}``X;m>J1(hq4upH&PdotcfTo{c^iRn{*Biwv0k<`;Ja+i5~akiY!D zzV-$XaL2+-ut3vBj*@jS+vc<~+3D64X=JuGUV?h!c3}on_0a>ZoY>sV8^+n4O5Hw$ z;g8&EguUIroZqSR6}xNl+$N<)kALK8{s>H};`<_erFKogqBfsYD^ET}baZ^^8T3YN zZq`R)g2ngg@ts@vpE&u`k>e}MZzVdp7s?t3R$$0keT~-uBM`9jlF68gRa_;v zuiUnYjie+nSc5Zk!fh%-Aiei!?16N2?#FV*ij{a4)|#iJY>UU=<@yv`Y^}%hQf2hC z#3He?YR@)oj|J^#=1TR8tXGv6$2A~3s@Yk!0uoL03+e;|KVbq|M@@EdmU8>{T5o42 zcG#fj@99Zc$96dv?|)A}*V$k<5<1XGp(%A!v=8JwFf={GNG4tq$n1Cq&+=Pm?>3P= z_mik$-kg%?q$TXwbv!$8ud0>17^#7L-FK+Qsz^Y5UVC?9J}g)1mX()qA2x4x*4;+S zcpT6iSS!VHu-Q~LK36>)T1&q5$%(UMSr4IBs5aoY63MXrD%-S1J3l`P`b~Q)(7BWN zG_E^+{l?2~65^j_-ZwLMhcho%Uu=L&E9Yx&ff3@eXS~%@qrS~gdg{E{nV92P>R{MVu=rpFs5VmQ&nPx z>@nT$YmBj?Tl&-eufS!85c+X57&M~qoTAW&M zK^|{wWbRsqD~FAeo59W?Tfl+$HdA!f?nzd>>U42A zXCCxC&aSpbW*H^K@!DRN)_?GYTdqh=?x1>(yMFI8>jAf;sCcdQf^E@bysuc57Z?HS{dF=l;*l-{cF($o zu3GU?hMpBA^qspipC#mhAbWPMbH-f3>S!fmm!r7oyxbdl;#M}^-3iNS8rld|GsZh) zkiB*-G;;Cu)BO?C8n%9t`!_U<89vz>4&(0i$y#~z5N+O=*u2oXawTl=T`uR*=k1~36uy|Bg$Qriy^;7E#Hwp(icfL6*lEav=H zt=33}Q|Gxc*3VQO8gbS)M==J5(QYMbaUhjik0xYyQ{Ps5*{O7!r!TihDQWuz0SP%w z=RUJLaDtvcGhW3K&s^jiN_@XqIXH0cIinw=#P9quL_`&zcPMEJj~43m6;m)Z-qr91 z1^1dVib)YB#Y0r~Ok?45ln{mK90%#E{QSP!6^RaN)y-Tz8Oasj+`?t!q@>4*Xmb$T`!{cCD#xo_3u|$(U zl8E53(cG@R;Y1qRSfX}eq_vo46c;+#POg=_yrZ!4g>OOnMH(K7%)^4gB7@ngCKp{p zmVpR?2Exkui%L{f@$wVUWBk{|!!6_z@Q7FSrRNF~0Z8L+6sbJ`QQq5^APOO@KV0_S za5?&9y+Rb?o~y7GXedy%KG1aa$jt^)on;iD(ihi6oQ6b`#;&&>MAtCRLd7Hs1TGF! zxmAuyD&E%q?2IS4p^}|x_yt_*Ii~7-odUfwbswZR5}Uv8=lDO{2EPhI6c3-cTovEh z`$@8s^Es%$4i|61p$kn(nu1CTla@Wy{MmpILAKcG+eiT?$CM4C9v3{9=*a6eFDh(L zz5Ed}IKwL3I_|sqAr`}Kxwr`@3|b_6L3pOVIs&O@%omSbw<~A|tMp2>h-k-Z%V}IrrS}u66EO!z_lynt}JX>v{IG_iOkxjpbW-ITej_ zzd55}lheJ!h5cIR1^Tk3`W)2IO?KmQPwC9_u$+|L>~Ri{8?@$cC<^#rM4GFB%*()_ z?uLw28Ptc95KIlAqoxjP=}v7WulxRW7ZH766cA$03VE%eWK9xrpRqEYhq-Kx-_`Ox zNY{;?woAvPwpKn0!rGPMxoSxC=zAR^c5>I};#@tRqm+q$D*NkzRmIxM1JV_5q3p2W ze48HDN@TCvaI{aY(uo{ia*Ai6c87YQ%r$Y&IPWj@WbF>~MTUOFyx>`rf($)v`fXRtKLTgb`XANY7XawL(Vh6Sc|;SyC`Nu=c3c2T zq4|wccu2C6jx}WW_Y#EqME4&cg2|579Wm1BRK@bkr6?<|1ZUSjX5c_n#J%bvH{B?tmJm!v!Fjl36ypotoQIJv{Q`6_Ai z28a3i6eS(Alln0WwL^-R+uD-OyOAA%{@N;NbA?R`$hxokCi%{*P_|GW{husK*V9Bh zCJG1O{=oZcVCkd6=P~(-TivR|a9si?!(0%E=b+1=Nk!Jx!j7x~CoH~ONyt-Iz&VlD ziRxtFu&!<`L9hf=oV>3#R2t@dr&T#72Wr8=DN(iIUoF>0cVZEmA9`2==IB7Pa&EAG zqqGFQk@(tc<4Pe3P<+wht_TiVu5nP$m*!XZAI%cVoHTxGMi$BBXz&Dse5DaR-HiG|XoM_IlCB3feX02;Q4ZLuen^s<8FLJ^>+4<6IW>`L*YPczpAm>s z35S{(p<+oY&~K9J1B--d)zgLH2qUbtbC&Mt;Dq-lF^6bxUJuteu22me@eFwE-;Idx zv3*1Ahms{Or@^i50!;DLRA`8eRZq++Wkj-2eb!GYsU6t;XfU8(n;_yIDq_Wx{_(TL zbBX$wl>I#Spf|cR*HX7r4id zEJ!mzqpPToKG2ecY7V58bbKu@|tJxU%U(hIZT0Ri%#S3_wn=qOWJGVDGQ4U_Ru1XAW28 z$TR(E{xXYd>DB(q0-5YLa^M_uu)l64o!52CSS|c*YGR+8Yv;_uWZArBLMRnpALGzs9rX-_+#aIho@gvFDm_TR5 zcLxGCRFb(l#OJP`l0RqTmDE7`F>{^dWaoWi{O`rp+sv}ACea1TL1%9&*xnx|rIHM< z#hm0HpCp0RtZ$j>=z}bvs3H{dq%BXc@sPddaCw5K=F1qX+)FjE2{c(|+c+#?{PnRt z@aE{|?n-YTj=TNw8KH6q?{=}lR+TYFu~u4XWzJ&g1y1mK_uTgR`mAaiO=IznTU|01 zJg9CqGk&{wX$A-CnanmSzG39ZA56y682o~ zAa|#}Rc_X%QZaCVd#MuVxg40Ut8I$HfdrKQ-1+rn56souAGX8NEHV@|buzPQ?%Ll_ z!m`_UHEy;eq(a_xbYTdK-PSOHDL-iJ2j5eNrDxB@Hd@jpPr_qn`p2pc<^_~=XDXzr zL(lmF_Rybv`-3gHRsZSNJtToe5bP;_mJZ=$g3~lomwiM^K)p(rd}@EI3i|f90vLOGVZyep~l&LIq10I``0sHplNo)Pl05F7OpeC zz|)eDL@&F%)x*UeMd;RT$#JO^+%8)fXTkGpqNeasq=&8?IKw3pC-NPy&1bunFovPS zvk<_~h*`lO&*g40tUYm1IK+tZdjndjPb*q&Tj}NLxx)F>kFZs$ zGRShoW%M9HdRmWp0Q4YmSh|2zct`E<&tS6h_$tD0e6?VdLil&v$#GhJyGHt+k4miec1VKLh_At5^)xzG+4sQnTcZzN@BH>>|mSM>ax1A zudyO)rgG_!h;Y?7_z7R`eECudVB9Ap%xASKG2}&L5aTYbjkL*B^yu7#)4v# zp8Nz>x)sb5M*~nBi?OAqK^4CPr;rUxc~|pn^Bn~>{GbFnS^7dcS>a3n zBmqhXPlv|wmuDPEnisnT#9mrhtNn_B$s$1s=)67UYJ;E@mBBpYR0pqwlUy&+SPyE` zinIgsZ{{j^rcgwR(WS57o1GqCYfb0AaetfMs5ZxKlF))>iwDQfhU3j@YyE6gGu6U< z^s8FU@e?lhf)hHlqZW~8F>oMacbz0X&oFw-U0iS=H+T38 zVSf302orlpj2`vu$K2I8*}qGQ+CbBWGrsM^mPT&>$qd|hn4bM;g~U`gz>Q>gWYUPa z3)`q`AN?-$)IEp3OarVTyOLOm=;qM8Dd`Zq{;i{kj)@AbV)U)Ar-5;U6DpRmUVU$a zv-=u{-K17XeRZ#(WnQB{dS&I1lHt{uEX|R zKOzWRJSgsWa1;vlRzFFu#P>k_&<>o0+1k4)Og8uiom@Ys~Cm;j&Fdm+?=lU;ya`^Ika{Pwnsw@-5W{Kjc2vq}l* zRI`NY;6hjb%A2p`LqFqHN`KBAA62mj6Wkbi|Gd#==;Ki^Cb%bq+Uc}*9KI`LT$Ep> zYVMrqcI(|X|CP-S!7qQB1zhDnXwH4Ea}4qzo+e7Zix_`e?eNQ3&Va5`Wc2{ zZX(nw#$mpOa7&hwFL}jIE*qo$$E_Zl^RzjvLyMwDWb`w(-VRCfB%fYN9wpY)fR}PK za7-~v(t*t>=#nhDb9*9AjV9oL{PEKcnJ{Crq2vI<UWMS*)gLFyH#ZCwH;o>)pw8M| ze{etEg12RMJ=fF?&Z)X~2+1$akX2GaF^iF>N#&U|8Ms`_(Kc>Ig4Ihla*RWg1x1YZ zZ>|yPWQO4g6=UT!HzFP9pTOH67#BAX^((NvX}`jl5t>d`{-`vQ>c%Hph8xedA2uAn zl`iV1g4#~J#cF`dDi=&z!cr=xzzQ>dP}PD1hgC9;>M9z%XVP1tNhD*Z`EekriW0XD z3{t6k?3*FADXXenYbF)i)T!FJDa)bn*0}OEQba$0L12CQ*3|z~x%v$2yWK4T*pp&a z5g9U1BO>Ckwk$qr)#WC&a-TdqRC%JB9xP$$qwmw5Z_}$7)yGp*@6hg^_5aKQ9iu66 z`7)}@WWkA_CZF+H#v17+vy`c~gPKUR*uDD5VyMx-Q&4?NHJDLt(>xAh>IO@7Y?xhc z7gHkcInkN%j7Jd;YZVkaNFRdEPgb=O*;eh5kd4764y)spTeER5#*>{O8!iH7m(v2s z_t%@v$ZQ~;(7lSbBzf+a=D#BxzLdDj%VjZDP*g{sz;Umk={kz{`f@Ws{R=uFJ2Lra zyC}LIXpQ_GMB375Dn?2Iq|t9$K+)r6`e$9vKQ?#7Gd|m*lY*;@Il&5r=Db}SEIFu3 z6BaGQm9S(}N)Q=u^R)-B7gL@~W;%jwQR2j<4d@8O0OSferKqy!I=F)SPrR<(D)J9vn&Y zoHRGf4EYRa!0OO&J!y(&a5iZK< z?BjJ!HZP@iDd=Cp85IwLYN;e+1>#w~tO~m2II@3ErY3<`XpUgx%exh@4Z+j`xTWQt zenRDO0xPO@wUT!@^ZkyJ$H9r-)N(>9SUoc;XT4p#q|=+mT2upg?nv!xACt?IspRt6 zxXEHchi3X#9Ar_b*QO>B0S6tCu(G?CT#HOxb-ZoT#dTe=leOAX*%`irP4>x22h?Oi z0~k-3fl~Iy^8_c=b|77D0UkOI42{;~2>b>Yq#6L;_;k4xl!vd06x>==;x-*tqh5#&iiXUYMA}>NUreX92-e+`2SN-sHD} z=&h{V&sLvvfZ_4svV{t3$ZZ+kwG|EcQ=ApE8cFDg>~Uv(q$;rXo3RW2RdWX6|FZFniI(qk%k$~n)^ z1q8+*(rn&6EWgJL-&cS^1+J#wu^DKj{+-6r--yjX6i^qS@pVMj34v?#^|Jh|lXWWP z1)831XsN@jjT?h{uTK*ir;t->kcV?XIgOI_-Q)AmOV-jjQ-l^igKznJF^uqkRr!ej z;7B&LJ%Lc;{Wr1JkMLBGf+tKjUL%BraF}Ts?SGEH%`AaU@F73Xu|m3CHLp{ec(Npq z$xJ2M^n0|iBIAkM7;s`!8R zw-I9t<8zotQ!(ikZO|@l(Mh}@Z*{M8v>O*s7I-w;Er)G7~`<#BNAOpxkAW6@o{_pe!Gr?+T`#G{oT7 z>EQk#Qj2YXj#R+K&t}u#fC9k3R>J#>Z=nH3t(0q^KV2rkqgES4TFWsZ{!4i2_ha%E zC!?L^SN!|<14)iop90o@^|RIbH$CRc8PJOGUsups^srBONa|`Izf!Gy-?NNS zkESIi88sm}$wziF0mEkki$)B-`#cJ!ErOqxX=%}dfe4Q=mZnOL@0`_a3U25+h zZ|N3}?xFI7vrqtKNBm!1TsghU7w}5Tpc6+^WC4Kki@cZa2@Ot^29AA^@jAc6FjGx% zbUTp>NuDo!J;HALsvM)L(u82KY62pdEoMOdbhDc8gl;iO3I1EPG;xavvKQp;wJp*} z>5I*@{IgH=O>ajHRnq!F)$jC~_6V?wfIPe3%ZbT>L0mT(x;xE?~N@qUVOF5m7A-1@-|Vux9XA(<3t zadw1wul?#}-TRxptiR{5PCy70tTwWkCF|1~B8w7IMy5FNHWXb{L01Q(fb9 zAR0Ndsvk||ttb}5LfXFDfe{rfz8)EBSo}-YViN#O3{WV5Rmom8&*Z6?7#DmieKPB$ zkPy^sxA(;YpBtV(Vl_QtklX)wzr4Yy$-J3GY6ToaS0;~4V4s-J?BSaquFaggNnJ9P zwxQ)voPx3KQ`rZf$O|l*ADvyI2kV&Y%E<2d<=w7R*w`Pa-pF9~B!gSDsN7@E@rEXp zbI8j2tDnoxZ}WPP&VLT&IZ;`i=_*~kInIt7V#uqVo6lx^@1v$%0MVne{tLue)W+m! z>|0tBKOFvj{NO?ci1D(N@ zrN?X?dEWIG61*pSmK;p+>5HzSUiH(XO9w^Tr&eH&(X0nTp)^lIDBH{E=yZR`hMYT_ zst_^5xn8_i$EzQYshi_gn*mZr2|@-1Nzk zgq=jYCGsL)#Dw5kX=@f=HP2Zv9Ot+jUTF$Cq_hjMsvGn+nm1Q8b)R7!MSt`!SuWu4#I9Z{E{_BIi`s8NrA zTo=i2cb!~!5neb?EdBC<61x8s%K1?*7tEDOJ`|jLeJ707qjj|2tTr_1yHwi}J)AvP zuw2G|;B)CL@9C;@ZsWJw2QV0mU$BPi9-r*@PP+0XpEjoT!hD*g?zU48?p)-=Z7)ut@1xC|JMc`C($A?~@obhW+eU z8U^U(RG5z~t}-koF%0{05?wOOJ^f#_!KTgrTc)+tcOHf6bP zOf5cje64AGM0!$rQkCG*hvr?VzHyDmKa%g)O%)SIeLRg6|EV0`>U&;rnl0~Dv0CJ) z*u^3Gx?GcO&J;YBuk$mLAZ{q<*{C*7Un_TbrDt@>G?#1y;^3K7Io~Io&Gt{SZ}35t zs1b1frTE*~l>SdORDeS06?_xeaf5RZ-96qabREK!dRDYkb3m-Jk|*7k3N%;_2(Z8w zztu_7)2-}-Bbn>j_k!Z!7QJ3ekuPHNm27#lGxE zz$yr~u|zA`(u3JL_a+6gl^D7rqTI3|)(6DX*`Zu2evb?e-u0~7R~KJ?y@eB!uO7f= zqQcVg)&Ya(8{xcI?%a9u1v7A;gb(7gn)zksDuG=qI@jx%a;f9-yvjG%>Ku=L${_j@ zv)(XJ;a0W1b&5{#<)lmnKK>421Q3=}i0hz`Escf5OFnT!-cu0^mo#R?- z>-mFY$IYo2i=4x-eBVPh6%;8%xbmSFTZaoKb<(RhuPVDpCl=0_jw*c8PUES-11aCo zDqnZ-j(RL+$?wi2VeFoM)$Ov7gVZNP(fdYgHIGIYauBQ|*v3s@YPy11B1ydh2c4J# z{6a=JZ17OJf)m{OM#@EeoA!0r&W4mf^lMS>Oikog%9-JHBs~qDQhTIIVHh;$nV5s~* z!-fEHq8ChpOs<60Ge?ZpjMd82L*^G9VZW4`JiFQMaDHC7q2zjKcop{$p%%lb(5C!^`r7OVS*vPn;#VgJPj^DVM2hw1`iw(-Dnw0Oa_IIb zv_|~m^O&r)vaxcsqK>4Kg@}ZuQysofU|_LN#jMAlyGUnthdEQWs2QhDQ=NaqAUZEC zKt|tkUl>@WLgk{;aA;ALtBc2{UdZ(*^vldqj>bs=dBO|K`lP`VaBm%);7BieV(pB4 z2ZHU->NBcqlQysejS|NX9Swobo2?F;{Np2^x>Yk_a%d$?i9u?#)ag>M@hVudYpaOiq7;AS~xH=j;ix9Vk!d9ykVjvjsy<9MT!g45AEALM%)n)s<# zb+~r?ZL0m<@240?P00qZ1%7Mr7f^9p6b5PbS2k9^*g0f{X2(elDp!hF+kn~lfiB2xR6q;H*s8H3dnExtfB@xEG(iB@zKZfY+w>FWq$XqN1 z)*5KrR5gg(Cl53_AAj+)GW&|6a)wCD-B-!XDbdZakE*fzaTZQ!;beSG1^p*m1=7f+ zx~7SUB7pvFL~vU4I6*`Kg+=H|qAM?-!I@`dGL$F&r1rcn>})%>d2lW@VOX~7hl1bB z>ufWTy)7$u5!eH;MM({f-e8OL{vFy2{-!+Q?_k`n<_*MisjYRUE61<%o$lUH_gim5%*)%6Ay6WCe~CDrZH+7U}(d)h|#kV6pH-S{HGjHCC} z>HaqLFp}(0=a=+zZn=pgLL&y6{UnxC5!U-QJO=I9lg2OEJXl585U*t1n_gV-izu+f zgg()Z2_5d3*zP|=U41kWn<8`BkpH=2Oc!q61j?X#Kj=!f>o-X;qYf9*_} z+uC3-ylzKVNiUU9oA7D$nSw@lW~Sq#p0%hgwGSl@K1nQq*xYMh>fEKLPQ6(fT=4pU zZ>iZPDwj}6=(jVhQ2*z6tYvK{tOZlf^yt^+(=gJ55ubxpk(-iBshUMoW=lHH9?rY2 z7V)`8qW@U+_7zz5cFU83dNhLZ)goS#ri!{iI!mS6AkB0S0kKr_Jdto&C?T>}nk~$I zmw%Dv2R+M%P7N@Ugc#)0M3vm< zCAnYFy7V@N{B1aFvC`*Zu`?gPBZwVp9GE(NyO{%U`9pf0rP_&s{pNW^No zs^d|0)RR{cjazpaL76DnJk-GQjYXvm<|90(sJVf@^ZeulsK}>NH09{vltiu4NxW9Z zUu)YfPhbWyf=$+Y!T3ULGjV$#uI+#g5(FIGc*9ed-9&1K1MUgt>O%8{!E2bi;)~6{ z8YmaKK=JsWk@)}v$juu+*P^No;nP!89JI*+4d@302{#l&a7F_+-sIOgbgRzV={W_n zTZqXV-%wU~f80E^>Zo9KA8$j_Ni{g1y@KYkYha4%nnMZwVujQTKf(eYe&1-~w2h%$ z4*i|`F%vvl@%eNWGpab|oNJbq19*h5SUdRi(|y0oyStXJIL$S4j!QbV`+Zk}3v`MO^I;*Fq) zuC`M_!hr0qXQ2gw{T2WC(4^6o%5K)mfe>%G6sLDm zgtv!Q-EhT;D$!&UK^If};9 zH>^_00F@Z>)UbeV2}(({fX81YLS(aJuT>b*zQ$#`C7|ZsNkELZ zahdW$mLvK7wmGj-GNf5LcR&gOlRK8a#wrv0qqoQ}P(99rlLslUDss7}5IY^YM4AoywM%SRv&V5zt4d(Pjj zzRn*qXeQ~yKP&eElnA!J67RU*<$MrpmYo7jr7cEEWNJ zV~YRL`SN$?h#QPQ4?5RLgB z_uzEn)ucHHd5f78q02h;nV&{ctAb6a^Lj(mQ6>A#-Sz!;xP3$gJdQUOhn5d2>z>G2 zU9I6q6@GUdQg!9k57~ViS(whe-crSxTvl)~YO(KfJ!j`abe=Bd|5rvD!|#Uq$5qhlpM2o4zrj|=B;E)UdY#| zQI=yoaxUjc#j@a}dh@cqv8zUXVn^*jant1Cw=1Q8dtc1XDG&h~Su}sq$uSeq=M^rSS0!%g++nko01&&nys4P1us*N2$k3P z=4|lG?_QB$&jH=UccUV)GDK6=IgA~AtK!ZUV;{%0z-kl^2qy~ttq>3QY?d|mNeceV z;l9TvxmJDe4RwPo3sFg+0ZVnzm8EvceWR;h-TN2X0^uL4UTN3m^SE%{rpT#1lCJ9L zQ#iQa?cF99lWbH%WBroiLD!?eN+IH4DWE{4fin-)FWgXuMJm|Gea^)d^kS8iSx-9`Aa!;%Houy)TYZEk@T)h7-5qpeN$D! z<#X|yGh}}1xZ7i0Yh(Ex*3crn%~Zt%pITOoL%nH5l&aQmjEPOg5#2qy{t3m{qBv&cAPDn0Mf?ZJ57Kq-D~8Rp5Oyq% z2hSY`qC6zxs*RS9(>U$n#9oG4Rtp(3#DrT_+x6Bdc6LNMf`uuow?%6)(rwVla7~9F znMU4VVWvX4nNC9&J+KRfm2X13^QBh;;uPl(?wF)rIUiqdVd-veokJmH?gTCw##c}7 zr|e>4$HkGRnVjw7@-|si&0C__$}ztLpcxA7`AV8EwgCMY1qvqV&&MZfVU(_vdk#{* zXp%o4EsucTxGft!S;|P70BNZkYVf3#53s%GrQ}Kuk{oLMj)B5j7}plxR7EK7rZjKD z_DW-yZLt3h!CgT&&8CyYPR*M{2c3lqD75=^O2R?C@3Sd{QUB%RQ7{?}X#nI%DoiNs zV@t1~a$re4v4q~t4Mx^5E2767buKnv+NPVRNthfpj*F-Q8`xef6G4Kv@QQXWUNp0d znA>xu?bLD|db}mkXUQqjc^eIS#u_e>0kWC|t-TSmrP90=+jKG@z7s@E@j9|#*UFH1 zC#up^12SYnh(+S{1h3kJ{F!JPBF0oOmx5PG+hKmEWo3djy4$$_x~i?y!(C9>TJC`+ z7Bhc`xgv6v>M-6qBLh<^i^>DV478)EMdnp27O#beom2MY)fk&Y1Pz&Hs(^%w=0Gtz z;d$Ea{z!XSZH@a;<&*}y?M7A%WLa%|gwoOn$%QR*<2NI`f}Xr;KUw>nZe^W_kt$lT zar)KlW0E#Qq`c3jiMgqi`Q!ZV2+qpg1Nt07{<7kuvSMSs*j2xKCi%VF%p$8}R0BH} zIJoSPBAY-RCyz7F8EC}JNtGT~zt^<~p?5XRM)Us4+v{RqROQ29;6U8f3Z?!W#VZA0 z)JTD+tC2TA;Xfa5p8@7^Kc1%CKvUSh-vfQ5hl*a=OHWcpYCZTRx+MDebBbbh6sh(~ zt=2pWO{BrT*e9;)`(Je?1Bg)2C7o5@xM7FCVTdRDV_3h;>mQ!KV*$o>CbixW9<=2) z^4LdjagBn49MMbcI2ML(7XAL?9qP@;y6yum8AWweS(oUL)u-elSx__`-|F%Q+ZUU6 z?7muN#mHhqs8B#sU4yH3daFaJX_0EIcccv11DD@OGJ@CFxi*w)(v8TkzP$Vxc|kVh z*JBlN9$A+i6+yG~HkV+{8GQ0Mt7UdYdU;K-8p8~a?6Di)(0)?f(CGUY#OP{QxJ+@Q zi2RQ9r<@u!yus4Xso*#o&9^n1N4|3Rl8%=I(Dy&wIg?~&#G2dpOm@kn4q78i1f4cr z-1^s4M(V}~Au8B>g#BoEHxC<+I8C)1FM4Fqxg{;2%VOH&m>%suOdYM;6AlS;nLHl2VU{gnQq+%Sd zYWTNNvBSwkvV5@<9?JanzQ0`gzdVyZ0X{3JVI%ped)hV$frNP(R;G9Wx}-SmK&eC; zF56h_*Js+TjS^g$#+hEDDYW$C&%E9=`eJ+ZUb2ndqBy%?4dp(o$EWsz@L!_+4kpz8 zfMpeGMEwpOlAlv<+@kPc>ZSZ+1b0OM+rxdXWZY2*Y(Ko)M!&ia;XJ5vB4Z&3e{dT* znq$i=!DsDCdu|>?dX_q6(f%6L!vM{JK8Xst-w*H9I7MT%83@-UWnRQ|AKJUdhi)8D^1|C5#5}(zsk;ZFp<`?#61q$x zFb{>^{~c0(1(tt&{`~o_De7=w3bE%`4=I~G(2c~qpT7dp2s8Ci4s`!orrW75yQY)0#Rr%fmyU#kT9wW8sG!|wdhjxo1b@~A};d!v91(9aD zzqZoFbQ*U+hW(6+9)nZSfzq~WZN;p*-;|5RVFs;2iTHWZxAeM#>>UrS*@ZgyW1AK~ z^yYZO)rn0h>^;fZq3B0}+x}dNBxEt!%--lclu=e|YZql05_)d*G}2wC!A9Po96d%U z8~V@^R~({9yelY!02(uP0e#!BPl4nBGN5kWV2yxe9ft$OY(KN$a=>B03@VTm6wPpu z=#=pyt!HWoEHa>fj^rOr;#Uq5Rz(XX zoW^RiD*8e?gnV#=D=E#;z_-`Vc-_KQe1LYq_x6Ft|dsfFt@96-Vk`tmhbVjS{*?jimcyZqxwl4V@nqJfYU zgCkp+50oa}D1^nI{TGTm($_fA$dwXy87&s9qdiVO*lGtzA-V*dlXy>sj(!*Pqb z1~=CQzdP2gv<~wBvg-U>Tpo(k3FfD@LFNXH!_P?hg8qf0&$b~TeB zH9;Im4m{iud#AQv1<&-lxD7@3oM5Toe^NqADyDbzE2;PP=t!e<$@-i9mBhaQB{Vy( zxuwYOVv9u_Yb^OpK>7B?Lvy=LraUGdCJnBRn1VRvJdkacOprxL55{l(u*K=M1#P*jdHAhuCKp!n89Pw5b_=m zkP4G-t7#WO+!h)+DW2gCbjcS_WRE0BAL2cf4=}JS^Hf)w_D2L%7vSjf!QPPIZzvY9LH25??|5jFb zr&K$PhDVaMZvFzaA?qEYMs0u?VBvvmVZ#M!R$PCEdIx`gSxDo3`O*Z-ws?K4@Q*|9 zQ+l3TTzTrov(C>GqU3uN^x8?7*4vJMsco4hRq~%(KVTIyY2yu)&F+$Wmqy<8LXxdG zU?3xnU7O@XZt3l#P5Sd0dhYIpn%)EUk`h?spoAFjFMHoquHuuPuF{>cj^)D1oWl$onrW@tO2Jw zc;$!AiwmQ6KR-A+lN{ERnHOZ9q23&GxS!jMkgO?e0Lu6}wmY(1;`0JSSYa+sVHr1H4mT#UQN^^j7wi49zmezmk|);8&1 zY3glb@qwKqoF{*$5JZWa&~90r-rZ+GeC6f8Nqw*mqz>%Ua7}zVm@Zt~3=^V=<_? zl@$ok7odOyDy5v=iN4pNnBjFBwx)3HXOXh=Rltzw`jSO)%UorWznEWiVv<|GbWo|i zVhFfaGcjWL@>aZ_;2rtobQt67Ub6}-O@Au=U7OVh}hVGoF5-S-^el0Cb3M%Jq%qDPC>3#lT@Nh z($U2miLyB9Q5Bi1+iJ)$7)O9J+*)pC6kw1*L< zf&fn+#Ox^ub%H43d<9AFc;V!ERg1&fFg0Z$Ev=q`#!H*+lLGIDlF~q;&hMYjrdq!A zD33OZ(Qn^B8$@NBB${id^Q;o2*`#5il0_o}TgSR)8Hf$L5UC8s7@@az)e7Q~2>tII>cNGTU8A_`KJu{}UxMj-%bM39(|O!%YR(a_V&teAn&C7%#_G0~77y=eJ(7Y&*n{M^j_oh43HI-)x1 z&iC}TyUBW88HGHZ9)`Cu4QK5~2C;DcpO54-p- ziG@PVMY?dfHRXGAmn2YGIP!X~5@Az%sGb25Ak(n)yxkFYT$?Q?PK>Unc{@%eh+-q; z#FA^7k=1>`X>n_Z(G*tIWVo&HQHt3Iq!J|hX1HDPnZaGrbEQQ^?FYwAYUyF*C$&zJ zJ%hc}Tv}YkqM2?Dnm}X>iu{fmnNQMK?u`Tm2{ths-7h)qcx4^CcPbOFgUjk%&Ti0A zvCJ}4?kb*Ymw0iM8yr#n8l~N+_OK^y0*i25@m5w^G==C_Npr6bI2X0moIyOh%WX_2 z+P+}tgcH!gR36p0)hs#j+`tsASb<}$aoz7@f;{dPQQPB z7$S3}qooB4mc)>7RGMH2ar-yft_{S_&c42=Ralx<9A;BcHOxUF^^N&{^PD&@wJ{zk%lCdoAcM8qw$v?Y3VuyR@%xQn$Mnu zuy1{%3)T|GXU%=x>)J?&aR$qjI6izhv4waz4t-Zn0^q1zDHOBsTN9CgqQ~c-SRU=K zY8d4)Pz{snc+qu4`WoKl68;4!&IboseG^jZE|n*+#RI7gd$K(9H&ScDuQAu-Y8md%9}(jzInk zee{f@7bqRM;$Lk%{ke*gey-x#LEKlpWmU%*!=*LNMbK7UQgDON- z5{o2M>@$tFrP;+!Gc@OU0uYdpv+hq@yJsXpzR_x0?_6zePQm<>%JhwSUYFk?BQ=Y% zw6S?lMdk&}K$-W;=c>%-U5`~aW$(xT)4KGpAJ$Z%xx2eN?wOwK%*OihUw;1zxNZ4z zfF6+kC|@@=*`Z}wW(m=dMSX+GIYs0RT!jo3# ziuc~(ird=>$K-tm4Q~*6x3Zo&l+713zEL5FEv6-XKumrs>r1(T%%7t}5dv_sC+1&i z;I=4$V|;Co@(KA+hdq0>AUVOLSAoJj^kdK1WK_f(Og#>0w=OQ1`W{=tet|bJ zE_D*KZ(Y-eo!ksQt^8g^d+ue@aT&J^CkA>Di}m?^ZciufPu}HoC7ThsiqBojQjV*V z-_?r*`%*j$ugDw^xtF^+sDbyfYFX=&0bRQ6==FTu5%*sA?dIi3@S8XLB%iY-*oT9P zm@~Fp(_f)cFxORGqaX3H*OC)p_4s@MiA}nzJ}aUwk2e?(Q_Dq?cPf-@3WluqS=dU4 z4XS=L8(@4P{c5YRqbhT?ujrcTfD&^=*@uGa!~M~+d{ns$4iGH?k4#TQLWvl3pQN2z z_g?DDIikSzDfIwO{c$WB1s_8^`_)y%!9It&{;rnQ10ZcrS->x5eeWK$_$)>hL~w9C z@@t+VQEKf3`2Q8(EF615D3Y5CRVf3U;lh{|Ham2TQlj@T}e$$Jc|rsJeicX4fN3_ zbu8X;f<^Z<)JPVD(Dhze-G{@rrx0Atcz(AsCNjBXUhoRbzX>5npsgw3ZoFYL*41@x zN=gye7+j2uaw`EcfU!xmr}}&axB|OgJ^q`gWw%qClpnhdGZ3-+d7mJA1$vsaO1G5E z>#NqBtv`R8*G&jp&hC+TmJ5&>^gIERVeAhNq_Z=hgum&JZrnBdJ8c&>fnT}i~sfPr)bL>4i3jy1Zy?oSr4bdI*8sgWi_ z8y9N*^*!+@8-ivgC`EuJB%CmweJ5hu|6E+rjYW=I&@=YOllSL621M@uIFK#)0R1GE%%t zhUE8p9}Gu7Ey6$RTm{rJ7m7@|%3%h3;x5Tv3>|bf^3hNoCC4Smm44rBe?pm}30)dxLdU;OfjRh?*IT`q+79TYkUmsm)*%ovwJqf z?Rr|MZ5MJ{2@!U8jPWUIv5RG*W`u-*c)as179y%&f^?e4{c*xS;%(2=syvNw_wWc| znDN+cJf}MD(Z#s;Da?mh zeMU15j!v&sniZXjP?bOFH7jY9txB?I>`Jj`2vZQkGbtLZp%<4Y+?UokP|B6~s!zC7 z^Jq;w);ePw!FREDZE5g2U6jd=8+O7H-)7v3-@D?m<@V1A8f#rM6YC(6H<+1ueTE;W zSq73OY>LI-(j~gIpUUb;l@Qm@0sHQ|4K;e)W12e_$THz!Xo-2||Moa@R%l=UyF;dd z1qb0t-(x9i!%NJed?hm8$`gbn8O5)TBfpom<8JQ4ClPFg?9%O ztr;?IjH^o;=2$KlyP}$DmK8j5mlUWTwFq~gI^g@n6X0yz)D+2Z^Ca-}67o4A@!|LP z^E;!rvI@lp&d7ZOmKbysGo|q?EK`O;B8g$XS$5n&&DWIPXfi+?K&fa+sS|L>=~*syrNt6aU6ZPf;ghOYkmEiLvsv6Uq!tgs4>)uSes0}uNRi#ieYmQpV{Y>~znV+)f=|@WJBqP%IzS-VdT^(z$#p; zLxRpV2_Rb5w=pBx)$|UXlf^ezf3$G&Z}X{DXYnl@fC_$ffE+P?b#a2SSh`rn&lKm3 z5C@h|`{Cmmb`tR%Y}S+(aX<^`W2i%>gUmdOunC{Yg!7`~-4nmL@uLMySxoV%I@9+h zxmYGJu1vdeGXwT92XDNbzSLVL(q%%BvHgOp-An;HzbzS~dv$7raSzEhHqEW@8kCoI z#XF{P%#p*IbOsR*^^a37RZ})q;#He&&|;~;6Lrj^OpJp^A8JOu>hM!lW6AM6d6sqS z*vun)_N-jb%jR@AP~mDIMoQQma&f=TB!+zH*z)7pID{K&6Xi89zchN?Nl4i8;)3rx zQb4V%rr`WOH9L@@9o>$R0n@t)|Btk8A3q1 zyF|LBq(P)xN$HmE&bi-+diK8O?7i>#-T#;me$2Pldi#BzcfnaJ<6&G6&DP#nr$7I> zL+CBoXN%gj?gDxEB5mR}pvv*H-SB0IRT_XE=;+qv*G^}vGYZ)*lVscd=BiU*E(we> z)C9svldkx`$$=M;Bc9Xz&0`+CheOJSU))sYQ$x%a#))93K9k<;UtB_hfZPywCcTWQ@d{@BG+&#$yqSc0GN^>24%$`hI3&qS>(@ zu|uUDgDf22=LYXr1-jQ+ki}T?!)ko-UMs6R7cZ(*1E-&qTMDkrnRu~v8k(}MtO`Na z7H_-C4jKpaM3Qaa-Uvt_5XZ$9a~!5xeA*{J{*q;t7&D)Af7Q}JBUJ@~| z=;ql`k88OZFc}($%}+<9iXF@*K}Z5@h|3@BPi$5j|-J6*wSVXlR+8c(g@iRXcG zH$#%e&|)7yiR&tJQ7Aq;t~|)@`oum_^YG#1vW4hX(g8Sw;8Fc?eVQrIJe>M!1Hc1- zBFE$tmji~{&Wnr-pm;G_jOkI#VOTt|#D46IRJv^u+g!D&^}wioHU?q%y*4{W;_s}h zQ~*$!(gxZ^@82@8a7hOw*rr=z@C@uNKowc+aU|7TDXh&e=B_Z~?!fVuTT2g(SU#^< zC-4|msYepTzPT@lK<(q5SA)MMh(Go+3qMjNhM1o%6(Jh_Es%U%8apc}yfjJSv(IIr z%E`+k9{UfabpiyRsVS`+T?=#bqD@*J9(4%5`?O_EL1Qb8B+z?0tw6vzJ^|yBUr^|k zao{W;R>r}7XZ-jgOmmdM6QL6SF<9dC6$M`ubXY`xh8Ge`a)v*=$9IvhcdNpi>J`=MT!H4Uhxz^i zU27WzpVcjVymfZ@%a5;u;Qgn4`uz`=>DuCAhzRoVcZf4uQBN{k+50^_g`+vYO8|X7 z#mx;nYBo>y)a83$Bc!NUN_71Cv^di?3c&s84+2>dYQKA)$CAOD;kwy6^=X@04R%)H zXGjG1OAC0+mheIf(Fs!ECrSbuM@1MFwDW%Od@U?`T-{a4=I)i=A{HTT#}D+|x;gLq zZLy|uv!<_ZIl94L$%E5WzbQUcsiUK?6y0gSR@`Vcx>s*KzRarsM*nsAE5fy)k=h6N zopJ+`qTXeXUAMj=!(Tjwz(7eK)A@iN;~=Nga;~;fQ^f-{_OGUhG+bU=*TauPyj4D; zf_>W7B!|k~z#t~qK{OiCcqpHrwlK>@>dA7nB+gDM<@aKx5gl+d^E3P)FUM2E8ofyN zkCFv!h}v$+TeiC)R-uD~e&)WE#&zE-JZO_c-x?1dIxktpp2U?X_^-ivr$c1=1; z06~@%7}3O!Y-UAJDnJ3}X4MqoB(Ta#S`*(kYSGYfN#f8S-CTJ#Zv#2(O4B}WXV_U~ zf;5J3peqx82>tlMBoAY2!Ts7f|8*z(8w>V z^5*meFa%Sb{ub<<-E?Bw-TN>MLA-~&ygS5d@&Rf=7f>`tg;gnEt<|+A%RQj_|ZlM+E;{)EWq35v8iCYJE=+laxQZuvUlVX13_@g(+`%6dlD$dhz1| zePx?XP4s8>7dkX%WIp2L{3pkKrfNBp-HGZb&Z|zcG>_M)74$D>dfZCVb@8YY&a?J4 z5VJwJnK#J4;5mVG6k*A#!9fO7HgDCRQ9X(b`#O<+A|;8|EwU5>CzBpoRYuI*a!HsR z3s#{b$IB8@y5Ti$rnJH>2~O^1PL%!h z#DdMLS&2E&;tuR*it3>iFHt;$Bxp01@cpLYE1Z%jr#;c6%3bD%3BNms&)r{ZOfjenvn1tu z>L`uRKg~^gT3|>9P^Rt=gjK#*k*o{Kd@0`L&S*Xws4VHz+v;BxcfHpXE)y=nA5N0&Ltb2m@XvSzwB)Gwa6U?s$c0(~1Oy8310YLOA~D!TRN zs?Cb)r&z>NDd0a)mG#B}W!(mDF0KvYMX8AUP_gLv_yE_VpPBjj-T9pyf4=X3EboFu z5Lsl;F?ZS&$RXF|IX^wmX7gI?Nrl|1XdzGVsbxWEL0_>GU6R4rb@0+lGp7%!W)nLo z1o({-tG$|85)x^#D^*6CPU!Qe;T>Lm!itm(tJuONq`EIZ-cAvv05m1XSi_#Uii!ss zOw)>vsL4(LQF~_@*1Epy$yyO#aAX`B(R3t~7zz{-}EF^`5!f zG|x;rQ+e;5K}4d9wkE zLvI2hX8X)ckeKCP329o#xMa;rmE>Vh2TKqzo}nA;-VYK!mdxN=ZKkk`e5`7ch)7c@ zZrne6(p^dAZ`5Z*z#=JeL*nu3fg~{bQLOTH?xrEL=s_?aIP$^R+-T<=py$$uKUM~} z&_CIOPW1aY&&po+D^7I0NI^#^ImY!Rq)5ByUA>-7^F2)yif#}N#?V|niYDLfGkGTn z=4SH*FV?T-#&=c}r0jLga=Je!$BG%`*Xc<302lZzA`dX zm}T{7UJ4E$aop%lR}iMxl#uOI5Zb?Ok*BN#zeWUVr9daSw94CX#&f>e^Rn|iiNhq6 z2^SZ4J!NTfaw>oT^N%U~gJ$_#D}nvsV}p3QI~!s-{kiozy!z9pml19bv~%U*Pv99+ zM>7$TVrVkq*7S#$_y}U+g5PB$UhrmgQyv6uih0TuMqbJkvdz*1Ez766-u+*Dz;=9( z5+d{W+B(8)DZgV;WoUH%jA;HC@c3tX>){7pEE1Dv3OIs-me?lJn03q6t77aV%U8X4 zoip3-(6i*WTCa@gJzC+x?}#Y?w4cNYTl>?o&i3A!ekRe4i+|`)PNOr7pW5a8K|>2Ddp{NKtz5hcZi6b$N`_mz#C&^Bm_r_gP2=dHvqM zBM+V9@Va)nN!yGIWf%E~n7s?6LAGM^P0X}U5+!h7FD(xli0Yq59GeZLYWBWVl2<3i zC~=c*Q6Psm`#HZ*3Vnu*k@sG55u?)MW2Z6sL&@2Z8OalK>WKH(icE(=pQk7G4vZcO zd*+Ci8PDXEdDEO}=wg^~mhE_APi~k&4C%8(5;aZ;6>bsaSTpXKpfKv4{pxe(-U^4C zzL`)wcn}^quD&|%C?0z@pjl3or0GSNGxS4@o&Twq@3K(9dDiUEi*fi^LBoc0%Cy3R zlnx?ga%|TEO)8}{G&5t?m3M0FrIzWGSzv)8Hq)r$@gL%ho6~b7p4~HXx>M8$!#+Ut zqldhip%GNNVv0m#x&2*Ouaz&cYdyxqx$HVkuk0?|kZ4i*Uz_}NKwWj1Xng?2Gk`QJ za^XUvF`!{)X0Ctlu)pw}8r`5Pd@PGHE;jbxrfG;^3N_T@{!TY zpWrI#1hHp~tn)vq4-t2$5OM=R)N^XO{Yp1U8k3)+T9F>ceUH$O&Ud9m!`oJqrHMn&p6j%9)^O*Chf%x%w+tf95z*Td z8L~E^5R+p~DSQ0esvFNZCK@XFKwYROKU%NgVrTm8x@@9d9$0>Hh0;Fb=S32gV1cdw z7PXv!{Y0ySP5)=bl*^sI884i-^LUx^)u`mCPhFt<1&i2`zRsWYOe(!c-7AQ=%E*-= zVp`L?JJ=TmhYp`owrKEv8`uZ`pGTtv9L?!>De{e!4dINTZr{hKW&lXg&5$Op2l0>R z{-0v2nJ}5kF zJRV&h0XPFhhVNaYXqe;HNq|o`x^Ua_B&KGW`i-B zB#IT1HWokC-lqQKF7yq%_OZZiJY>>&Q4;N$QCP>IKht%VBMx%wAc03sJYRWhDDSvl z-6nC$=9ojT3gH%2Hl6*p<*V*h5YbX&n+Mj>77g*62u^FFi9R}hYdA+laMtWif@3jD zkLRYhCK$A$oGWn{MFD1}5-SO(5ZkCee0e^_9y(^VvDIFXrzcsNZhakd2g0D4_`GE{ zik>=lUA*OzgGe)UE6@9q5+UjWg(KiKUW&;xb9m#DMD!iSz%|bBG9AD0*M9lRZLAa* z)}-}=#_-ZY^AfAId5szg6Du2{eN!2kJ@tu-L(4L%cQBoGX=PAwI-dDo_Q?-`7-Bf& zU-viK)<*$aPWR|~EdWJ(?%(_bMsUEFm3~bD%iD4tg;2m#C=K=Xp8)*k_H>QHLtX|3 z>3NM7Sb3TZ=kI^{AAYdO!usSc!tZu$dP6Ngv3au&#)(|8&ob{a9#tP-kB}OSVWqS$ z)T0|V%o3v_J^=@5%4|+uGLG)$C(KyLF+nNmaE*}L{eVa>7X{oLuSKGH6P}OmU1=(u z>j7a&eWBa)sh9o+g)E@kfVKEXH~G|!+)`-g^7iihayPQ75|z;fEx5t_ekY#sLVp8m zQMbZ#GAoD18tg7+vR(w-vWyVKCS-lPTC|7O)~7?vjF=3S#s)LTg6FqYNP!~z_B@@I z@zn?eCo$%W%H0jrSh`n*-_$fq%7WA)Z@bx7`UgfXu9Ayr5Tdz)W9u+iWU~u%Zo#TV z8S(OXrc-4Sg*}pk!*+&0O$s*yz}%m%l8snC)Z}2~eQxyUW*$~j;~qv2Hk#vD*dJR2 zQB0{$MWjpZ&fZ>Ky1mHS$e8#bkE328^Q^=JUWxr$H1@`=aPbmHKRs;mab)OwO`D9) zptbQ2vx1?|3{cd{A@>J{iDpVnZ)UGdi%+3pbuwsCUR@7n1Q!vK)htmM=^sjfh<@-- z4^irl+sV9U9)Rwk4BkxT{^pjP0Fm(j8&M60{zCIq1Ot^WRv>l)0g@9ex8(*PDHO^0 z?8Nb-`t{CJd2K(kP~qX?68!tBl@GBms21e_Q{?)Y4$A~r%JO@h$OlTo((Q!!Q@qG-x_@z3i0b2%rgqK{&m#MShPu zf1>br)&liF&prI!d4Ay=FK*~c^Ct|mWwGj#My%stPB0+PjQg)id*n4F*Q%INb_c7LNk z3OVA0G3dUL8b4?y3)V-GyKex|1gH%a;@J(a2fnQ#HLIn%T&foB8^^%rX&f4?_(5jA#ro(7&kDm>Q4z7q06S`H00pZ~&Y*>;=ME{I&j0^u z%2T`zL!Bl{b?e`+I5mVZ^(S-5;ztb+5B~?1e};`pBIxqy&N-{Lwd{PJK6d_4S6BCR zPm5ME8V1pns-eeWs0l#PPJdBl?Mg_`r0D7Wo0IqC^dx^eETx3_mtRVH>sF$J+y ziH5KkKBfwt6Q{xEqtT5K>$g;~b%B>IiY|8{q37+hb!alQXy`8=3QKMT5k^Ujr9 zCqmHQw=Skl5NB9PA^_XRA`8V(t3CI97m1u+HQbAYjpuCXHGnB_q90n=wBwEOPS+*2 zS-;2J8)a)~&>OP~P9Zwv$;IoNPr7S(=C7+uGAK-|1&{r>nxdm%#N4RK31*G-RQbU z=@)Z?!qc=Sa8*D&^?03{Li$}iaNlUFDkrjmat!EbZzm4*5(McDx@gc_tZEf7yyUM2DTy~QAHMN5V8VS=a;^@EejwSp$FG-}3+O{edosVgKu)bRht#Gp4Rbs9C!l zF5rN*K|FsR@E_o~rSu5yXS-voSsW&V`TBrgTi@fZtgKX>JBfu=khp*#>1Ue~HUSf` zfl)pFzc|2#uYM|_fsKjeV*Z4?4`B7F)|Upvzfa)3K5zg5pU+|243IMa*`u1%pn|95 z8ef$)E4J9Lz36NIIha;tJx)(UqjC7}p@|d0AOnM}Tj8Vi?^$nA%*@O#8~Q(fq>6JE zqJtIT@qkyn0Lr2FVMPx^yUZ3wUZkjzC{Sok6I^HA#FQ3UQ z3`oMY=r3Mue@G6^vq!L+>Ijh;&@WB-`P0~Q`8EP_ofYK|uvlQ=x!SWich&z8Ay%X6 z)Y?3UKr~fVjp`gVHIr^JMxtHQUWL-Yqw0+>jHo{?eJYIxR>$M9ek}H@kFfo}qyj;Q z>+H|>H(J!wLW#K03VL2Rl5Pe4lJ7S+K?~P`F);b&WOZR({F)u~IeD z*;VndU$x&%4%!Dcz~7_|`;QX)ti$53a2)ffKT?JWJRrKv`g9W1bx_^-h$CEOjUber z<;}GmJVPfr+zV(hHgMpCdl#}~riziFS^H&>_dWBjUusF%)^?_Awo)#C!b3asuKii4 z_|kA+4e0zAlkdM9V*)P(sR**Mc|~q+?!v*!iVBB=e4q@v8Wt;lQqQ$6Xea(~!ZGGv z*7EY0V=?^`ijGU1FBf_YFrkTgID3}*tcaj8{k)+&OrsD;N!pQp{@9||`M^RS#!*cF z)*etO4A**$1a(q&O&B zJb(XedN%~$cEhf$Q1N&<=Unn&cGHc9mY<*hrKV=k&DB|vz5oe^rypc6mG5Z;>YqLR zzue5Pj{t4OWH9ynyzDNJ{y6f%@|k7u**v7Bm)I=mUdshn@|j0rfRN-|(LBxGgzGub zCR>UDA*jPtzFTMZP_dQlqV|LBO8*VUuErtta`YMsOq9LbsQ!GbPd@A<(Qp|5o+JVA z7lbtTA#*pIB1;xxewz!%7fM=GEwQ;rAH1{4(CNUhe*oVT9C}dOxY3U2-@OpuA-YA$}J~=xCd~tE{pH_+H6Cr#FhTywi*zJ>UZBJI7I7hK0 zm_O0g18C$a^KC(xHAS|(5`}g&;c64*0H+6cO95^TlJhGGP>YFxiG1$O*jd!o>_sQ- z5Ekq?T%g2XtD*=KY&<%(zjF(o0e`^`esJ5kHvkcxUPD$v)jz*%4J-ngmE6fgsGlPCDw=!p%Hy)z_2t1Stq#h|gc2x%le&HeBsC zoD4v6@Wn%}ETYp_oZ*Df14X&(aXo?rK`9C#=s_TP{P&o}B>+RLlBf~%NA|w}f6+t1 zc7BuzDFBVMKtA$+<7TKDKqCWSIu9bh#L>W_rrGfHqj3wi3T0A8NG3tVKc zBXchNoh;v}E~H-~T>-%!_Bs}{+Z#mmDB(RgE;6ktOAUmc!}r{xg0-0tl!8#eb7~}d z2T%F8iype~#>Q<$3R7Y=e?#2JGr~04O}$p201bc6Db{lNo#)&D#t^k{s6N2NUf<|0{j+u$yREUVvVSs37yu z(!A+^SWB5j$Loyp0CsfI$BY^>1YoAyWRhXCmcSWSEn1~Je!-FN|Q)PNN%H9K|K2r%|kUFJ_@g{lalQl z#p7FG8vJ{&2rdwYN&0nKP^2SKUi4i_+T8I2@UY7A$Q)TMKs7@5e`5S9@b8O4r;iru zPr!C{R6K7AUI0@I$p4G!kO}xPP8DAZh2Q-@Hc1%N6~D3M)P8$-&FqAI=)rv$? zrg=64^PBW;8+NSqtglq?l#Y%R4@crQFD2z&1j%lY?`3!#1{noo3hsNeP<8Z3^8cH@ zpU)4vZ=<8xkp6Wbz(Nz((n@9_8vt_S)!BU1J%98cw`YB$$CCQEN@<~@Z{Sel;FM@Z z`|(5)HPODo9|nycyzPQBDv6YsM-XN+tO%(a{YT1b@jYleybVY8)U@)$VsDcLaCj# z(iJQ$EUb{b%nT$O7I|&h)7Z)?{OUrvRz502hllSOzwqt-3`d8N1~$ZO!4<*FM^=M_ z;-Ux(M>?Rg2IAdL1JjGw0=~&&WOOX4VdZ?2U^|*i_=_2vjQozMv(MVzC-fA2viSPL zP0EV|jNhx=n$}Nhfo5HYdd80U(d`IG4b_f3$^W~pm4lTlvgJVhZjJ{AY^=xEoENEH z?lIW!a)05gg7$y)!|}!?hfM~ZjbCrSvt?I+o_i+L+wLBf@Rn!28k2XoL9N(j3}|C; zH1t{v=UOmFA-FJ}><}YgYAKF$1QUM#h}PlqP^0;y-m58lhD@m(la!^SBqU0l379g$1NP)$%VCzyl&g zPHxION9Lme!ZWPqrl)^TLss0UJ>>H zG{Uyq$jw3$o4q2?PJU4g!l=(0=G&@Tv`lplXD|Wlc?0)`?avW` zS^)EM67nAJuM6u39weDP5cXX0J6lCpfXX_8u)ak2B1F5z4gF@LG262;Gmk-Z7HoK#F8*B=q}CQ?bYfy_NfF5Tl2rhl>aEp& zxWMj9*YbGNq8pMc?rSA^0WTF2E9v-dF2QFX0o?jUV)e%d0Z(C6fLcgLBtUq8hprmEC8X&v_Tkdr-y_W z7X)~$_+J^`YfKlXF*2m;Sp}V)a^)ArMri!}=aCmr(K%yHsiA|qCR79?eX6AlZ zeQM_66vDiuyt`3`}g;dYa_-&?$ZXA5$_N)Iuvx>3Qs=K zIAPN1JQhq_7oPE!MJ=ekIPH-+L<<`KiVqZw04_pt4BID!fD zN>cIX;zxKVhJ$!bo<`Fdex)RFkFb;)NYOs2r#=CAb&KjO$1MW$m#jm}@zZu~JgTr`eHs(moI9hI~~`&6Wt~`SsBd9zxzo5G-EPp1+L$x1q`F z6&zmspP^)=o_ljmZ`p&eu_fKfU7$B-MU5Kr9y>>wc9TlBJPr@Vx+0grlD?Mt@jpJ| zK1DFSN}Fn41M|`e*J&A2v=}b)J;n0ozFdvBF;HMki#Y#WVmd-aMJP26^g|Mhq?Qg~ z?5e2&x1bVl8b=dpU0s($r;n>VtXVxX3E%Ua)O-~aQNV?QGVrK>@pZ?PzwvdKY3@#{ z3*V4$!7?v`5;=845?6%6>ha8(EUS70I1S^2Qjg;G-89qZ63R*dY=^s)!WFbftTjX= zfDyQ&tm*8*=FMR2rfku~w2ohL_4s=kj&T+rG3T%svMO0zRG_#o-CIJRRmHP*a<9Pb zBAA0&C-AoYTAb0P09dSSJf|<=yC>VZert-7RqH+TiAikC9T`1Pr z_NNPJ1E`^+0UlyaHps2Ta*F9BG-)YP(2X-12NvPVhw8u=XwbylwftDZ>M=Ib>!F+C zO8dN-0FKHRF(&MK0a9`uwjE*#ia+)d(Td*jjD5|igAl!=OH=uvlBOc&=UE~(LqWNl zw#22YYJ{8NZtDvW`(c%-3^TTTLBZsV=}Eyu*W;$8q^o3DJMkpe?PEJvGGtxjHkQ_iK#o`C-EBI1r7OM^@dy|D zU!(G0w?E=_+5bRxk`L61!(4k~8R}iyBQP0go@VZBn7O-mMm@rhzzq~}3y;DNS&L$# zAjA$iTX*vKY-_I4m%wI?s&qt8^*suh2{GUezoe`bMbUGMNHejtruZ_Nff*Rscx}R) zQ-g675$zbD4C-ym?y!GGC)=5VO91hp!tO{P_H~89F1HHX=2WdG<(Qs$CTTLjGlXtu z*CzFHka?oU?8&K~?VnUFZMSN1w$wPbO9`{nNdOt3zEDOKCi*LGPmNFKJEE#MIYKBfz=>^CrO1P}M6x;FnB?Hl`0TM%g)LzW z5O1*6ZKocq0)+4a*7cA>|Eirjrf;v%xVBjMdf9clm9Y9^rj~0(IA@@4h z@BdE(CYu{<0E#g-e>65eUidtmkV6vs;~OlOjO%(A=e<@`_Gvj!L6hBD+KI|*^oi`N zV%<;;cm@b+a6&xA^T++aaDoEo-@y%B4nl{Bhd*)GKSt@+#hb*x-KIiW1*SvwGeKy` z8}dcty#h5s$>--si7yjT4?@3^iQz=bDF)TQJ=iF8w%Cg~`^q0moPlzOtjL~FL{M@T z!f2|^i~t#SDVg&g9GDorm>lQ3Xk5Plh%`-29jCX{3^52w^J8c?%+GZ^5Xijjsh5_P zuhfiRXaGC=^`k!gUpF5MNU{WK@Yr8B593choR3+Ed4e6MAna2nueUCZ1<&yOs$C_N zER*#i*JC0aGT>mu0Eb_|+6G{`b?QAd@n&xQBx*}6((8@Crh@2Iaw7JH{z`HBvt=7VFJbb#JgsRdX_;==$V~xag>~5lXz>M7efU_B zFI!fOO^io2Lwp1lBZa=Oar3JHn#uT+?fG~2@WvJs%Ckw%m{`<4%34nYqs0_4oz>|Z z*Ob}x7j<|)z6T}<0#ix)6Y_;}!NU61{GkX+ml5e*u?^MTvQvvNuRlm??Y(A!-#AFv z&A|9=jhho5wHvSQiF{1R)Z0&I)o9QiCg@N7OY^=3T;#q9Do3@eEdqpSzM&Z=AdD-c zR!r+9|5`guPc5ze?u-88)X>H#r(Ew*=MI+Ng;)xEW2|7PRZ_h%6+cYIm)o`e%J?EI z-5&~|{ri9$IEhIRq3C$?!xnMvowdTi?01Nv^1-tY$>> z+fcaq=5UDZlz9G`2^*=;m)+Th6LKa16qc_A)c8js#gXahrT^BB15EK#fT`Egw*@4= z_v&s=r%#++Yv$aFdJU-9eU{^(=u*NidaA+Xc!Ao{23KNk)aoEK$!%2nW&-pyIs^qP z(89)KHGH5jij1i@n9E$EFh~@nL=^@Gk>U+4oO>-}a06(aD#cB*hXu-qiC{&J%lBe3 zKiQ-~bwfb}v6(%5R#jFyL6Y`*ccT=9Z}Q6nqH?x(4OYy2$Aa+6J_;Fje*qHh1{7(^ z;!vl>_MF?!H#2mTFMsK8cLRgHgaWMp(1fzOomZ-d=J#N^~; zhlBO*DC*7Tf`WoX9&0k=fniLTQC(bo?~5ScmG_HJz`&08ie-u~6Ec2x z;bhB$jSt|`j#txt0)PJ(?;}--Hu_l*$k<1+nhjO(&(yB*rZ53uy}1@D_%5Oq zv}73iHN{{I8Pk4+N+7ALTTDSn*i+vxCnHo!@9T;H?b8^NpI-gTH8DYWcr)4=zQgBpL}H}&y6?)XkwS0PJ1qfy}s#Xt5Kqmh=N^-dGen@1_Os0 zrpsGf$q3PoLRH;TT8K2u;K+Q>dCeQWD1v3|{8j@Bz>wd|?5O`M|4uOZSJWr_Ypd>z z7oo%Rlc}-wVoWbDGvGh5L`A+i_{XruH=Fh3^y9QWTOwt_qyiMztOj4>&;?5fekbrj z@_UD4(|xE3tc8S07#tk@aqsw}|&~fDL;s>Cy zs(I*&CFCR01T8y1I<+kH>9k9wCM@rDSV04On`ff1<# z+{9ftPQfFv#5SKnuE{#B(LOnupxRv#cJlBu97>yoWU?-Uth_FYEOZDZVaqdjKOSf2IVwN&) zJ|^YuUy4lY#|iabk(e0Q%k&DQTQoFybAid0`9wRyE8Ed%w+s8EzzqkZNxh`TcBxAE zRl^mWQDU8!@b$>iNB4=V{40<-GEgx~4?$Zlcbe}xS;TeH$FjD;E&;e5s zqDN^kNY6Dg7**W;ZGx$S7GidB7cgGutGA)5uFurjTi-OjE(SDkYX_=PW}Mtr(H@X^ z9{I&31}6iGm##0NxQt&(Hv4?*19hW%xme{k>%w?^Zmt5; zcmJj{;$=5*f_M~L9$;a?Lk?~9j z*b5%}sJMfyFKfO{Nt19HgiT^$!kb|lr6oIIRjduEU8xL zjBp`rVH*M^{PcRM!eY;Y5w2`b9n~Dl?{xOPz8tLU<0Ym5OekL1M}Cg_L`P3jpwfFK zqEp3}&^!UWQVx3PDTJ8GfSpv9L*%Y*JT2$(@H1vGV756{Nqefw~RHFG-byZw=p8B zj+^$c%F15|Io=8mUGYuD6dnn+n1}AI7q_-bgD9b5g%IP*bfA(4#sjh~m- zJuCLKnPW_(7-=>>FK8F~eya?06O5RXR1c^>9rPd+7#D6HTre{$xy9!@eVhFzYEFY$ z=!EaOfIpq^0~%RE0Q3Zvi@c8gq~#_}uJ<4*MO<$z(U-i1x(jlOZZLO@JRy7>5gmii z9akp$#%!FXbt?v)IabyY*oEe;kMKBz~ud9A%(H?;| z}jYO<)?-UeZ{e3<0rCIeJtG9X6Pm-jHHw68~IhBZZm_Qm7T3i6SkKaK0-Wqp|(_4 zRS@RPO6J3hX=C2PhUMw$ z9rxlm>x)b6uy4=$`dUwgeEp}rJ#e7~aHnp11jo*r1hmtPWx1>;s-@JU-=lAf7P#lR z9+A`O&m|)%aPTz5$kxAi>jNur->xUB?&X@)?J9$r*D}`@T$bwWRnVO6_?3zVrFSe^ zWk10(EeK9pD2Wv}+8%$(Qu!I==Dze$bMJU(N+zDmTAFhx#jr_+r!nby{Z=W5FZePJ z>7Nhb&zZ2uFVwW$lpM@zOZ5*-`uqELI$Pt=$<bGtli zQqh{h0m$)(}Ca$c+y%uoqonBtT^LsSs)Xx5mGOAhL%^?T|9)R;Td8v2&n-TB1bxm zWpSTS``^y9*(X(e(y+R`#q6(X#HjqjSl0MPFsPa%7a^|nor;nYRXsa4`tScw;r(04 z?=H1IR#=#Ip6BwF%E+IO?>7Q6{Z3~~S65eKoU%5iPf+slo=Jr)`)9@u$>%IB9*INA z8iBxNVH!uzt!Jo#C5G-tVNfRZwQuG2&TyFz)HJ^O2nU}n`_6#px9)rbuF%%RE&lzj zK=TxZ;zVj=xaPZp%!qcw!eYts9b&Q`L_S>bCU(c-RfN=VS7`M=>~|jQ zJqAD5^j(BUTA~?4*Hoi*Z7NH`c<$j-cyrotoPvH!OamaS4aj?cgcTDetZ)wxmyawh z?H0BQVr=#d}cJYSRzklZDX{PjE>C`lm3!;kbh0lg5LxhM^-2p_4N?CR<-13bl zBGT=Cpa{Y9Yye}>&A(MW@ydMWNmNFL^}k%=8x^Or($e@Q_p>QpSX!L5`F)i^&9N)s z(Hf7V$TX8)Beb~$Up*ggc=hS~STuC_Y?VZ#P6A9H6$}a_G5nPoe{}BCBI^|9%G(5} zq51Ma4sfeVJtxnp=-m>1@_`f1M>4;YABu0VCEkFDR_OFN8(AF{EIyMO9UEmC`c5!r zu{89L?|$Y%jGW=l3Z;)iRO0eJuDVhVWLp|RrFkT=g^g((W$7Y2wnJYSu<f?oGdjc1p_J zBaq#Q1$(?W4!9alX?;oJV$-9lFg!h?kAppwKHIf2zc|KpB5!x-V7V)d_$d)FamlW( z>uTIbKQK@usG8FFAD8-N0AXz?gQuB(WUS2q|K?LVOp%SA{`ea0f-qp+$Rc%Xlu}|~ zmL(}0Q13c7fd1PG0$Pa_dF=Vba?`mfP?zls!2wmsb6GzkQ1Vehqu3xf zni|C!J~rPzOjw1=)6McSe-1i9#LX`%;;{O3zFAaOJSU$aJc~l5Yd^rHM$GtIEB@@} z-N#yakUA=$l#g|}W^|6*TpjkG2nbADEO9+Y63%yfE_^X?#@BDUejoPrr6xOyjQ1Ytum#JrV$`;|H@kE@I(#Ujr&ce*}^_Ve~$r{`KG) z2oTnLh{P*DNjF12`w{bWC&dhWrVcv_jj}c9{4q7N9{?9kY#hZ#QwS)uWzucye>>~eTe?p>PZ<$R~urzG5 zqprUtR+Nz(YxEo2&H8HbuU}V`aPBK2}R)yO9Vy#7GO}K4QcGl`j_( z%_#_Alc}K)vJCsW-RTg+sk2>2?jzwKq*rbmwAAl$1>OPZ+~wR*p!oR1cpU4G_x;CJ zg2C{*JKD$*u(8fXkV{GdewS?W`nDwu_#5~1TU`Zc^O_Fe{ncY|jkfHr|-zrJ7kMgb#-3rW!Nd zWEdUl%}?f}1+3unI&Q|=`gKlQw^%;x+&YnwmJaGq;Td{s@IOYUKH}J>W@c<;rS>b` zSVh{Gnf#aTC3W^|T9(DNvL9~op-;)lU8?&&u&BdlGpamdn8Zf__XztPizp(^5P>!~ zGJRpz1h6Xc`6G*kURWTzF$Z>nyDeOz0>qGII~Dg%+g9)>ykiOM&jwqBndvdcoi_Sc z&B&I6%(A4yHV-w~If0D`(EJ$D^K{`e-0;Oq@G)RAF`t$9{XoT8iAN@jmA>HV;I7;i z^#LmO%K>4)G(U~36aW=$+t&GzXXf?4h-E;6NXORZFawmJ=QTMVCa_!EmhYBLUfQ-~ z99>$9PiKd=LxlJ^VvRu~k}j{8^kl^B0cES?~bk z5+qDpxN2) z|K4HoE0TF^rO6PtFBn+%zjA=Dvym`>`*z+%yvPu?OuOtPw&wJey42@qDxltnnSvF) z&pi^3_nPeqW8#ewqlTmR;r$RJQ!9Ji5d$P)??K9oRycpc;(#Ma#NpL=@UqofE`yEJ zlq5$FSl(qvyX0?E{VfEL(pNU;D1WjKSgQLLdnjIE&TZtp{9|Pb>$XG7F|cBTPH;s# zkluD2y)+;zy-=$;#tg!$vW;(-2f%aZzj?I{=Iw6k}F!(~_ zUj(YpNbbU#jLX?QFB1U|TibHQ6R-$pAZ;}gzwdL`wWOVoj8rF8>+{FQ*6x=)&HwcR zc)$w`cZfhWeeqekFTzX5=KAYuPs@JFwBBHL8%gia^HvU70V(uK?xZ2pZ%+c;#;N2n z!^e2HT4|UkvCZ3UH%DG=+r(epHpy_DmcFT~D$g1*l$$b@Cg!T2wp^M8#CeY6%f&2H zq#QiWO;&KJOLm)-kPxB#(AbK=THk1NLq}0MNv|)c*c(VDb#<8mOm}&*Bb>Cl_^=Pn z<9{@{f6Z`!*1|%opXB*EfQBB=v~IpIUKS{@cq*ttSNsm?MZu;<7jSBM;oJ_gAG4$k zR3Djz-db8aytysS6L)M6^K>-6q5D^IhMFx=)kVZjL zK!zER2I=l@L6L5dkdU0A1f}{i9$~rcuJ8-g+ojW_ z#~J(HZ(t2`(>tXU$rf$1b-^fS z(E_11Ffn89c7{v0AxK65Qk4-y z`(bshHu&k0N9z07XuZEriTJ~GTNV~~i+oYZLY;J{(-D9q;MFkms=qUM{ zhNUNZ_)5H3_PsVAfxAV7J;x8KrTb@ETE7VNA51=B_YCSf8A&}k;y&hK*f{lBq$4&i zmfVA5IOv)8aziQpAnpYkA+u3po5fzHoFkN5x4@4Bfv>OO-*?NM0MrvYck`J@13czj z4pz!2;h`KH9Q(tsJ1@>FeBEVs4W@;<(soU8kGZFKDsp;^aBB9sO!EkB558aJrYR~d zUYVQeWS6LVx;*pIt>IN>IMRFA`&|5;@Jc zE%LtE(|u0FL3iXLD*JoTm3SH*9>_srVY5EXaTjgnFMx{mSY=;0VP{PgWKOPAvpmvj z+}r%<3GE2uz_D~L2xCg!o>HC_-WDVwvbjp@j|vv^mnt)oe=Pfw%~*Q+16Ip zIy1^OD#uYB+;A|gl?M{@f9edzpAl9AoG$)-e!4kQsd!=y*uj97XANrWe8`Lto{Wc8 zn(j5YYVt%oR?DFPlby>E*^gFMty^-R%3S#9a_553J>VvMHgzpAPKx-t*C+UYcnROJ z-OV54B_8i~_j}0+M&$Woc5zsc&pKRVLnHmk`KSB8R(x7x6MSTZO>YGwzCg8*l~zd0 zrjC>Bd0a71+Y?SNk*k;vjvC|%0R0{V{{^aHvvhs=Ar*L;B?7?KFybpp-lf7vQmS-B zR}UA{GIa#W&qx4FzR%3IUb&Rp)$MC3x4LFwMs5g}Z=HlChq^ghFR!xzJ6C9vP~e~q zjh+c;H$#cUtoptmp$64LZ+DtaZJz9Kok?WKjuG#V@C7_WDJ5);qg!k)@4Al?I+N1~ zp+$0f*Y@4_e9uwv>pa`FvHb}A-q-GVcHOD2OLB27IQ#x3d@r|}8Izx!Q-p2j0g9rz zwYB))({@k`T7{T&0))DI!9QRa;i~b;bXv=E{o(F)pG6&?_g}K650$l~=C2XG3>7C0)`w-d!?&NpkPSHqe zo7j_tepe-8$<2_+R(O--nc%ki@WLea`;+RYiE#+%-&#%x(ZB5N?$+5xynWkM?yvW( z(nAG9-2KXseqKW5zkZ+mSOm@{<^=$qYnRfpS^;pc&5?*L?bNpM^DRd7rd6Wdjy7msnv){ctzV^&KLIeb9aFDKs*w3G!eUAqs(JpSc zAX-I9WWn2S+VPL>(|X_t9}IO)r-cd?*=v6eAd3K`pXtdDJ{TJ$H033U|UIP1mAUd`A1}H!2 zYTi@{v?VgU18@5#QSM+2n`k;u*`J@I5r_?DKOK;Gh35 z>#|cmY7*duhdm%l8iy|nKCZ1q=UCMo6)F~4H%3Zd0cz6VoAx9Jb3lbZT{u`!t8JwL#Q}wPuMLR z`c9SaFXjZVc@Pp3Mpy{gUBO^>;(6Kn!xLv48yj$rldRqI#@0Jkxn&x==YGBa7KpFN z`a8ynZf7T1_PT zxkm4FR6_YqQKIqU6%y}SBuyM+Dq#fEdl0wFrM~8sjdMJc)biNN;L1HbbfbS6m5^Zi zm$v@BUFA4jTwD!~0;!Li7N=LrCMEq4APQa6Ig}Ot ziX*>Xi9E%xJ~=+X#4Eo;kGhKjm6IHvW6Kp{@NHoz?|I zjzdP&!4s4W-9i;%Fg@<|UjsOc1clGrS2t2Ft^5Q=A zBDklaz^?9K?5aKwXRUfO#QIb#s(f}HgsXf`3+?U#ObYVPOiEp2A`F|~N)i)7b6LK5e&7TFsJN;6Y<&&rBou+>r{Kfcr!+>=}Nf)<2K@$L1 zsKLE|oN7@(Us~mXg3f>(%PG+mWp5dThaPnpL^2b%0H5}&gLi+w(o{nXFqGD-iy$!! zrc_(4-jw@yw#|F;;yvqczUxPiz-a*X&-^bjUM2F66Lc;8%f`Y2fEb|G{S=&N{SpN! zoUxH0?^X0=K45*P_n8d5@a-;^_by7zdS^e=ll~5bil75UqQm#)-7~0g@xpDTWZ>%k zbYMIGWi6=}jVvvBYD0(WJnL4xP)SW~Bn3E;^DihS0zrk`&nkv(81aCONKf6KHo}PK zw*qxN)Mo1oZSzsUYmWK~%o{58734i!7H$Nmefr-Tw#Vy_ZgCj*F#MOL$dEgM)dsC; zgLz_Is`QkU`rQk&fLAa53v)mg^6$o*;)tSD+&X_|OO)Vvui6n@mC_sek7i$pXuUR(RLDQmgwvp@RZ$`%@U zCMG69ND1;g>TiV-fu^pHwS4lw^q1{m<5vEpj~@IgE}wgZ+LG$QtC%eT8y2+@i-pC+ zz!n80!~IJ#kh8uIu1|@_p$ZFEF5iBlJ$gGbCdS}j9c$Pk&RX3`?Yg_Pt!?o`cIOSf zsj4VSi}Lq>?_UZEDhrN$C&0*NOCD&k&xK&)qL`p^Qew&T-l_Q>ddOe8N=(e_ zype7_)drqRVrZ?Wr`Po6VPHBygMWYbZ!9PY_hzl#{g8b#udbLo)_SUeVj>yk*UY}5 zqZ;bBQfR<0?R$T^iIxRzR}K&cI?n#Yl8NB$M7nB;`qwxg6^@#s-e8;>E%D_po-RO_ zNC=%A+6GPoLXF2FJ&r>^;4lC1M`uB%yMGW7tLf6rJq=Qdv_(*cFZ|w^ORG)Z#nU@aY5J7hT`@TSWmHlU^++2ZW>f!sKkZjl@3IMx zk=+n6Wnqq3lr(ljKCS%#{6D6s2*BI9$3PkWw?+plA<=p&oR2C;O{BlKLi;BX4l>2S z+U^E*kRh(?O@LvaxWB*r_geBV8Ch6x*OD6>8NF@bw*DqY?_>w^=6`@r^>`oz8CzXe zbetuA3A8pq_3aA`%sc|Mq+TKh?)$X6_yXMmrDdBwm{k!c zmJ(i%{TDXgDzN_fS@NK@`#WnBmXtkZwc9LHRbk-KgCl>TSz0S&ApJY-!8MpHgBAin zk;!|Ed&l1b=`~b1Y!DaFYFGRhC2kU7qAr$q65duY4~hxj*>NyU8!9%WzhWmEkKZ=w8PX$C>Tiggq?R4PFeq$nB7jk@D9C3W9 zi7?$eY2v{g4`jJQ&#bfuW=T|O5fMCGH&OcQo6$s|~r|HKS z&xL9&AbsuhBKW6d>N9|xDq*}kqO|LH9y`U!&7T9w&-8YKq8u+6whm<(U;As#8%z_Igvl26BWVoPQQ%8J{QG$cs5=I zdOUr5;raq2{=X^jI?3+0K-Qo&mT`{*^PIDq1p~X|-@s_*+rQ8N@f8a28}w6cm}K3m z82k{bD@ydI4r*UPl^9P!{KSAcgXDUBx*Oc8RdoI2N55c77rEDb`oS{7_n({z-@5et zU-EtLa8jp*jM$(x$kR?0fSTQu!~eqz*5d=@diA=I96C=01RLOGoNOpCcKi`2it#-d z?@gR$6b0`*7m`Hp<;9PO0wOc9TBYaZgcW}N`ip=2^-0pMz7QaN`pW(A3U$w;?FPq7 zikXkt|2DCgKEO`yrPxZNMW}uglY87Iw0jAo$T~z;zYX~Z2I{!wnh|jMl4dV4^&^BJN4Wd@B6@MTxFoG zLOB9@+CMDM>pT$G@#$n=vHb%%{7=6HzO4p)+bDlo5IUpw4o9(~(4NFIXdf2~`e$sK z#W>F4zK{eVj`+R}s%onJ3e~r%2Z(N{va;dp=BafQn9zIw4DTN%jKKweLOFP~N(z8| zhqFEm{bo=)*w*mgS#|Vfxd_OF&fW~aPRr(tGH~KZ(d$3&?fd&PG@vtVsxef}u(i3F z{h8#KEcvp(1@ve8{=Bk^1smzu&X|U&e^P_*{Fd6mi>ZG;MTttl*^bIv2UzyqbJ^EO zz+GR#Mm;En{-xdODDRS4*{Dt6MX}0_7Uq9|K}=Np$BvGoa$2g*rd#MYUuJ<@b6T{u zKx=LUfCx|$xvLuD+E-BWq`%`Q4nF=uh4#uhX?y#UYxk_`9WpIF4@Sg^oBNw6(VzOa z-vR#zybN$fysaZiRsTydBO@v3XBl1aAx2K9s+Bg#e|QscpQdPfe2Tf+N;I zLnzJ3uEdfUnCOo{HD+!1tNpJx7oiN6ztKS@h*m;c1>kvpmX$DfCkEb*ctkoTrtJ&f z2`0Xqh1zp*k-r!Y`_lh~;rOE{VQxTD5tbeE>}c=r56qsa+ral26Od3@0EyePd1@Sb zz*HIgzUM_{Dr{{y#bfY)`QLa93@8G+2Y4Qk0){xhiA5_SLlF?3SQ6Df z8aRpI085jumr9kv|D*u!^x8Tv7wQLtaFRXt5f{af1qEdt9N<`Ydf&T-9oPO2B4|7M zr!>8%QIX`@rWh+me0o4g{sR14=h5QB1x6C(75N~R_+=Vc$Gva1Cg2OSadF3;=cwpk zZVeeGs~Cb&ilrdh2oHU&?$q(qf~Or=Fn;52HLFyM#u_(~hD41~do&4SIKPPL#F~ zyt=V-*u{^!{abjTd1o3_R8Tzzs(w^lCQ+y~#+TsI-@Ft0D_`7C;iNt9x0rrx6#1^y zo#i^&7Da40lZR(5Y1{3v$KIx8}5^zd!v;ucfYc zm3#V`xlX>#-hUu?r{b)V^q%8}%c0v7p?k`#WJ{$oDG5FY7R=Eq=B^pDPtpe#o?9|1kLOIS{P=rU7p^v%B3x{e-O<(erey(5u_4Yuy=;r%ZOujA@x7A`5IBjwi!CP2Y7}je7jH z>^?EFQYizO&sC&+qP^cy`)f}`4uSkXcP4S7R{^@|z;4D#HF%?;@NdW=5`i+x2>&+; zzdrfLV6kwT8M{=Ob7)5N3*4esW8ZvW`bOZ+(k`)-u?~A2%q-t8PzWu0O}~6jYKc6r z;g~v9nS=8Tad+22>FaRN>z7m*7ZKMjYd1eA=e#@DjRk#wic)XyTtOcB`%*7le!<1j z9S7SqZM7WS;XYp2N%exzdI*;=MdrXx3hpx`L^11SL7;>rPF~&{VI{=nb$5Ej>NJ;y z#tN@@iG9b^`sr?*l{C7n5^seF-@SV&6yf9+G3FCpQD|&nmS4Ko+&?O`1MifO2VLE-i}stFN(Mp~ zRSR`DHN(gBmqRqgCU$IUa8>&f7j&Ej7Kh;jEmm4p-I|5J59ASG}Y zg2dd|G;cGe#LJYzgpw--4w8LK!8_k0oNicZB5v1YH8}*m(8`N750`#e?D1B5BVH@) z^_h6Hw4MF4`ZE*JQWgw)-A-1q0zV$)C31h@>z1{D#B|rVR$x7nWC&iNYBEBXJku6? zKJ>PdAU>qXFF3?9q z6VqHT!sfInc|But2g~L7G)EyLM0dqQR>F(66Vlm8J{XVIiI&z`-MfYpu;A<2L89VO zFvdIxW9Cm9V>vIr%WR-aCe{+~k~38HF@3A5i&HNBX2yM(16ImwwY}G?VIDvp#3cPE&a(B zu?@D9ECQUdt(p3G99>h=W+#2>xz()7$fVI5?S%v0i}LFmCv+Kc68fztg^KZXbLWzW z7&l|{qCRu#7APNq=Rzip-GTeXe@IX{*^)}Vsy6i6SxM&0`_~)O1xuBD^DI4Q_1azF z?#xB@U!MeOpFi$y3x#IxP`9Dc$cFB1+y@t1nnfsk&i}oUB@WbALs>M@!;EW zH}`L_JZ;byv(cZKwP{|;qYI^WG%Pkd@N4hUKI?u@1ZI^EeeTreB3X#d&=|8*M)44G zSS(gOx2V5TxuSt{rN045*Pgj8lMdnbB2~=D)IU)g+I?WB*YBaBBv7RO+_y(Fh=3L` zW%gu35lc$1|5S95-3w|JvlOu!t_THo#>e-gCE$;5C_a2=l7tLHzDiMUQ=07CQZX6H zFi|UchbUCu;czHvz6~cZL8Otuc10GG3$dwl3ag?8$ZA!Yj+@+glr3Z0S$Q1GQu|_l zjM=C-8|JSIaeJjxCVphyeV(YKT2rBmoVU=5OiR_luIrBI`^mP z!B)o+8$@-1$TJDY?D`>{#;ih1_r-Wb6b{+A$#|VCcU>3pL8|T;qkgE8bSit{QI%$V zvIg@A5gt{ezw=VLkgkxG{x9%np& zw-AV({#t{0#4Ye-aL#HmIqxwxfo;L}+3|A)xFgG{{<*xZ`eQYC4X4M>gjIi&A(F1Y zs0VQ|^0{5HQ*P#Am+&sTMzk>XVyFb3#)F6$#oLrhWFyS9D({25mUDB%_=>-UHnL_2 zC$iQmZf|@DWG8zG6EWKJnTW>m%pBY|q+%4{Htfo51jAgL^TL9~={6wnR_EZb$Tyx= zb%U}Q#^Z+cOGOSzl*FE_+NhMUDaUIK(Ise^6@^T-Hb;&Ih28b+V!-}LhjR5j$a-CK zqx>y?uXH*?$GnIQ5>LHOgQ1P@2F5*WJ?`Ir%mrkvl;2GXiW|5>nI$t#+b{FJNLocm z@l45ShtVX^bEly@+bpoSf0j32nDcRZ*@8{G!7ltEYw|Vsn(@lxZ3n)0Fw(&rWb*xd z5LX0|peFnTDf%qcj7$U)i=%LIoE@gVTt{#`LfB{T09#CWs(O^M;ajOxh1vJ@mYgMK&{M#d=sM;yoc>dF-B2b~dN0x#L9oj*` z3*<0MokJHT?*d`_bJK5OQng=S@Dl;lpT|kng%|D5arHlTgRsFU2+pYlcSP@;R;e>c zNGu&L)@zQhi+y88cGd(LzO9CQibLTH&40#GsTsoZHErD0CP1&9UM46&c)QawX64MA zXva6$G-N`2m5NHjDu%DyHTM{^pNX5M-KXcgmWE{fd|K27{x(Y;&vgsBz55Yr3vs*G zDJ!G*#phuIrP@i`lrN7WnY9Bl;9jBod@4>vVU4oE*pUWPd_0rZnVJY;7PZ&5CH8U( zI@!^b$|~^89SaqfssuWMoF->VD(nIk;ajm8Wqt<}F1d2!1pE5OL7F(XG;kz^xFnW> zcnPa>K5)qoh>K@xwgo7L$MmOdZ>Sg~O&hxAnJ*?QkU59Ex}JWUtX9Pkc4A#QPgTuZ zJYD6f$S{;zaARfBjVzIn#(=hQ<*LYr3QP$FXF_i?g@O;+dp^*Z6``(cNz4-8v4au+a zB~Z^E!kohMjTf1kVbzA~Te= zF-)Ot{7;TdD}@qTvskz6zX@%c#0r;lUlF^RLKX5viK*34T=e2jXAN{`{xnmY9j+S; z>r?IABrYf1!;$Pp z3Qe|Oh0P?HE6pUm3LCU6Z7FpR-qE#pCLbH;<#{eVPOCqO8)9DY0-Jh(LZnC|wxh6J zB^Bu~Bhg_buUqw+Fw>a+$W3Y%O3XAYNZlPyNy~_cd4LHls_>fiyl`Agy zcCr84PU>B@dZBC9r&meyWPbjR%8_2)wvHBd`VEK|toMeO-f0dxKWks#qAwq^^J zKaAv@nJ&D^{O(2#OeCOEtcnEMmv`D_^bU6VNQ}&kdpMz7N8x9W*=$w47yiqs7tLrZ2_DnncJu)mtDXqCDcCs^{Rn5^h1 zY|r?vkseWv*huildk=0D-j9(vPwW4#DaMaST9Jo-eb2Btg2r8okRxsC-49k}C5t<; z)e4iZ)pjhSgIUF*-yAK;r!dg+_rsfXI3=IHh#KxIOv@toPt1{V>@1O7sf8eh2Z%4> zK-p>4W@s8UFUYUS>K9GE|Ij3U9pU0u(wi7i9qgMIa)NwNT4T|XAUUqVtfw#Kq?rKw zoX#=^d$j0o@uAVO+;{2%X#5`TSu2Hrv#25S`fhX~i> zc|kP6MSlm-Sf84Cf6ZW)qxg&M0-BhZFbSK?_J~8XISp2eD-60|)F;2jH}NtbdNSu{ zvIZLPuENXpa>(v>H~$j`Tl&TQZAkpHaJ^8r#Si zNTb5IX)8d24*t1T5t4VCbcuG)d14_JcoOtmYb9B2XJz($9)43urO^`4bZT^nEGTwkVc+dd4k21jU#ND=1sG5P)}dv^n$On;)aKU7|VV=dTCUt!-&dtfqhtP^K(6(qtlm5zmL?r&ow-W7I4=L#IE$o=CQ<lnr zUx_??^8qrF_g32JjO7buJkDpOHN~@eu)gq(pi*j|Tl!Vmts7lEx7)KmSl|)CS~e(z;3|fBNIEi$&6(XBKPacB zmu9DWNQdOsl3fQNOswlmeffbOi1zg#)BMu zv`liIk++e|-rvHLsnK4ONC;k-4Taz4xZF5smduMIRG2(;VsV}Z(wfXTWPIItZ!0}f zDC?3zx2d{i_smtowaa62F#Qbs?iT5W#s!s#xz4D^YB^vcayVOcCayxAo8`;VTI2Sc zhB77i70$LV?%(S8lzW$3*L7vtH^0Wh6O<`*Zso}QnaL(Vl^?xt`s8EG7j*yXfC7U;c_vKs-%JMawfH62z5Da%xSHe3>27J!7 z|3!u|_=ya(JRIBR-&uRK{lhe4|DYtVYK6|6;`xTr!?;%u4fY9X;n`i!_sGr7!ZLn@ z$qa}{%HDEmckRw{@$6 z>K}!UP|}>c?PbT*7;U?wEpHZ{I6(DfOD~Hs;B&a22`eM%ycf zS%$cfe6PXl*=kW5+IXt#q}3R#*^jLvCFy;cYmJhA(_-8zCX8w&RZ-nlu)YgD-HCR* z90c!pT326pHjmS{Yq+kgOO3Zbgzoq}ZN8@OaV>0kE;(}S%+;rm;`vU2h&pY==*jD6 zR(<15vtYn12ATP;BRf;iE%$?P!hqSO&9>)@mXk$I6CXOTps7d_SSAwpIk^OuF*Mw4 zx0yuB6dCS03cXR&=uYP+nS3L9DfVh=#$%b+W(RYxdyLK^cGHc9Y?_a>{g#7Z?g2a3 z8LB+@FAceJZ1@sym+hV=tF$>Jqu~vgY^`$DpR;w%XwELx<)UO3I=QVOH6FIIZmu1LlgfXQSOI2@)Kxx8=1<>w zQoLOgYJV3VZPKq1ICF4z<-|fIx%hXN8^cS0e{kdQE78RBf0Fl~L_y{)(96q+wsCw0 zUlFy}xp||;W7TBt(iZO=W$T7+tL?;<>HCukZ}>*d`U(@hKHC)M5Z?A^(;#==Bb%^UuccpkAcov+hBgWEvHW{uPn;UcOYzDP2yQnwLUgt_u5~Q~0f6YU$=oX$3axKCB zpw#qaMZPUWNUADfjK6QU7C*Ezt$|1ABV!V7TwQHJ&AijoBaUKDbDkUGWrPKB10mD} zJY!k;G;^1$|0>L2d0!`EJ6V0~S+`JEzBOSrE_D3jY<3+u1>C-xu6@_FcRhAE%%Nb- z=Xv_#dLPL0t1}q09z=U-r6__(C1OYrv)L zvB;#-F3(G^6&eR*;`#gvvTU>KtWV87TU?v?{^sO(d0^suNLYwv2=bM!CFp?jny@?{C`Tuj?{6Q8zvb*{)v3H3e1DzjSGd-GiAtB*Z~37EDJ>qj%=ntlsfYf9)2OBs zaFXmVKiN*%HThsT778nERC&Mof zmAa(ez4HEM_$VH8#v#9RkDXQQa(2JGdFcfTO$D8Z>V!F&A@?1|vm>PeH98Pc!OxXX zYnR809-73rF%ptX&Rl8@_6T%wux>9N$yj_GxkCI#h~;n<%2dVKX;KzUpY^2Qt&@4= zntVe?yb5Gy5z*K&()LWYr06IPvVC{CNrYZvTdpOtXN@)b4TAlB!SR+>=t3N0Mk$jU zePmyAll1!^{Y@JcCOe&6&G}kLM%ucG?Y*)6(aklcKmpkyXW#FxBPEmN6&;p;dDUX#{66jb3N)!cG_)C*DnGVOLnGzBxM{Z##{U&4n5_3w0 zIn@&9=v@g~-Qa+}jAnKXj zk%Ht;YWYVMQNI=Lj z%h7$5j(*HU2>A`T)$GB7E1*O^Hq?%t(3ggenN61KNDI2DJz&=%U2EeQdYk)LY}l>;Xhd)G?D?8yTIjAwMpXMt-?07V6G3R# zZgua_hkH98{g}1)^cyP)LEv>*G$=Y>Gy2FNp=#^F(A(NJLST8~XsHywqUsLy>18WkMupg3k-$&aX323v1 zevrRNh0gqG3sKBSzgyR@PKYZy-nQG&LW?To!X-^NTL1}aCc_jqnbq_*17*YI1(2%k zH{p3V4@uyH{B4`Up{+s!7hdI#RjK=I((d7SS5Z1$y}_ zN*(pVo8^UTfzg(T{L|B~F6mOjKR4%CsGV)_eIr(+=lq$Esp+xW*3ij8`h1|4RQuc_ zR;lhwr!O@<)^~0VL9~@L2N{S?>L&r`6RCkz8){78SHxID1`j_~=m=I#aA@40vX|sypj(XKlYKXlCWs|fckRddBp~$1xV0F z&C_N@?BoK$iU?tZN`Jx7%`AGq@4JEUzBfHhRX2w!T>|fle-NBKuM7xhisJOc?u>|> zM~lf5w?Rcwjf&%=R&ZxQY~Go+;+M#5*v+L?W>_IaL&Ky7d57%$msj`mElx>x9Ouv~ z%+JdK-s!b{hYAtE;+!9%z3kFBMPf2Kj(#*$MC+NR_4c31nD4;JfkxV=ydqN*$=1SV zP>?^jb^<)9rG_i=l*Viq{X?SM@Ee>kqwG9QZer}GLA9wI^%Ji6wFoq z@IJk0SvXiX*-5V)$m%@d3RBXnHG8gSV|ZEeyQKTQtw=?Pyg&qF0?qhSs>WsIBTiHO zgNvKDCUu=}&Pc6~1!g!I+JJt4S_Xp4!yIb;=!nL7nj=?jg!8n z*GF;NA{m6VafKWu-nv2`o=`U@DBccSgT$z7m?J_zJD6`Vdre1rwX8k-lA6A3R8V!^`=#JJ z(y__}F2KZPcR?R*Q?9k$fO6M%AkV?7O`>s=hDi_>QH-rLP0=gz15eH*g5lcRJ2@@l z=sbxGtBAXcnYu12x*+NUHcW&g=i+DTMv8I&qqd51MXY8`>sz;~AOq6oYI>GwExjZ| zdBcls+@VN;5++q=>37N*$jhXAqx{8>p2rYY;f7T%)d>aKo&z>zk`SNOkA%qVuqmhe zeM?rnnX?L?ThcGopfu=WEOHN>qp1f5Fd>80Y^*)fxv2bD!?Wc;J>3V{)Ax!T9 zM{8ZJ)nH{(TZ$U%$60!e24b}R4-f#auIPZ>qCeNMfX*7NQUNPZE(Z}sANQtx4}}f0 z?lk_&8mEczn^%*;`EraOYqoa-({I&inv4_qQU+;1GQ$`Dba|Fdl=c(>t!zANf=AI4 zm|c@cZjeB;ylKP>MWjgtQuy75zYPIWq|Jezcq-BjE|@(jXGZ!lBYbG z{u=68RP{YbcckfJF1Z9A1E+|dd%n`MwbiH+`GL>e+vQ0mBJiMnInCI5`N>n1>1Hof zz#ySW%VW3$L|XsH6eP+HVl!WhBguon>hT%F4TI^YOKVUx7j9j_Dl#XJ*k7b7+MIF8 z;eanUdXPODMM-!A9+`R~r*%e=ETL#>%sOM1yVZ zY}Edi+-k&-Sl)W1MuO$C)CcyWWg4Auop>9vOn(hCoeP{^QPg2n$oOuZ;`ktn4I^dO z#q{usAP(gZ1?Zw?H&VPm3ldh0c{<3awvHhXyD1D&(~gN^f7R^&gx=#C?v7b(FIv|xR*h&n zgeT|~*7?!;>mDk-Wpo%^fv~;1dG!j3e9~>LV(4?lHzqFP%GX_N}q7AMLcO(=El2A*S#$W)Z=pQqlLO*At$@oT=$~> zMXT|#9KD$bb@8z&xH!h&HeZKVDpP(IR%;Kvuzf|Q*v!MJ_(=rs=c0vhsIJ@W<-&sV zb;vODf-`io)J*E7;w=Uc2Kc1WPfq)i0S9>6jNWl&-A?bqKHJ+B!xvM%7P55;;|FwU zt#Pa~L=aQ5_)UGifPr%!pD%>{$8>@BTd#e}@*T;v{Z$e8?S9X>O&(HZjQ2)CAAHnA zpN^HB-%oGvbru*!g%y;71nu8HqE0%vXU~fv7V#M(>&KIYZdb|kxg;vwiYC)vRH8e0 zde-&{IFqqnKZ~%ucZhpOZlg8IEEHEYK4TBowVV6Z8+10%yY$AhW1ua&?@aVMdrBb!%@E{gJO|m&cX*RxVeYwc9=CHo! zTjw?1eJz7@=RNO1ZmEE0v~eoJwJS?B`-R*Gjdl#Cr7c1TFTCbw3YP6oiQ2j-VgmC{ z$}>n|w861_A^*(W;1-ih|MzRIr|)_SYPh;n4DJLH(Dvoj_c}1MWn35dhM}rMGJjFk zZKFnwDu1!#z-pkHv^$p8&o3gLj(*e?hH9S0`W+g;geqwG7RbMb?yUJAgRwZkkBNFU zUB~c^^<+4CmKL#%9x9fUGOHRn>D8YsO}|*0>4dxjr^w{W!FPrz6o(|rcw?XGL%i;C zUKKInEtlm!uDou*NydiEj(T=(j5K0QScAn?_oKpML*{uh8Q3ie){umxAnrzCvk`uw zFP2pAuN7Za7+_ID6Qupykw&fcrzDdSVw!Hkog3A0Ku`cpEcfqha&Fr97fI+N@^7cd zdi)SZ*TU#?$;SH*v#2-YpBZ5_n>p5AK63K89d|j#(ZitHQ7K^2AV}_W`^l8(yhO`fI`w1Ma;@0GHiDh~PXClKaU!Iyid~ zVA}V8{>iy4K%-6e&J3b4NcszK+55^++#e0~!%!`T9MTtl<=@SBQl8NR#xv5u^qQv1 z1}J3*=&9!#lmMmvr1Z_-taoEob7*f=(>MSjqPM!X*%@)8HAeiy!|?u6K1B8?!a^(H zb_iwc)nsM3{RJ`Cm@4gzC{M=DQB{D+Z}LwVS;6poXj~cAN}sBruzi2@GZ$4pN`09< zCF20M)r2%8_Ly4N$NuYx0uMQd8n=7HTBb4uyM`UHG%U=Q+YJi5rlUXL;n|pJz0Rvt zSTzsS|Lne5HJVOlgp8g;bq9tc6=skH3>=m6*AQoRFQkA`Tth@z@u@ZnSpWW;Uj?oDg;NI{FnGZ?goPNqbp z@qP8yIz8#CLZ8cQ?;O&GM%8PB)69P5X4a_U1f!E2n$+?N1jxO@h=T%iDi1wsgoh!D z_YB(q;HM)+vh&~_b}uB7xm%cPgES#?=dhvk>LSlpsCsm+G0Sprrom%geEqI*;i8dw zaH0%@`Lfe`Mx*bu-u@k|2+7G%9;ui2dXh|ZewOF{aKKQIpleQcQL!z0KedpNHN0)LOQM}_wec2LTXu9S?<%&qEgsw8o>&vJn~n^vhf2NFv9vl(w;|AJE@Ur!9z zJhaoQ*W4yNr3EvJEpCRi2Y(qhzp0T(%J|4g_S>vBRNMM;wsfd;ZzZ8C=wKWZwaaJq zd#R5I*Z;iz@YZOPD=dWcjgu`Ad@Ngx5r%!WSmaXiC*!7w>+EB%#vgHK1{Z&=jC9KC zcsqZbWH{?=khD${bkZ@nHYeG(c{uCr;i*}N=VZFs zY%2qj%+X;>CA22j!$9vxJRy7^O&Dok27xvHb3$tm#%p7FQ-E2AyB{gp7Qf&0f?0xd z9>^EnAgbk(fkHHoa|uwu=dH1-E6H~8G-q;`7rk}m)cS8XY_y91!=>AamKy6o}7|gVHvT% zMshLIhGz~wkHS2?oi3+(;w=C)R4@?;@{0XfU zs8)u2Pn;*h=v%pjI>6Uj)CCPjwdFuKh*unGfBK8!^)@DJI_!ae{un%5KDh7Nb!~`_ zIMCPr`)3FHnt`-en^gRqyjmV2H}tRP9X3cmfA)NcLt_?QvH2ybAccBL;;U+4z!>tG zI~^Xt((|J6SP6bZh|@rQtib-3gK6}G&;2E3C*7wc@As`D6`KxC`W#nN9E87w<8#Jo zCOUuai0kU%M07#7Ll{f)2S=?+)MQ6?7K(UFLN?Ag8f|hco&R%-*tp=3MmJDmMAzZp z07>42G;N|4wT&HM2V1j`Dt9mr^|v4XqsYaYV7)faggT0rVM9_Ki|ms zUd-R;(yx}$RF?9nZ$iCb@kU0+)NJ3#i4k^sk>y%f3|Vtm7y;=Dmp5HjW;kcT;GR#W z3Dj@Ib6Xdv_|rR_tE{TEL$F5>$F(Wk-+n~f0%aN_2c3)PO@DO`tT8fGk_NM6)bygQ zQLzeU*7nar|Gct&fLU^n1R*Wrhi~bc(6rJmo7FNc_xpWjBX3%ka=l=ClEo&M_aZfd z*$Upjz3T7|5?|e_3`Nkm)k)VddghB3@LS(NEOrYHzwJWSfBIX7WZ==|n2oCc1U#zj zXc#qme_tu045inh_{nb-`+Z-(QlnWf5%I3M+IEve3#3igOKb+2OxE0f!(r0e`8?k! z5#}$YjAhwdd=Of=z-V@G;3=uiAZM05_>}1NgPOO9mfl4red+O}<8L`#`GS=2SK|%5 zzhzgScsT{9Cvn{4GWuk)`gD}jAr!aI3N?LK{wq-2ws-v=TAr3A`(`u~q|(q?jB zB3`t^d5~$Ku##_17`JiXeeQzE4^AqZZxT=lcm`Xo;#h48g#2>VgBpn)lOHmItJ^J= zf~qvt9L?D2_|HpB9qG} zgBoT3eX+sZHwe=; zVUjA`G&dNPEAt8Pqb#v5+zzpKN5n!$;r)EL=z6FpOdx50so$eci)YL( zQV1Ns7E-2`^yj;O`nFft-bn)&Oinuarv104V-d4{eJ($S+!vS2Zo<=tGhLoPDoOjk zdC<3G68jXPMG#rMgD^8L0pkRD-}aOg)%}up!-rXKTHaPc2!`gxlAUciM+=X~g3795 zEp&xCRz}AKtd8XFuf;IiYC}$d4RBWc;vOPXe@A@CvMc(?Jqje-L5lX$i^#l&j;4g+jki&tA$UuO7;g4G;3&vh`uTG?yfmgK8+{flW2N$f&G|BG|A(>n zj;Ff+|A0$LR8&YZBNRerHl^(C*n5wVy`AKetgK`^_TiY>`;hDz4%vIJ>^R2#InL?2 zs_*Z9-1mPz*U{y3-s82N&)4%cnjAu)@=#x%BmaAgp!@PNSYa_kOyE(iWgk)JJC^@W zt3A*zt%PrVM;VPXQcD9y&1AWm?J?Sl6#?3eN@W|@cQBJCfcJ^9@R5#YSq4VexzC31 zjAg4M!Upqoa^^dttF06MJCm5NY~$locyC2(oKY6RpEOW4vpaF*_#&Ju(#VB1J=23-D|0?5AvzcHq$S0Nf zp7+u7Hwzw&8R3N6R4vn&hY_oDRKnj}PQ zvu_2FGtMpd-!Gwms-W~~9d6|F%vBB#Qj~9^1&fBa-%+!i4lVJ>*U3( zqm}`QUo3=K1_1u-8zAGrC|(vz0gt&Oa(8}P23ahWK9R2k967!@cFVSM{to|pB0Rsb z|9ua1WP)~&KG~?|c4F8r<_nJj=^$tMemnvV)v=*!;)L|+m;PNzA6{mC&@+^Fg3!?t zty%*fb)HVOsw#EBUsvas_r)9}Je2WCk%FL?y7Mvc=wJocc#MexQK!)08Q*_z7Bh?G z>+9=$b$JMWS}L9jsFPR?=6(ZKS72TH@8kZ<2%rDI|8y09xVxz;z!Km5amHvf`p`ai!G8;a3B8#T*|*?`x;SKvj7 zdY%8V`WWCbBp@LEw^ysM@u3y}u#^$4yDi`X%=fnHisRZwo+LT5{Ps)#Ei~pQpH-md zgo2oex>@V0Y;aAj`p#(6w|2q(|5UIEo*$+(}@bc?yqv$7~7X7m?)Ib6KB zU_X{QRzi>}MOn}0n=1F3);E+Hh@N~OH2WD?Acp8@H|vfFKewBivM3RqkHlhpfZ`3m zske8X&JByJEq{F*Wt^|VtI*n+Txd=0qP%t4%$Om%dnBgKz68TQRRgMzd3yrFzJ z5%MG|7V?c(4imbsRF~Qg2{~!Cr3E*t-BhTV4FAMPX*1Z|G<<7!**#wrDbMe~)VDCc z$4GTMw_ROfP`lYtA1Rk;*Xc!UIad7bYa?1PWFfF#%eK}8Cmc1!qFTCcy=)YqZfL~f zUU2yHj1-Ga&4vmO0y3;AfcQ`a6CfYbuLeH?J=U%HZv9(?58Oq1dfop zUwj}0IGyuj^jZfnHu*tTs^jFRCPA8^;x9FndoK&D6wyurYmavxZ#eM3G4^ZW>p6%! zDP|=1Q+dkj#gt}g^-94ET=aPIADuWQ0}|UXhLcK~JdfAmBj9faIDNS;z}M%zm~Ii+ zroGNV{Gn@YU3F!IJPsaC0$z1XRc93yxU+K_ILaqV-@fx`yEF zuji5Om|(VwgK|hF^N7=pj^H-Cb34Trg4GgARqiXHmalCmo|l^on14WmGwtuo6c&UM z5$y-$r}n7Mwx~9C;ir2Aqwt-}SpB<`q~qbdzeSw-&ZsnK9NDpgz9%s#X@vfi&b#PU z>ZAc4i_JL-j2?X}#u@mr7W)m-dNnWDdxr>ag(V@IiPbkh$96d^o$#@i(?dg_>m*+T za!we8`=+l2*z@bMDpRywHW+&yksYK`SEDJa7mC|+8HCgiKdt-rdEaW>CtnrA=yd!!8< z4*M6>HR;k+Z%2)S0wc)ZJ<#E5zCeUw|^jUR& z=g{f7sECj}C^<#C?Nva0=^ovXDc=(Z=92WaO2xI7%35No&*e_%M2Mx|Z_%aXk8 zwn8pk{t2PN_J-n^%FJ*Q?5oii_AU=@cc!ho9a*vdtZ;?X!zAJimcB*Hnrqi;4elSj zG`h)!q~t)_P|$AH^4#T8ny0f06}mUjIve|B#&q1vcb`GjyV&0+d{Doq6)AsI?AK&^ zR%;vTN-R8?xh_u!7faH!V3b3|jHoR8^SqhUsu1;{%qvARcIC z_Sm!@#~C_NEeoG8Km-2m8b$fy7!+!u{6-2RVe)q&0_`>%v^^)tcEj2c$8)Cg z7cv%~m1F6#C{qj8_fKYAlBep84GMS@T)q7^c~Dp-C+eEAT~PjFT*;YZs;*+w!K%vO z&f5{)fQnFchEx5FvSTVP#;&XWVsaF{b$SV(eZ{&jndA(%Q3>V#$Pf#HruK-4;eee* zC~W!n!V{pKcdxaVZ1Z!$bUCFG6B=}#YFyPBCawTPjg>d)7|Pk_2T?*gt8-Ta zay2fETXFze4Pr~}>%wpekQE$yqoaK+(>(ooo;;O`ajSNo7S>2%nj)x))F4{F)LXk; zuI|)^;<%~DzqBYR-+foox>wZtMAF~M0w1C(G?KM|Vuyn7xKGlL_W41RnnLDYyd{WOW`ERn|Hm+N;8xquS9J6#y}tY^)w$n|n6XPu z$`4;MHgh7>8Wtr=uzt##yZ^R|Hu=6f@Ki?1+ym+~b)fNj4p?_j0xy9>X+L zR~)(4tmahWla5|MaXoH0%=wq3WvezVjQeJ8tB8YVl2S8POTQev!%y1S5+KsnO?y0< z%^8<%#K^2%>(P^M#k`rOeI9$uh$N?_HR99UxSoDWaE$T=$ZH-vlb$=5-4_)9jp0yH zroQFneBD}0oKQgQbAfbV(n$(qETInY=-{%c zfvJPcqC!TNGIH=kjyH2^dujY7EP@H)m0yAwN_GOCH{viy9H@GNqq!&IuO6lQ+p$+~ zYm=O(x?~&6f|YvJdQ015GnIu(r1V-5c+@YPlIujxlqL~-V>UWJqX%J!DZOFN<yEt z#tE(a*~D7)X9`$9fJ##|_1rP#5*Aby>RNT@Jxn1$47Hu_W1s8zfk*V;KAf=R_wkE5 zn(&=eb+a?=rLn}Kxsming8FV>Dz>6i9i^m}Q!Fmz)#pZrMt{3wytx*luB&k>f+M*% zoc$8dozO029-e@=p7s-4(Rh^$k7xzvm>z;OBt^ajjjw#pnRhy z5_GRd)~6<`iX~kw)9%@WMuCJ^_a@`~M>MVQ=Q^Icky!hi*=&BB&_N<*y*fJUpUy8n z;(q-iDl{gni{=I{H5U(JX?*d(@{Fl=!P^$6&J#Xks;4^0Wpxr&b_>vZ^RGP?f(%2% zz)4dww$)`DLa$bTcGZpD;Y9Y0XC#%J$p6#W?zj|iCGltP?sVcOjYvJUD~61^bsD2} z%0l)REU0te9jUqreG`J?mNl^p@xOpsXQ^e?%UJz$Ap%aFtPY1VSTsnjAAWO}2hn&K zEI@!NtEDT9)?vj|I(MreW-ACZS|WRYXILHp3k3>}-yC217ObF_&AmC1X}=iyRn8}a z-+Q4AF!po<(ZdYra?B9jj|q>>VZAwIH29F@pc4Dqo$tWI$WYzF-v(c%yj}uckgHMr zl9-sdUy!2tx3Y|WH!6nNq53u6FQyR4L~sfFE~2B|Ul55)DShELV=S`h@pl7~`Y2QP z+E*%a+W)7Y@5pH+{=vJcOHME?G8ygoz^9|KFUT2qW>h-buFw9If|bmEzQM>T?32ixD_vOBSQSZ%HVMGdiKCr%|C*_&7IS3xj4I!PN&8tQp(yx2a?OEW*^oZ7;@V;OD+Aqtu>pT$q=I!KIW)&i% z@p70a%-z+Wo0%KZcUrj5UDkf`k)BC{&WfSZD?^T5MsT8Ahx8z3p>MvFUfl%S4PAw) zSUP64SW1#B%izeF)-5p0olb(DYkTS2Uw(%Snwg)UcfOfjGH^V}{$G{r+-ApJehK@2 zRDX%!J3LLMpY!f7Vf*uVj2*~qOy0cy1yc<@7P+uo)#i#$#Cq}ZatlX;&ZcjiXm&k~y!N)Z2WQtN$d zYu@P(3j+NInmLqb8jP=%4}lQ~%=TEqwIk(T?grdgP^7aT+ zayTDfpT&RrMRL=x@XI1?6*(8eJpZdxhHz@W1Y^Zar^7!j99R-YS`NEw7r6xD@f1LU zRV^yM+jRc#W80Pam7}12&)9S)Qw)fUtpGcfB+}V?a8LGW8}3jrkw{=%q1toJ?;X{` zv{^CwNFAav*CIh zpboo5uyo_v@&heX=6Vk|3L`hFxpQr0{UU6XKUI6q=e!mJT_t8x{jRr->@*XawOB;! z?)_WadFAur13vN~0(|DO3ct$mflm6PxA@M%OaHF&x zO^4qVk)ypcrZ#5gaRwyYeCZH)Z%T*U!>_USMSNY=JzVBz%PKcp+&n7O7}95N1QKPJ zW$WID;&tms$2qUL-SjhxecWCx&LNXgZya~0ms!dgVWK#cK zK^F-R2ycp2UT-XPM|)w_b&#C5FZEM2Ow0mj>pFaD1? z{Z!AtS!09~N)!Hm)C3qbIGC%E2B_zC{FOkPqDB;S4s)=hO=@sJ+vQVPmBZeh`1Flh~kSk5K z!VcR2l6NY6LJ>_A$}MA(J~#bDE*`p5ci_ZnyV(70b1PqX4<4Ic zN44^=Sf&%@(GCUmub?l+TovHav?oOuFn}8;>SNO%uSjirKk~~ydt6{O~Pb|HJSR8{b_Smgo?Kpg2i=Yo54PDWjF zm^3ol08NwTknB~J>Ll>=KMYUPjp-QR4q78QJiGS~NmQ*`(pGOH9B6P#OEVg4M>Wbv z!|Y~obAz9}A>tp(Ozd&25(Ehv4%PtEEL3n-$Q(_n51loyHuI~Rs&`a@OA0&8H$${9 zK5_svQdy{Wr5b>0i38A4vyDN$Zfi<&9^bSIla!Y7g6{PpxXXBU*oX4kR!^H%(1wcH zy{kDXb%?yz^WM(vLjE=$5u^QPBLb1OW;Mnt2JKlkA1e;s#}Z3TcKHK7kzQkqo+Yy~ z7cM!sC^G)2@ohk<-W_o)@<||9O2TR=KMF+pSKan*Xua_1tnkQm+S$+0v)5E-M?-KM z|4@|p@n03uR#bgt_%lH#MpaZBB^}vO-7s;FO*)WK(|-u22d^p~uTv*m*h7bAB=W^0>jX-ya&L#07@j@OE3rB;InBPd~0CQIC-d_xw*#h5*q z)H3MSsMMy#@tl>IfX8aqkV%DbC;&?4xU?SApPfnbhA5Q0Z~qhF#$Cp%**4;NZ|MaH zpCH0D$5td3K9x8*3Qne<6g-Xk$5T`7zzBqsCm75aN1~vAYHkqyZGw#V4bgdmpYcVh zoptCFd^DH6!;1~}gUR3VB?zXYvvz}eUM;KAA}yf%E+U9w;K$b#l?+&kzWmy;=(;E4 z9xoHpG_fofUrNm=@Z}B>8{}KKkXme%2bMH&AQ*)mSmYvXgrNy#?;Q;=I%18D-H z;Yjo4M7W@@-TG*+xA}RoD~83Vu1<*mpWXP$<7Nu`)8oT^l1pm2>eGK4Ms4Y-u}TN$ zA4>$_pS~We(ePsvzGmod7Yi{OYqY=E#8N;g@(dYYCpP1=Qw<#lY#^THDc-X{O%pRD#^id`$A@PojSHP zM6;|c23+9v)FAwO<)zHG3aNaN@MM(?I{FJ-uNB8{F(eorNnP78-Y4ftiC(uzAyK~6 zraKfR5$&GP{=Cf}*x(1s(a^llqKR@3nY4&^#WL;l8YLuaM%~*MD76jV*6X!n2kpsx;2bInM0e zXyaD_FH*!SMidV2-zZbkMs`z*;9~5pm-y7Np$)^A+>eLFrkJ>52Vy5L`^CvBWNi+3 z3!Vn$50KF&x;1}T5%KcvHogqc6;C{&?0V{+FMFdW9b|sbv@=k)y2LI3H(NvefGHz&r9pA9Q0+!HN3&-jb8WG_c8G26w0kN#N~gc9 z>wRc~4D$V~zQXWNU?3jnJdx@JvSpu>Buc18ObN0))DTK)E;>RM7-#lZggN`8Pog;Z3Dj74@55(iKwN>L( zm$fvI^K|#omyv@>EkvTOUn@y(J3L!Ct6cUmtMjNlNN6l8 zuE}TGy>OJi$aK$Tb&AzH|3kSTmuDwUV99m^mHA0+#`ETNeiw zgO5A=ry0D{_k$#xBBDlizPlf;#RK6Ac9f+}))Oc9+x|RPX-%$F7>47Q3VF1;W^9J- zy%GV%mzg(4{NwU78ad!DU=Y2g>EnkO*}kNFKigX`HA1QRd(r;P*%OcOThH)>3ST%` zb=0I#XMi}Q@NaTCAP?mU;(MNVlJiVpW;^_FG1X4$#}lMVJp4rtaz z`F_C$P;i)JYLm~_y3yIc9u_n`$gCGzCQ#R;q7&$G@W5z&>ssc$A$h(CFZVMZ4sAGL zDLMP0abrdEy=;yVG=8O{udgp6{(<~kwlY+}2vv$F5cu&18uDkUEglPg*k!x+Ls3r; za!ns3uTyLqQn#DI`u7#y2jZUgQ$fRb=qqdt48lo&HFRb=N$% z7$hW65ukbIj$7IjM61q~&z{(@x$Nz;lXxCEVQH{sMUQ_pdrS zI(~-}T)%$3BB%(Ks@ae>Gh_Id&uEoJtwE)2CuUI5E1;J9QMS548?y$eAda_CQs&lg zYk-S~8V7eeGYx>0^ZP0a3i{x!!lI(SzY->Uy(vIp{WJI!d@$}Z-lPsv7|PdS%3);u z#U(%A(oR$qS!_h5ff*jifx?jJ_ejam3qX$2I`@rl{`lMW-i6i+pX~^b>WLlyXe1b? zw1J;E1Hk&Ib@|_yWCIt-Fji5V8DO&3Seq6|K5r@ocUI z*We#cfjPPrf`=#G$ZLTe(X1|i;(9)7#7|u|5-0?YPBy`ta(yrxOYG40_V&LRA^Q8M z)YZ$QJrKR;F*d*p^*`=*#RLZx!19n!ie#b_E;Q}nGImu}6@W?0%ZHBk15*ku!3JBv zaFW-5zrd}f_4QKl2QVrgk@VVdfE4G%v1pZb{&iGL3s757U7m{>GqH?96mhfk z&}n+){ny)blE`bps39ApRe$NKvWX(+QPrgb0 z8j6bOaquY$?X1~Q*mKI=ujn^wwFcMFGpbMoY!E?w_y_TE<-t@i``Yfd#&d5Xm!75 z)yvCkV@mI^U2P3A1pi+LQ9p({#ytD6APkWw3iX~!(RJu^6yt|_&*{vfbPd{Gm;1T- zx`fa+RuCd$DrS%v4b9U$MMLw#nc@y-nZXy7^eyu4QNyZydz{#NJ{VA*`%7w%|AF$`tR%Er>pRhhBQ=gyW#QT#S2Zqy?tWQ zS@|pZ_*JO>|Kjr7YbT38);eG`%=@C2(kw_ue77->re!~SzG>rYB6idWLLu$T69FD3 zn6I@o+uJ`;CKS0h3CLRL-8qlJ*ch;ZR2Xo};ULmB-rNg)Bc)a**b#qZIQlD> zPmdRcM}pe1<4AhZWB!?-%1fY8gkl0%)~YrDB`QQr+E_BsHdM4ZwvYGQ@*4??FmP4 z&mzYGL1N2$0?Vc2*~QV4lkD!VwCC*wq(FU-BcgJn?@Xn0|V; zWE~(w`tS^F2XRnV_VScxIXlHdyQXG?HJ^2>ktn^fhe!8ZV#4lS_Zj~r>(Gwy&J~Ejcr2d;P?qs^W*+QH5RxiMZSHk^sG2o8{%4y||UFXIH8h>O>{k;jhEmS2?tvl|Fx3QyD z8DB5ge7OZxjd0_xkdq$-jV1<9*d9Ga;o`+ib7DQQ3OyVX3W2PXVgOc}r2Y{PHt@Z$ zLCim&*l~L`<*fEu&k3AjVli)8Hi`zG|L_hgx7C>Q_{H@7PwRZ-c3(7wqF9)fr=(QI z!;`+MU*lP76VK@*S>f#gWSA#7NdnrHm9%Pck?A0HyvxruOQ>PpL$ns5F^tbWltJ&@ zKa`qp1_c~kq7#iewH2!MDB~x_W0zMI7|RU_%2QVe)hycp&Dc0tPzfD+*%{4hGw2dt zOb>IH-{A1cusP_O;wg{4sgdoKC{%_}u=%1GpZNMC@*g68u)+=b5Cgq;hii2EET5hT z+p;~jekg}oKhl3Wl${5s$W4H0_J73!t>-MXMsZYxXA1*ZXpQ37*xh=4c9S#zwb!7w zpb#+pTcViAA1HhS)+~#<2Tem|YXO`@*XJ7$><$W+hbmQ$iGwUmkwSqvyWQ-2rv(56 zId5-YDg+u@|5l|xQ?&S9h|t;Y%foCu(b$tKpI#}PWHKr*L*;QG-}62MWjjjG({{l$ z9#9-@$t6mhOoKrJ6|!W}vHBhQVtJwE57(~Pd1{s$g~EdT38+oFh1pm$eM9&W(<kD-0&kXbp9t=srp%ZAhlEc3ratfg&Cf51eTF8! zY(>r%6|kZl6@YbsfNfRZZ(?xjJTUgQ;sjcp}fh`3TcP+|m6kpGAH zsui~n8$94C1*gaN=?#PIvbFKdBI69Ya@vEr5oF(q=aHE_KvYybc^XnB!w;Q{srRSyHU}$i1H(l(I(K$<)^GL|nRNUe4riT{FV6%{kdtQLr8zs(z40uZ%MtC(Z(_y1wg(!BZVPd#+G<|3K$=Y`SH61NwraUsQ@e#nyz z9JwhsG+$bg|!oYF&qh&cVn7nWB6QSHe=130+3FAk&v4*W!~Z0bAAUD;bbqg!IB;G8b`sF6w_udu)SBEI~BH~ zu`qb0E<(D-J1IVbB=+KivoCv=-i{fIgPmMfq7qCb*t?qxER*85oD$aUHeCb~-l^M` zcPFf>+Uh|JGe0T{P$CY?X>Ih|a0&Nn2VpJXJI<8KG}ldS^DR~pb?eoL#Lht89(5j< zxV`eq)q|YO6pbh$+txO)Sx6jMeR*GS7ucVfleH+zVqDHBexJ;Xwk9lReVSatMECfv zKgQ%3$@LO?3EI`pdF|~o3JHItXWBj!6S@?F?kcgOr$C!V7Lae%hW<(H*GJ7dNL&6v ziH`ZN2BWY!^^X<5MjvPcAG2UH@X}1(_)e7V>{9zI^GJ5(eRh`$!HOdcCu8F70yA;% z)_%NYIaPaBvJsB>Y4L!r2e4D~aa47i%N^uLiGp#p%K?$sm3Ioas z=kA?4D@(QlA~^;Ww)G4~5caVIvo#i0%E3W~k#T^AGm!${qUf82FWHZ(3RQ`(lJMPP zFXvsK(?mvW4ex-m?0T9@redNyy&QtTKw_{Dq)*5>;opX z{0feIz%DV-jJwW;{OQL#UjQjji&jG#YItt(F~EYK%y*wV;0K$x>~NF(mhA$n6oo*x z@XjRVR}~=tIk3P^`gWKC<<<9i5}sG>elNw67|N~q_(T9hk(_&fPM^(aN6}}SfeMTBRSN_R^w%oVq1^d*gOiTv#IyE? zUh_o@iF1Im31W~D`Y63Tv^Y#u#UuUdl0T!vNf=jn<{z7a-WUgiCWZhamuk+72=ti z|0m!`*9tK!e2u?fDJomDq#;jp0F9o;YS|~**=z__$^)!E?&v`lIve}$KviqSlk)Zi zEpgYbtK~O}S#{572w&%vU$SxbCk1_GU*!3c%!&4eVZ~URH9H?D#N1IeBd(7hKYrIM ztXa*k{?kaubYYzx6`Wa~X?va?ySc>r`T04Efd4mAeN(>&xY{b+CmWd7Y9ATE6vWbK zqyO5Q`Yd1%Ud~TrH^7p?|SDGaIl(3;C`FTvyaw|IEw_KcWu z>u5nh7IUuZN!lt=#Yj%snMXNaB8)pTS`3&@zn9co4uHlq4{y92IC2g8aw+@1+Ti-P zqh#C8iw|t+64?>!6RF9#8()vo+L^ApUJ}eUc)14O^n_M-DNMm(l8z zxrD(R3u5F1YPI|)oM%-Txl*9kL)QgOx_zuLh@L!(RC@YlzFuU-Kdr!fF9!go7UFHY zYq6gg{f2lnQ~y{LJ{vye{fTPYS=?Azu|%3pyX_>v`)1wZ|40i(^gDs2ZkPsq;msgGA{ zT#*#kCni9+9wgOua4B_hWZ;JkbkS6=E?b%Nru5*DWL>F##{RK+Eyca<4;){PyNsq6 z?+v)~5vxkxm{GG&Da}=x?f&6l(i@c0qVO z`7{xrWRUtE05l)YxViic8sNx2;D?N7mlaRcMU@F>^ZN@|FVsEE%{&68#6*q<_1MzL zef~N!ZX;c4a=;zc+7pt`)c@v0;p0I)NBWuC4x_ez}DcyzW!EB_4Ir(d0Aes z3_RlZ+}&!Ss%EN0Zd9*Byij$SziPiC`lw;>^rJ;pBif)#8y+g%Y!fAiDeDQ(Dz2|3-vXmSeuMmDDJ-w?1Q2IxQvNKyoYc3{q zvH($^#iplw^~i*rUoyL`Q+|J=m>|PJ?25#Aw-#+(rts{9<5ppr@V5=z_fcSgRXiC# zMt#scE#F+k+KAoH?veAoM4Bu**t)aRtP*{gsH4$n!24a+vu)N5_B+h#$)#39D^_~Q zxIbgM%7?w<S z$k70_$hBDpJ)raA%eMLpskSp2MR$cfE zg5$Jc=qMq4`ZihB`@mME_Teq8t=)Lr3FL_0(pM%Oi=IQ!*1D#(w&JJ3PftS<__8jE z^T}}C-_&~Hl0QIjYFaDR4;yoB{2ImyPs`*BiYnW+f%(9dhqoP8;fv)#ff z7Pp+ohj%_&H4=Cyw?|Af$%Z)Lk73ZP{ARxBB_|Spzzq|yN#ZyRugQsk_X;?KaJ3-Qj+R!4 zn`A{HBBCkBX_wvezkycP8)A;G+`j**=8)%M7&uQOoswdM8Qnu1{El~9#e9&^ZE*0*+XM||IH33kyweQSu_JR^8h^|@D0LPg|R_dTw& z!tEp6<|@`fGu!&yjw1!nd<8$kG59Lvq5L_rLLF9%rBNFXzb*COisI47hlksy2%mv;fvG>?g(%@Sc%!QvPUr(a8%u zI@ok*u%Q=`5Wvqkp9m)<31Y=w1RB7bZ!s}-C4qXvqhekwt9Tz?sYdKi9x#nTG!M?@ z020)*m=I)UzZ?Li4}s9ndVCtc#sJ25s#H8HIAHefy z6b8^T-#_k8wDb~Ge9`XE@fJX!)AojPU(LI{`{lV}*nNOjivCk9D6xe72l#OZyaZlv zQEQKWuXDQ@b_0vb`)3M8vM{PkF%cMPTs@-*0!964X8^Sv1n}20HDgXO<@mq4p6P)G z@V<2WJ`D`|RNXvK>#;hfoj&r;-48Fp3R6oGq1L)QGCI0W-3K5AWvmnHOJz?qjVWjjH~Mr=snvFlXO)DOKs zGB9A3B`(G_K*wi8ugrXMOJaMZV5*PN+Vyp!)M9n{>t9_; z8uYzGLuO~7!y!HJc;fK!;m$go5pLLo_op|?d_L6~%dy`Bjb_zz_OAx|$=9a70m#zd z$iP(@DDWc71E$RAz{3>vI9yqfdrrc=;PVJ|2u(^Hf1AT_0gI66=ejM}O6|G(X6?aP z5~^ilL~mES#60}ZeNn#u11R~4<{Qg)}>3A_M=7Q6Zz(st=C6N-KTtT1gw8=sAkjk0UtrZMc|F&pR=LkEE_<@ zzIx>wF!X-ruaf=M&JtkhV<#>)ql0Ve1K`BWpHdcKp?4+|b%smqIqqVz^7C6FqcAn@ zGFb}0IWj)JzV8U&f}`L7@3siKKeHI7~kaAz~6on=Csu|ti z0#2ZdN%L~Tq@I+N6yOsWax_<=0LX4Z8ZU}o1rdE`)GARRTNyFS!VN4-|I03sKb9o` zD={%~pG52osJ2$tPB;ze0P!uVto`TdNW&FNQ$#Mdqr6M)dOzf}ghD{G*1h!4-?~a2 zW)0l!B5Uy&0`xbpWB~X(2m8UKn46Ubd~Es=(oifx&3#k4i@t~T?D%U2RM5%)qqDsG z&Mu^dNXVj&TyfvYPwII8N4ODkVA~L48NWi@4ml;AKYp(^8`zYe{+L$&)KhPS3yS$J zVUMx4T(tp73&hrJi=eV*sU-kV7oE<~{*JfP;wC(``+`rUA*{yN0&vT$0BR>7mGD=u zpveDEP9K|Z?49>HwIQc`Pc#YwpmEodD2KY3!!1(!%0X#gsHP#QR zs;Yl>I_cxq0EXZBN9z%9AMe|S@VgqHj4hFa zw@APSXEha`hxY%oT?{}8dHyT+6G`;tC;~(?lvHgABGIKuY9#Qn$z&2++VY+E%gMOZ zPP-%)I}`3vJ<=hYm)*EGt#?bH8+VZ!p46F43(nv=P})N$_4UscL=S(IK{wdU)H2kW z>F4L$goTO!*&d5@p6*1tGg3Eo3UppD^`1D*+pn-7FfM`KV7Spt#1-`8@c4O>%VzaG z9;?F3@9sT4c(9;vI(^U)T}IH2+|R)UH|1+rHVZZn++*zNG^<-&4!#C-t!tD^-ZR+< za+qys&RZxJz0kHbg^}OXzV#DP;0?6H_bqCHKqlbNoWjDwwXqKW8Ui0)n%4l+UqhH7 zlS*~T9MC%`dM5F^QV;bTk$k{DwwF>EVaALbLr{A@N@$8kQAAWxERU6S1N(lODWKH# zPNtb~&MuRj9~B<^m2Njgt?=5bk6-HM_Xb(Rl=l|D93i3j@hg0C9wMO0$flG7~SMsJnK_9 zH>~qS*A_AYIf3wba?eZ{C0+)?m18|C70b6Tbz1P3>VwRTUnD00A_N0RR5psB04`k> zik>rVUB8Ec?=2p7&`q%jIVE{MkqeB86^;^wk8$>7cOEu2xnp;Eb zI=BA?{(Ur9^-ly`=UQ)R2vhO30Qb`XClt7={a=4o4>8fI17H^IZ8~Bw(WC~-Y68eK zE`}62jxwqq;Fxj8tHI>#c5ek{f$<(!sh#LaZYr3&Nw}W{e+l1>3F)WgURS-`S`haR zSiA?6tGs1u9(rrxU{QtnASq(-WMz+v)rd8WO9SBrn4kt=I-9f=;y8@7k-WP`94P4G zgvl}Z^3ks;@0A+vIXW}WT*;6T&#VQS)ZBYw_q`p~&sD%8eIOP1!k=~4Nn|~|v!4Y8 z5(oW*@M@E~8f7s$@z}W{>%4VrzQ}V>FeQ20(5tFgTNq1`PKiM{G ztn~fW6Ar+T`S~o!YfwSK?A~1zg$E}*mo@3PShiEn=U@^%nIC9zBCte`(t$d0m;wki zvwxe{IRIr$PZ;N;M`QOQgDCSFm#0sIHm`Ha?a zME%suI-2gn8=k%kMIcwMgn634JUoNms;ak=C(3WCjE>U(V_dq)y`G5`=JXH5T48lK z3y>^k6qJqtx#l+ySiKc)@;m)1yD-^p*0p11lx-{+Pj)RCYo)0FB{f%`b4*T}1u{w9bS+$VjKI5PY_ zQ{6k`Uc4pI-L<|63M3*@&mf6T&UY*$`o@I6QTgR zq>p&^12p6^>Rayt-F9Hei=>tC=i8K=x6$b2g#SO2J3@D@=-0!&Kob=GdY zsaJ>WUK*2WlAHI79;#lO;ty}KTL;V;*?E1Kz>pJxur_@6sA&)OMQqVLzi`=`r;a_1Zwb{UBRR#f;c8YUu{i0-dV_q zQ8_kQaR;nr!2WXoYZTAVcX5=q$^*EZ!yI=Hi=S*?cKZbo6=_VRg$fltmq3xPpu9l1 z&Hs3^kS7^GbTrSPiyiZ%$7RlP)| zQ4fWEG3)MuGIF*(FN&<4FV&-cM9dSGHy)SABY$qR9N9&JJiM zD~%QTw(-(00uh@ldfSRJqqc1fVmm%$oCFsz)|KFYq)%%qn^3FNqDzZ}K7yVR0tD}>-N-4_gttvDt$0qUk@Y|{b%M5Picxd}M zkmjXz0MmM*sdoUjH0iyI@0ha};DvX91oek~f8H|b=GmIlqoOb}tD(eY?0xfNW1!|gjM$|K&s}2v?{FV zf&A94es2`hHV#uM;1#6r@Q&v18!-$xJt5`PRLIbw&jQ8N(B8vc{qfn^scsw)$Olj218*0=X#H3+uq2dvvQI{k*ZM!%UH&u}V<{8moHJX1 zeLpXUnQLcXM%#1-rh7|D6}5cfPcCD?DeAB^EZP&lW1x`Pj`zUszY>ITxd`XIQYP)X zqm`TuvFz8&O8=(f=WAH34Wwnvllq^~!Eik%Dnq(r{0?(oxlwtfY;~JvGXtA_>jMr) z2f-75Q4-Wzx#q42&iD zhsEXMNhk@}xBgQm*J_4cR=1=!cKK5~Ts0#MbmNiR*KUg5ZnN%ITSiVUE#kPBf-g^B z1s~l-E8YNso*XX9A|k@%k_47x4*(&y^+1V167}DD4~yidz6KEP_t>F0q5PFVf-eq; zt0)?z?3me(mH#D;W10wfFOBjH=Bwz+uLVl4c75*p`*Xu8VuOks*H4W0Bku#t^%a-e zdSX0l`*4V7P~G>XTtV!CKZN6AdjGdGNG-olL24CJ_>}PWMMZ8d>RVzrMc-e1A$D1V z=H92OQWpKv2-PO+=?0j7ldyu~XFLIFCu=GZE74w9mdHR+8x2j6K&~MXHnZ3;R*YZk zjxa}CVy6s!B~M#dZLz0~9v6q1Y4^5JW03ZO_okv~8g#DgTF@n-7VdT;;^yIO7WTKY zcA6I-nKcRJWhnQN2_ZL)1zl4(9!46w-RuWAc-AxWx?f7&6ZOsXC z4vfK)0OUj>@vMp4L5+})r;m5(vIFmNo*neQB;}C1#!1%>ItYzCU5x#r%wRt+BKMZl z@@7@pnn$BH-6-asPZ9X&R68$ECc&LnRgFYOaMVf(3N5LO=l>gQJcaMm70q;isrb?&6Zy|#mrAkvVp562oLOTY z68=Bzy=7cg+Zrz{2#TPB0wRsltu#nVH!Mop01>1^x<5*zLqAaY{vjR~-A3zSR$W2}0t(g|>&V60kzi4O0kd z5tySN0Q~er({#k8tSI^*QoA`<$d-nAErgWq8mJx1$h4RC`E+DPmj*ffq|}wxy8g4d z7HgT1_U@chX^O!PX`)WH1Ba6SZ#s=TozZk#Tp=zSYD7sbwaY}UBkV$9s%q zs-BqA`a35>h?`(1HErUjTCGuZQlthAn^U|xPEUhcawk0lfdGj?^}xZgj;7!OzN2%$ zf=WAsf8ST{3ty_2q9q!;Q zRJ8UtCKw3vd=U#}xv|Y)xZpR$c9Pw1LQU1n_$}79>^BRc!_v?XZ^H@Vl+0fj$$@d0 zOi%KJ;R)ldi%#2%@mGUP26N2ljV|yEj__9}^gAp%bP*8|VG`x`>p5rUDc*q&2vh3Z z7YKfcfZ7VY@0vz!h+`7d08Rs2fplaLYXV5N%@aI!ko&=c9WlEgqmYByMKuuQWQW_} zPs2hRt>mgI9s6CZzw@0FBsDCXfYx8u+&qUwNH0x+uic8|XTY4yniVr$2;B}*2wFiK z=Nwr}x0j3^tUvfW(LCjSav|==^Xt+frwu=w)tIveEuwiPQY@N+REro?Bz{64d{Gzm z&tQM3Y$G4Ym+YeJX7S|OuZv786P%`wj)59O_XeLE603I01N}ma!`a&0iJxx#;}n41 zdQxH@b9(ekQRu{g!}9cCVYvo0R_m^(d-1OXBOHtbAmS-H_+B8xh2;QFBrHptA+}{7 zbl!xnr+z{4!EXv30!*8U9UARWt|kA<_P5Dn>SWV04GPJZ0c4b!0sX-hH2%SFb}f5C zBn?hGW_pfJYnqA{ys@oFCwTSAqBg%7tED*2<kA1=Y0*Sr zVKbdBQ?jiu&p>F)Vz$@1#i5G0y!jTXS-RyqZ%BI717$*gokm@S0PGMFYJR7|3cO4eCw2)Buil}q~W0da%eGE6KIK3gd` zyMS^{t{izOsV*U$sIah?A>aNf(^Pus37QiuO(lZfns*8G9HzVv*ENk|?81~A|GB*f zfeC`Zy;f^z$YrI$<0cuH)&hvBqJZ!OUfwmMQjC3F;q0e&lS(-x8fUn?;zTh+Qnz34 zyLNrLC^wtFx6!Ymza)|b+=BzJ1bKNi4|i7?>FMeJC0OUM3=9pI>L^!@7WO9s#}IlbNhKtcGlgyHF_=+38v$VnppMpgdl!RDM3 z&eE@MFC@eXbZvu9J%^H#sQ#u)luVD}p6<8kSYp}-blVxqi9V;!eQFxI_zcrXDSvTr z!tlnzEgtcRT00c9`*5ZE5T6zijPvlI0mO1j1A%fnH3S&A52EbkRDO!0 zC428LTF5tOzJ0Qn`t~iV=nPbrCCKgMP&!fK^QZq{3KX;!Fq&%d4ap1BgAIfMf_Sa& zAPxls4=~NN`ae1i{;vee4G^hVB2OBtcCApJA~cGQcMsg1{>aMpm-c(hkZ0I>=VUK= ztiqlcZ6VsmpKRECq&WM(iNXPlIQ5z5Lh`f#7^zx~EqS47!Sb80)c^ZZ03~rxknp%j zo+aS0@l{!+;?3$dMbD5?oY`M=i6%ao8t2yIE~Euy-NRWvJJ@1fBAI~GT|?7B9&+1krVZ0_+JPls4N?$1F!+lj zJ@nDp*x3z1v&gV1yY+EhZ1VnW?LkP#P2}Dpry)EG&i{VN^hJfVB{-4UmB;kR-wFXX zTl)38ez!2vYQcgSqPQ&vB;5yf?WKV?_cB9W9&)2Dc>+l|R-u7?-)ywBJ8h$|SD{x_ z3P459zfR?tCJ$hdlfAFx?#JDChYuFwa<2w-2<++ni*(@(>e&L!os4K=4WwoihF_y` z8>{$&PT)NgZ&6rbqN3b(I9e?+1PpQbKF)O)-2N_w(X~kU=Sl=6S5&P_#&vJe6VRcL zbJer8mPDvlX<091TP-VUh5h<~20x~2^JlJ{D>sP1F|p{D?;C#kbU_k`G2g=rc}o>h zND z>PPKXVC$`o2%rGsS$EAJ8v5|Gw) zc49c!5-Jg{z_&zovKOA2Q{M8QXhrzzAyDFKam<_uml6&hd>RqPjmUb{5b##~8q@@5 z1JE*6oE%ldg;^Hi9 z&qZpr>k;OZvDdqQfED}~f1b0t5gE>^kJ->pqifzGL@xkC>oDI;ehbp&| z;slw0iIR)u0{F-Y#~9?KG%7)%(CvOS&@k)OUn1?h)>8m-oHky8a$$*HU~UY$D;od* z%2ZRZ)E|uz>Ilx+C;(##b+Wubpv134@NK%xpmu)}-ouKDYYz6A-o~6XSE) z&wH2?5`uSfxfE9)dX_2TNQ-LcP3;3)PDBYL=kJp(FR}twv&Rqk?|Px zlWqA$~GIaudW32H{2>2Pj~+iZ}5mQY%lodkQ%3E|MRQ0SNl=7hEinAFuW#fBZ{- z;V^sdEDtW%xEmN55vvuOk7;3x!~vhlzfj2e_kP2%jgYez0!9OohZcCu7L1f{o_B$y z^}EHQdnHOf=I&>?n5t}+ydyrU~9fzVhkDsCB<>7a?M${6G>k2lq;(BD4kaBFtVkRC8eJ%rbyQb`|}g zTAkr4XM2J7swL*SHV-en4<5o|@XDS+FUgMtti{oBVPPq`P9cGTPyaTt&V>TSDOlyS z(A8~(hye5rw*K&Xk2_*Q&jHS&Z=B@t^?;`UmbtuLyr@y2P-5EXRTNqxzpV!->=`6g zf%N2Zy@rmJ_uqT3Fj>$k*z2C#)ZF|~L0($A+dcQyFer8WPt5620f@cqwcsK$14R$M z-XEHYorv{*2hH0dB)^S`PzV5NSWhK&Q6uIDIGMzgS-W_V{O)*_bBWae??QB<`_odu z4S$`lhpzC^*I{rDv&*?PTsJw*N1y1ceS7iK|F7RclHwYC;10)l4aC3>(tzm0|6Ke% z3D>7AU(m`uF7BCIMBX!pL7{}=3HsnIl2fqI(LKn4jg}RViJAP$i~nn_@F4HWo=+&F z2GL#G7+WYAe>re+{w+ljs~QExaLr>|qU%V(08)#oSle#9BA`#LI5;vL4nkXW6awJ; zw63G-LjSWHP*iw_4k5WplYWLoA0keZ#d}GDu804?DPc==P_LYERvidLB!w-kPx{Pt zYP(nQUp|p@iECK{hTZGDC4d|jext*vpy)Od--p9Vcj#RgDcUrgNIGzZkyk_kJdTH! zpP#>tedo21XfBO>(qjz@PR zQ^iMV!0DK=KXyl)jwRw6V4N{gibk+Q&#x=kTrjCHC)Vu3LTkM@mLbRx3JW{jUP=V6 zj=r>K^a1l7J~xP|SXmWWDpvAesmtmj2~O|nSh*cYw5pL>=!nU*VJBfyRve}P#f4HttcT6B_EArxut4LC79 zz`D2%5F5V5T?XB1|KaKcT=`x3DbQYrSD}0B6BgSEW+TOiCQ|24yo~Z^dRFl&sS4Jh zb%t7zOb+tf1gJLiZ9#Br_eHll70H9A!hMZJMxA5=mX*f6G>IL65SHsG)u}k7{&EAt zN!|rG8=ARQnuQT_ejf#gvfaJNfc`jKG_*S=4q~XnJ8rbQQ zL;Nvbx8qVWel3vKH`h~G$N_N7moKmWOJ>)SgUvgB81M<%xXuBNsZ#~b5K47Y1g@T< zIm$l?bAlHc5;_@w$VG=HFeWBD*QfAV!<9rMxZg=s3XmIn3qv`$GwE2gFf^v^~|ayYB&`df^goN8=rY;EHB!+HRSobDY=HTZsBhCm?O zlgifC`G1K#gn{mH2E_Y;g#zI%gt_z$ZalxX8b%`t|KsQ* z!|76g(WNeSIOR+JX*?F_Z6};`FmVY@Dq;XBfRDxT$L~JCE;{fuu%9pR5ZHFo-V-DO zD~2&?PV>kydnBO;F>Lk-8WdYjJs$e@!q-X@0)d|H*#i*w-_rLOFiCTbO=}a93PQnQ zKauW5+7*JpS2G*@!Qd{27+m6nP^ACqVY~6QHbMO3HU;F_v-s{KU>z-z8|3KMR4c2G zHzr938vsr-jI~~fv6ng8U2Wy>KuW4(y06~v;0_r!-vQtOA>_3-i1^ul72cBKYJhsy zlV>agXVvg%*dOY-aDZa$YXkLEvk=~m*e@I`aP;4lQjl2Jg$gW0GPqd@=RLeQg^mL9 zr^;gTnWw1l%_VXZ`1w8bd-o3QM6})5*jOT>x3MQ#b`E$B?Dz~q;F=Q10K=66gd<59 z2aQG{nhuzPEcR)auUroVswKqPf7d>{7zQsTu+mFah)J@TsJ3n-H2O#9T);duQLtkV z<|C95holq(&OJjVyc|Je8}UFyki+k*|B4BJ;qf#1LXdg{pn(lwJh|L4;dYe$FasD_ zFp(qxd=r@ptw3guBQWo&K~j~f>U<}_0j))YdfM|=H<93;7KQmiNJIqT8!6z92>D6o zEJws;`UEzbtz80hX9b;7_8PyvY>!-WXVtf?L;r6~PzUm`@v;tH79ysj>I>(7rDO$! z(scpaulZ+iYZp%1JMcp0tJR0j$89=&%eL7Otm`ik_zlXtXJ+r zChm&rvv!>Vds0zcX0sp-p+~fKxMspfFI?ZP?9_hUI5pTkj`a&n6%^#OJ?j{+T5XdNr_x1>P6B9=&<7Zxn6i)0d^Gvb#EB z6e3<8?;T!FpWRl1pn75 z98gS@85$t||92Tcu~|vRNKG0C@YZIt7~6HU1i)MLGJ&dp?yuT^@)Vfoz}q-k=cPtA z2l^P&f#*zX0nj%BzDAEIwB5Ba#!mh%xzGyL8aG$hABn9j6i=7XXihR;gc-BTbRJQqT$gGeX$7(vr>oWmai-ucjSyC3)(O`L-|lpj8P zP|NWBc;TVC5KI5JVi3cl)_J{(RyK?&eN!s8VvTPJB=?6d$0J=ZqZD}{dERk3yCh>2 zpV?!3S^uoOWk=~hH^Cj8%z?c*At6#@zzKxWco;z-wX}dhT-buHU-qN}$ieS6RsWx3 z6eRcsECIP_o`(#H{7Xr}27PJDP#`Ko57Q7m4LlvLk?=bx(a3$FKu1ScUaH@iYDgCU zct<(?qYQ#e&fzco$1yG7ZFifN_efO$&%=H4u@`9rkpUm*?C)zyM;Kz7a8)3ZjJ5Xw(NwypNC>)At_O%*zesJrnJVH1AFM41y*C%9#Ov z|2aH07+!y?`4w__xZrU}>!m?#2^ri2In|uFi~*<+CCNlM8F|?*xO~zGb$K8=7*s3> zz!J+3=A)7U$W_h_$3)td-nEgOuV+F2k5OM#IY>}Wwpr--=k=fER|0QY^N~#xpnAA@ z_`U=_e&?lCB+z*1Nr4{dM$Z&tQN91z|9Crb_IN*WmaO^rFVk*av<&^H%cqy~85iD< zE5+SuPi~Z^RdY`)^ziUSGohi9s$%a9>FwZA=`ihn4DB#miXZP)Lu?LkT+aq8wg9I& zUQCNzQD2U(_Bjq+9^?-7nP?lT(a+~{qrbYh8tgT6M0NJ7bBcBPIvNh$oqZuH9AV*W zeSC9x#$WoL5OJ9^E$BJKbMUaIKV|`{J{SPkCeU||(N>p=mfQ4~5}EVL!*tDpmsF`X zNbO#Idw;FG=aWm4X}35^#o9-MRLw_9oUjf96bRX*}jG6f-6FM3~H*S_b0 zCu$n`_}qA|O@$LB-?!_?-gmIu7>_R*|H$Gt=&d>xRujOb^)>pKv&Z*Mj#1>GRg`%9CmubkD#~=oiPN@=bRcB@k z5$L;o8+AKceYV`GLcSfl*Yk1nha}cqtD$T|Z09wEAW;>YtY6cRTFC63)3BeQw4=mm z?1&Z61{E2)K{O1u=5d0q2DKh2_~kyqNd22pd**f=Y@_F%#U_4=t=p@Wpa|@*7+TnNdopfn+@oP%i=P_Yy#yA`S-S(YeJS;$($AA=~Bq#fIZ`)k|I`C!^;+ z`#r@LHB@SqC>JMK)i%@3x6woy&;-I=_}Q>Dq%0r2X5d<_#ddaBiAlz6tvj)Zb1tUO z(D-(jS(o;(gXh!JW64NRs1y~+V~~~wJ~&~Whc|4I{qWZ@_BlN`wrYHu7tSD=B^wAj zpzuMpBj$qEsl-Jct?UuQp`l3a$v4^wFgQJ0Ur{>QPN-&9Pga zsa{dfpSVLmbJskyW3%b3EwDg4&cZk&FYjDr9lgNulUF16Yi(-EE9^v)t=p|9`^R{W z`mWt*NagpO*>dKKknFn9DB<$U!~rstq%>s+5`B9D9f$WT-Dna?Cbh2yCfz3&l)6uj|a6-pD&oyHSW)z|CG#JY!x5aojr0 zd5&8z4?w%hI|YJoR)^Khs}Ei#R^5=jvpkh}a&}Nh$8PazeWy=b;N*w1K*4o{rz!~J z)%sRp_%7GFSoh2HvjL6FF~LnC_Bm)~4wpY@&2&Z=anwaHc z6E+iAxM$Dln6vOo^d9{9gbh%Kj|5g-$FJ@9ux^icx*4udOwe=tpA=O6!aV|3#zT4A z+Xw}x0k?{Gw!4rQ6OPhOre1=GnCHkH@7U!#Z>2!GKES5l?Vb_2+|CZUfxro&xLstB zSND?M;+`$+qZEzA#?a|n5UQ57lp(6OP{ObA?~U5z8+FEh*@eOTPA4TLHTl~u&(;-^ zK?39SGfvQU1aniI;(lsZK{$Xx39x}G3BrptMtN(tW499VtLS} zcTT1lQq~&DVSFD1ZnH*z=pYh^Jh0I9&Su=3jcgV|U=h1|tjbm0z4elnNJ$;$ajK(O zwPbS$t<$SH^BU#Y{qTwtzrYFjn?FVRAbx2h8{q8;1?J00f3p+XF`lr~&e|ixhWdj$ zbgN#kiWD+LHkuL;Ar;|5bWA@lsa+@E&RE6;{qc6aHbI0UioN{=G|y^_;%1$91?U!f zv|g=ER+%5ZGF;u>Q@yLFcqB|jE?$tAcMg_t#8H^vCwT|Db#uw(7Ic`U`v#tZj)0pD z*d2^p5efl^^ByRdlN93w|4~Tqi)xRXapy#L+_z+%12kk^qrUFC1t48O{nTHp23uA1 zAG~rO<9QjqecGVA`&?rk_wU2y2hq7^++HzSgru_!@X$wR_^>(?L|_V^8Ao6~4KAMa>HxpF4?dzlBnJ-m@H{yc`yZZ}Z&>hzw-2+}aSmwA7?1)I1Ze z&+v0=^Ll?IgNv%~M$lB-YbW+|zhcJ@t<2^U#j3AW$T1=;F4X{!JHkD0rG}S7XpE#kF1xD~ z&TLG4|1L%;W`KRudqn6%%?ybc{kgQGh&SK0gpcY!@Q;LJ zjdO3=8BZLwJh4xPtOB))M@)d2l7|P}1%1s+{z9#?PWoQzp(Gt{{pNM`bKL+0n|@!y z)XVGK=J&jd^-ACGWQ&zn?1<25jBiSF>CLi*XvHyNdoFoCK%KA{W3bnaHsO`UBLCKv z?fs4v{$bC%n6D;2WV=qiJK!{%Gck7U{xN?a>H86;_*5(FewXR&=(l8}<~3NAp4s>c zAsHe*|6PwlqiXQz$M)RY@G;whQNiCeFD8Oy3Z*Wk-+rZYQA2wN*ra!EA{j!MBHCl{ zvXp+d7QKpjS@+$P-LY48{jfi>E7fDS{ntEpirwOf;+oss`tOz77p^UO&yrNRFbxTT z=i8v2AyFndAqjFC(vzi5xqBb!y*M0J;tac*Bxm}FBN6R{YMoJQNQ77@hYO+leGH}X zgY=vx8-}>|uDhMPA-;mtSh z%IU7B@=34Z$+UQ*<}gDEXOTtpBtn`Clb{!F1l@UzSv3rdX1T+?o-US)Bu23NDems~ zSzjJ^@W>v?)T!)07imBq_x?4Kqq%n>l_R*0i!qMY=BcvNgYg*JG#+^O@KTZVBOC!h zOH0z!g%gJ7nuCA`$BF0B?8m)vP9Jq`SLWh7kEf9nsYGAFW{A})P=Ilp-(ENGz7X*^ zO;TmTZcT^7i?5k?^aAXiu3RNPr(vtxU4oy&EDqII*rCC5pL6XS)44ei5oM1(w6H2J zm2nc?e5bFwhPXg8U(mfag<{npBZuqM_w}zXGL9k z-+*8hil_J+PM|8eHflCAl<|{DzcMzx3Fu?OkMQBXkMk%-g)Z3`vzeSq_ApUXyOAas zB3Oo>%JL-QGu7VWt;C;bf!C2CxM`-Zu5`@X84-aLTLH5^Zw@Y^dC$-Uqqn2m&eo+H zHEA=AoM>_X9t1379^b-(Xz$&2%f9F_AKtT8kQVZ8nghw6bnsYZ1P9554MmHRdo{3e zZ;ATs0LJ*@Rcc?_J^s`OJjh8W3g{g~_(sQT{bEe5ya2`&C$=m+=8$W{ue8Tf%;O zF``F^O?1!)vSe=|5bP|R9B@?Nvx_YSs(S1hCPlw0O41k19%Q|92kP#no|D`Y(cv8# z8^H(&a)A%fHi0Fz-0|w^%4=e@KqeA|NBg_7@R9lolrvCle(f}%@6pL8p56B;NW;Rv zHcL{nWs_Tvn@{di^98fO2Y?)QJ+oe=Oq?Fg+-S!Y#^ z!sqYl0XzqozuvJU5o`tn$im2YtAl70?Ew$Q8xhDL#>1cm-kxZjpXX20AV^Cedsukx zV4Ji9Bk0A69K!!>UjuBf`$AaI1?wHaS#NKO4gnr8eu0S&3I9TNIgO`dhb9su}VTqXMe5${WyZu={COJ1{-lxJ%c~~#k`3Qpm-8#BnS%TkDKdBEEbQ+^AaS+CdD+1BdBv;_=QwJ1`P~h`5IEKT^bkhv{_1k@H{FgF9n( z2$Rm>TlOE$Ba9H>$Vhh~KUduF_wwMJZoJb)ko3P?c{n^^`JPeMSKtma2jKmKrlx%C zI9C*cbzAFcI=C5su1#8xQ|7EYRR# z;jbxCb?3kk0LuUL(EwCldCTZrJW(jY=N76wLxg~vVi*)w0IPy|mV&4uG|I@g`rKS7 zg@eBlqxYxqr#iTcezeSJ7QxzR;2orE{Zb>sI1xq%fL#seQ!irD??GlRsz2iZ)#Fzs%h+4lHCNADW*AJiWk`um>ymyaWf*T?Xu`h>gYh z`|!XJQ((^CxYIm{Z-xR3M3t922VqWoT)MpnUjPWpgRLxce6vV={xkxAa`jmx2TwTg zw%c_v&JxHy=mPK1Pi{UvMx2Q%eAm(Ict{bJ2?ls8j`717@$zp=&k`gMQEA9ujX959 zZvbnDDnS137~I-Hr2>Q^9NeyiI7s2z`(SzXb;zg=#yhxCsPG<{7NQC_ujWQek)Z}u zsnN^lgPFtQ!1F`#7KkUf9kffB;0~TZt|yqX{Td-JVh=>XJG|{j9={Q1q6E(r!$+Gz zHgLWVj^rP|(cgAnts0;W9Zpi)^HitW7vPDX>zOUw*1>>t(QH1e!3FRYL9@bTa zod@<1K4+eL{>TC04HrXgW*86$1{eYRe~j=yM)*Hqgi>h=@t!!M`|`ms*=JMF4i3q& zn=)#KSsJVMGm3iQ3xxHg; z{VY~*ulbruY%yzAZ1H`VtK`9I;jNwwtIPCs{8YsHRig8(ev9=FXzfBiEd|K1?}QZ! zKT`Qz@N~k6&3Li*S+q5?cwYOX&tV0ttEa!2Y+eqyBb?o-^>^WgKzixH9@MjVJ zqPd0$Hb+y7)~_X1=+827#za8|6Zm!Kqvz^G=Xc`t@Z7ytJPmVayQ?1KMCfqUTiEim5 z7~*s#bCZCK6a@yWZAhy+qyOAAnAq(!=r8C!V80X0?t8I3Y>#?DG%=zR04S| zrpp$uNF8N=ay2v3yI#h5TrT2c=Bx+phe^y2lI>6!X8fl3u|$F@^^&#t*85~2NuQYX zp4WbP#G$<<5r!RT1j2`_+sSN`!#R*at~)j*{;MN2BlDe5chC?9V>ZI@SI#Fe z!0 zLT!1%vxOIzj5ZLYt_rGtAwSwn6P(aloMg}guc(0=25UseO-8f_R4qz$lq$5wq|gJQ zrk&(j6SMQ%!uRMS*;V=MfN` z%bvoZ8O+-Ua}iMuj0#hC@P++4{J7@q>W_X#UzRf2Nt>n1C7a`roag=I(eo|$SRn^*Q>2>FXII7e8P^D7YFad{;S;Nz#Ng%GRFk?w(CA}2XzBSjNKgPT)>nEoj z#IM2;vsToOIZCNVOmy-YeqZ7KVJ|ZK65~6(KJj?P7}?4~5qK{*XA2L?dqb~Lw5y{~ zEx143BT!4X5LTJWl;hkLZ+vi>M40F}lAoQXJ&*6`luE zCa;2>HKUA^kad{T!>5aQek}ygpo)?^S$)g5;r@EIb7$qAe8kb{4eR`L-=q1@K^0d8 z%osk@zl=te((1IAuMzdR8DmKJ-D}x8$%t6RL2^vdBCY?+{F9E1HU(34(AX%Dd2sKQ zlIP9x*W!7nHZ{gRTdVK_)i^nYK4EuQ~Xwd!a zs^=59nNq}qh*R&2gRdH1mKrAQQZ^?Ok|G+&VMad^3|{Kgx=ia>wC??3A5u-hLS1FJSuJPmXd*qMaPJ#5(Xewb*UJe&v>|{`TI}=FFf5JC;B;M0_J!lJ}ExS6+Bs)G%x;aK$Ky6d} zrgFc3?Mm5c2XT^dCY5v=J?N72on)v?dO*>Y?%3;wo!OzhaZjNd$nCvGWHgSWV)DEpTlSLxU(X`?KmZw*F@KlJEx{i@@x;_mv%u5aJ} z_2+r=FiMb&WuK|ulrSWY?Y!+vuw&9}WuvcFBN%PErqNv2K}i;#1dR^lvwXt!>#b^x zao0O6wAwdKPao&c%ZKjl9+>ZyesZ&TAN|tpe#&8iKle#! zjVza3>@zG=d{Pt6eXAuaJ8t7Xsn4QC;a|SGk^Q=-N!}iUy>pbX$lvNdT(Y1=%1~ z(D>}qgt&yqGS1_n+q0R5X+k!$<1FZM>n{_>tt#vO z{;JuPumu@WmBpETWwoCz_J+fY1g)j{X`PKNqK=OY5BC#nRMtspPmc6yJ}X+#B^(An z`9fPU$Q;Nk`cv!gd z*WGsyk_^f;WzU>5s_kIi%0#qr)OMjZ-HeL@CAzY-^DdMVXBu4)Q~BNXVjN@d9D@yRhZI@6$oMnAL znaE@-e*UrZ3#aW3cY}%WxaPACSFJFjz4$kpvnWYngaIvTtHg9i(0R;VCXTgMC}d=y zo!xM4TO>>^vyj-i|JSlXeS!F1p_B!F6ETx+?kX*_c7NFC7t75voqpToXP<~l4w6rq z4AOQ$_MG7rg*nH}4}WY~Uir3Z4Z(u=}2P^$@D%N>}B0 zu(05APvw!$`hI0{pEpdi_j5`5f-y6+Z$02Ze7)68xij*ONoV9Z_^({&c;Am^%JGnA z4dt2{^rM3Mr!i%+u*Cr9LL;Zw{p?ipW-s3=-p);T5V zge5k&u|V{6@7yd&RtQM@YI})Ze?Z7d_jij#6qP3L8&du!Eag3Yk}B=qCOb1f5BZ-m zd{~J{Fl>YI4*SA*BTr3I=gq!Eau?lJkL?Q|7P5e*mppP^inETj^(y46iCDAL5wMY0 z4Guc#$J9KS#5Lq!=de?mSfN=`9J`l2%&lRgvmLGA&ayvqbEv@F+vSzxO|9R~-^aBU zZgn<`kM*-Rm8TS+7Km2Ip5@QD5a{Z!q)&M5I@62qxPK`$j!TL)%&?dFyxjadn{n>w zEM(N#PdxuDyWUD+Pttyi{dK<B!Fzor(fKtRSAzv6K{#JA~3P#XW#}H zltU+_zhilwqG~Hp&c*Ug-zS#H!Y5b8yULTQUoaZ%#^ukIf8c(9r`XB~e@%yqb^A1? z(9!r?QfgFAQFKzrlA1tJr)5Ts%xrd}m`jI!QP9(6^B>qjrnC`nu=+)PcY?_ROZu3D zPJ3*6D8~*wKQ7(g**5mA>@P3LdPwlbUCc+G=zg=&9ifL0$Qj33j;(^3obz%GhRHC$ z&yZO!l8LjxT{4^35}eO9RSt0N^$oCbSG>Ft6mQSx`OBOB~V)3(R=EwD{Vv=c8e^QlB=N&TjtpN?-kO znCtqYg5w@+%dnhCgPy?7e5v2sskLh_E~u4t{Rs)qH;pIxxe@K;cIk55^@2ZsW-xlF z!pO=rvTt%E_rZiGDjj@3InZ%j^)eWqis)(EBPV*D5>uyADuXGRKDdopxtY=tu&8x93%~22* ztq?8^OS{Ujjmmyr;2Uv(NY>C}XRy*5KWU89$Zqj^ZPc7ZX5S>CeS)3S8Wd|L!@^(e zlz-eAHqJ(s_#)zDqGZvy>?raf8*Iz82xfU%`}2{4rx!s&?qsnM5q3^-7 zlou1N`BAJInR}xYB{h4cNl7$>oFQf3wnzzwD(FnvH-eeU(n5C#6^l>hzLkACdGlmx zL76pkK^ZqIo;EwCgTcWrQNSUzMq1B!WDujfkm$GHb)p`9a3A_g1j*38e_Qz)%1mT_ zXCax@bqdYcGyJ#-^DoC%9h6W5t$P`Z?_Cw{JESL`0-f;#dLloniO@Yq)9lZzgHpR`$j*_T=*TMKWk$Y|g)+m7PEsot4L z*kPXkoc`C=Y#RHqR<>R4c^nAURDx6oeXgdjuk2h*Y{BD{o5T0)U3p6TM59d&A9TN* zT-4jIGAORrsU|K`S|gh-tPi!hS{*g4p&oNkd7VU6{chWv7$+&N3Ns5$eVu5RiPzQ> z4aO-~8H=}=D`0i)Jowv@%8*5#Fld9Jx!N2VLjmMwsXD;KuTy>e5pI6C^{U-^U7SSN z8pA*;7Ly{z9FsyGSd715}btSMzFU-9LsnFF)C-B~UWRJsiI$7th_dcI#lK#zz_=7V!OMYX-_H z^)pDAtvPNt`g6V2R9j2QYz~#o@z-87f6&@^OZu_)Q*S@FEz+rJtcOe;k41a=U{IeEKot9}iTV>z!!QniES z%pNpE>#!`UpIO}eY*s+}8+whLK%wa!bB#>)9nRd2B+@k+!jJmF#mx7kY*s&BcV9fz zN?yB_U4+>lr$zrd*okKU@cL>?S7xEn+M6Oi^{5a?r{s@^g{d&*_=F*bt~*W&nyNNS zPDPuPt^HN@&O>$C39k<<>_G4;u(sz6NB89%Y^pwy zVVmud?b}|V$0OvzV%7?2)jdMNl2wciK01&?VM6No(2rMG)F&PoIZG*(I#=_*`7sv8 zXmQ3N3+X5pl%pHXBGWs8#q>;M7?t%D+sL^t>&WP*8|gQV ztzY>?cK!Pt8D_8e%?jX}fF8}iBRl6F2T}0E`84nK04eB;sX0Z<`quKJiUYOF4{@WF zNOm1>4ZXtX*u!Ydv{7P5VR0-mf!uoPxOXO2Fq_3EU*dKxeWWBEr;j>ib4^tw1m^p# zqjh8kS=kH>i?`Qtj3>pi6ACr6n{3jJ#mQ2CtIJ+8j&9>QPU9%dGN;O4HO9Y0OpfW{ zjRhP#M!N`#sZY_Vs<)Z=dHfIT!X}%0trBlVQq*3~GeE>X0l{IU6;P zrCBI+I-CFC<*BXXY06ZxIy(a|r)bKmdzq?|*+k`(^&Z8RfL@~vq_*ev@J~)G2i&ynplO6GCL^H=jyzb8xGZIyn;X2nHgNRaC8xLeL>4P% zjWcz5n+<=_QP#EYDWh+HPD^E@ZGh^_)1>`YT-j>$elHvd#x|4DS1;l!e{#wdNQ`Cd!)P`qB}t)E9o12hRCZ)!VF>B%Yvh~GuEsK zk5&UAC|OxXonab!cT>pp1fIJ(B$KTfv^!}IShR&nFhgde*TP$Q%_3#rI-%M=9T(kf z+ZclK@AHqzl-6RTVW2rTl0A9N^GzSaKXm3TTik4Qr`y^3dtm@SCBl5 zC{yh!y=u+9h?PTWD$W_@rJ;1`w8QDR;?R%Lha~0qEpkndUwtF7a0#-7J?HFJ{Dy9? zmEqB7km?~3kD0`z&2$yF16nXr7~1&x^>TQ4)&TAlrqr;S#3HrgO>>PI-4dB<%?gp5 zQrd(Y8&y5|rRG&At3p9&Ii@oYa+qH<7~lMiAIP^I7G*)iOPa-#<3pViPY>7@#`g@?Y)-OJ(kf#%f4^(aUww7ic%V8lX?KyCS=-nw zq;xlKH+I`pO&gZ-CiLYEzqKDqH35cZfms4qcgBMI+BHTLajf4=b0{zlI^mAQzKGl)w!B?BIN+a15|o9R#6`j%($cZ9{XCXA_;4p~LT1pLOj{c_T&8`*pj{Pxh ziKikL(W=Fexx462S+!)j!NuGH#}i_eY2HMNYza)yVfRC%QJNrpT;9eojx*N zojyM&A%u1$B_Je7v^^8eE@Z-=oT@@HfMdRuM2^vzRZ+0|<$I3UhrQ228SM1=lh&|o zil_;~s`{VF?5n*m>}nfrX4~vY&EDZ@_r8Nyo)Rm^aC7H|ZUm)<-Ghl56Sd4~HE)sz zX$FoG+wCH@CGp-k@0)~BD;3-b!QtUk95s7)?XilK)Yh(et;wd{a>tam7{Ml@>zL^A za;dvyM9#GbhcoxlY&B@7Lk_M}lB;YBZkK6vk4VWqA@Ivy z2ol^OI0SbO?h@QBKyY`r;O_1YgIjQS_u#=jxci-XNZ#+>b$?E;={~lrYM)(ox}|+! z8NkWc?}2~XzBiv;B4+U!=BgdsYkGUhS5PImTx#0tAr}qA!SE|pVT=l1qB}>lqc&r~S_G`oqd`V$% zQKpm+lz3(f8y+@jus*OZY~UD#P4xTjd~RLMT837_`893yLV zy}Mr$)ldC6=LER?trxSu+JM>YbR(*1Ezyd0c4-26HT!|%Fe1fcx6&*O-MS<_u5mz( zRZM%8EcWD-O=+$vg*OfoKgCR}aL-6Z564 zX=KbpbG7L3`(0zB>QqgDh6K;gAH4?m0CMV>$srSz_27?woQhKcav4h+*PtFeJ)FNXoxq;2y(1twpKTkDLvEASOpr*45`oG;8ek|{Bb_tZJ<@TDZdNHTILYVv?b}CSj7fm6L+#rWYD@ce z)mC{@$~z32IKRr^=6RTnHrW<&z1a|fpkqZeCI|r8*MF9G#sfjaf6>ai5&UL`KxDE2 z|KxggQ8&CU9e__KP8kAo*&8^m>?&^oJeNAaT#|Gk^{3~20WYbbu}^#{$E?ySh{}+r zFB))bq3!F-hyOwM)A5LFpQw8w`c!h;%CD%c$OtF+v4j#1F#~Y-9R!cebBvl;(|P7= zz0}^4&Jx&X+ik#o*yCm8{g#ixJX39cz*3u>E8wX%?`~`4l65W$5_jgBCKqxDD-Kd( zki2ZsV5WU?9`UFVjVL3yOXO1)r27#tePS3kZ-txtyH(4oR2MB#hF|CbY3t9fYKw3d*$Qvo=noEbKxtMg!V7f+rHT(1qiBb|fs%{%X z?CN7m9x;6(XM)A>2<<2n*>+^xBD5=6Bo(t!+EEEL_kHCT0bI0ws+VL@eT&)MyYSmN z+W+E^nR%F`FaR%6GYu#8L-clQrW4QoFjkRA(Tgl+micZTjXQ67HQlMn#&m1PJ@aNq zB9*fUv>8uIWtcJ<4L~%Wve)p&;@!OBfWd$p@H3$BBs$5kj{rWYK0uPH!HRnp5slj2 zu>#^FI*R@?jgIMhtte>0Ma9P7G5tmOHwXejpzkH9LBfN6^SundyPzv$h6Nh3=|Dp7nSh9xZayB)DX%0Zx}m9g-h+4BHm5jOf!>Jwq((6#HZQqYAdi^o1@& zL&lz@apM-BuC^Kt40sb)xGY9&r9&p$!;?-+_)1M->eeO^O~?SEwWMFw`&}$5R{aqe zxw6)F^@q;p=h~3nT~VL>$ELIh(3Q$fxN4Za5SabTI#miK>003ltW+!g_Mvyqb>~>NoIMc@sHRMFX|kpQKeoQZC>} z&N{ItO+;3VsFlS~AI1e%&H^}<$V}rZBx7fMuS2*YNb?Ab2;_Txgukb26qf%mpzm*w zMNF26;#xCZZmv)cv53KL9#*{wVS<)+S&v2Pd*da$Am{hjJxhcEeC)>GBn z>bv1{xU@?al{>;ovF-V)7;X#;wa5&)mh`<4a^Us+__5oe0}56~P7B7p>XHfLNcB{1 zSI*=&m7U_BV4uOyU7FS#t?Z>xuG;1@IFj8q`$dUC*oPSF`RCn%*cSs~zkw7F=`Z%d zC&)V?NGEi%eqm)SO4;8!wKKWjmy#E^ma*gZ&>*j$NLl1%FeM^&Z3~IKN1Sf2OC+hC z|F!9*jL_=Al^enJSqOS5d33i3f8YYj1+NW^k0wg(wJ6IH)gp{|vTvTpXiEq^WB zO~N`czXo$>Gu4w6;?c}=sAXiRJ%55<`gbitkKluvWh8CN0g4I`^$RJG;^yK$`9kFg zJ%__O!S6f;Z8J|=xC_r08D#yjj<3wk(a{>*nLuIb;?Tz8FFPCpnn6B#)5_>993+j( z1cGf?ii-%XoQ+$>yeeg^l>Ou*qw&WviPg52MqN0EeG8=u6Jd<&NRk#-WNs5GJX#?Q z5#naEmmF_QF);^$L&L?OYvVSp;-wKUz>PkcSxJkg-=p_!;k_9Y^<=O3R?C;Qz&;H( z7qa8Sr>taWGx~nSDMkwUK{i0uRZf?or}PfuJLJo*QwDOHPW=}|?uhk~(S|=0cPSlK z94e%o{to@i2-esPFr~K^{Lvj=U%E(J z=rodrTj&o!kTxXxNBj%tbx!{Uc#6*CEEE3l_{t$7-2NOW%i@| zHbsA2KeC32U{oYuJ5BJ0vcXDETcU_}7aL+Sm}&d7>EV4~u~i$btfXSmFH?|FD@ z^xZJi8I=dB!xJNJz;s?#dqeU|YKbl&zTUM-8H!ZZoYb97lNv?KFjHV+km0yitUJHV z=X{eq*v|UJ!UH%EGml*!;(tgcU&jWyLhPeX`G0^nVFO;7wMYca0pIZU$0k8bu2WbJ zLNATZH+uzYU!=-_f~uCWj()emtB^jecB|So$KZ%L$UQIPq2N+qva$CNg^rZ&;U`#8 z>}HI;+S1gBHQ8||MnO4BeniPRq&O4nYcXlKJ1fMO=lT}Ep<98F;CD*M;z--#LF(Zb zU*?L)i3-e|Gb4Nb8!+`2WP@_sL5`Ng}K5W%>aQaQA*S&&U z`rbCA4UQq9AkgqncW%H*0moP)+{6BF^%_Y*^abHl+c!abMbC}E#sZz3h(DFQx?y%U zCd$PY$4{;HM5qUs#+&&20Dxa5brWj%j zUI;1`$D^JedpTE?OPK6kjK?FMn|Wa=(8qt#onhz`tyM#hS;EU-{T*77isb{4jXjrK zK*^X1WP%fM*rf{hB*MaXcrhFpUP`@BEFb0V=xRs)a#n$>&sh6g#Zro|RMvWopS zW{^UI?1E7fmD_dpX^K=kiJ&L{cU)OuZijTbUjeLr@k4BaoYW&tBL#G9AhYIWbFP87 zEN|q1iQDaHF0t*p!M?xy{41*1Z3YtU3hs=>ab2}kPYTGpTaYzI{Exg&A_F5IqIjHc zYDgdshLS|TLq2J9G0KdO%tiP;o4IWWQbEg2VS-I4^_dI34Zhu$Zbq5MPesZ z7S|UTxNyLkK6d@x9s_wfDaZkC*VxFtIlxXxo+MCobGFnl|87LDzgp_E&C3WxxBjQ0 zq!#+>2q!?&G-`w1n93@m8D&pN5zJOF)%RNM#yZBPOS@%!+_~(NOyDl#q?zMt_SDP> ziEzqFzYce0+s(!Ku4m1wBo4Ol=pHJ9@%Q)eXc@KIfx=5w z-^=Y#6_RGhp08Kuq zaM4ANJbw}vLOK9m_q+b#dWwek^&L(d^(Q!zdfUw#SekvtarF%mo5m|DSbn!@UBePY zAgZy&4vdJMCSjw0uKg}AReQHGq#aV6^y{+~&G_L~O|(Bvo}*wQK=J5oIE-4EK;lVJ zVDnRijR?hxje9#)L$hM7+MKjs?6}_b58q}T)NTr-e>U+)QJeWI-u1B1JGx zd=Jk)t}C2sW^HqZrEW$PYqvsEO)4}nIxMA2DbHYYGKELwUiG%t>YH9UoqIWb?Riy{ z(SI$sUY}{I1q8^Z{3n}$loD!)lo5=rtCmv^>xRbxZ=ZR-C%?;7S)3#vKFdgU%?3^# zA(AT0Pkm`QwIylyqP_^oDp>Yp@rt%8IgFx4X4W+^gwpnoZr^Nn0p1l&2iN7m?!&tAm)l!C(g; zMX-M9mZqDEkIM$n-smAi-#{Nwp1W}qvF=VRH7r9M$SqVee2+j7ymA`5w`T_l8G40aZbcgM$+=hg2{lK?!Drs{;>VA+}XGl zo~_b@{7D*Vq%@@Xev-ekST4N1{1cdnwWHV)qN5a~QI?uN+xlv&9q9sF8Q;1Ik*d^a*?Vb?HWQk~ z$oQMnf}p(eG9dD&NK@5{F=gcTL&XE=R)W=2%R?3fNcbT}g#`)I%(=I!NO@PmEeX3( zF$g`pNSiSV3=V^_s@r(!jM_`-G$UR%E-SnYx5)8uDwVEWzI`G<|^IK%7-EdagReXn;1_usvtP53yyIMc<>CZdho*3!NA8a7B z5(#gO|C?6dU(!InoLT?y6O|PDpa9+PeN)L=zY%!*gBtbv;}f|w~Xkw;M3}aMA+;d*ato!hf zw%g#nJaCo>6W-_p!chUG;RZ36fbofbfCQH&Q7#g$g?-+-V!ChL&V>hlPRkv`cpipG zJUwuhNNx^M&;mOdC-O$(mg<378xb9=ZMZRE_V`E1KZyIeMogQVF@P)1q`FX z4Gz9%%dqpOQaws<3Idp#54|^fnzG-9g)PTr8_%_MpksAn=CRKj$~!M>EzXLbv@l@X z>9|_i{$LZGPt1oQMAfuWVs_wPHj@Nl1uTq6oXzYZn8Vo+e&ZPr+iLEbLC#tn@E1~L zpc2=#lrp%rq=T)}nP)i{nE^TZ8Jj)%|M@51LeQ4HQ1@Nrf3hw}h^qx{RE+Ai%8AvO z^@2u1mf>u(WsA3|OU7kF8#vB5ZW_@}gv9?jdyO*O`%<6W{&Cj-Vkc zLmxJBXJGA@0SU~vr&*hl@;+zpHs6bj4|S$n2$i317|GdaP>_vasrO8-x_QHN7DhrK zO0=o1YE%<*TJN9%8!}o;Ip!U_Xnr$(F1aP4=-1q3>exo#O)3MPPk*>_*O?ZTgP$=f2TUNc7cHv^PbSFPnV zu2=ykEg?Uzw96lT!0P5aWm{4t&^?S|zCmB#|3nG*rD8>xS2nZrFMVX#B1~N{mlHcb zFo~KRee&gl6cm1)( z8#c7%hBZ)_o}hi_YzK2#Iy~Q)=7)@j$h18Am38_${(K>ZCgi))ShBRT_YX&9@7~9H zbjl_LpVTF%I$wRvJEzmo8?-E&f?x?j0Be=_I*#HI(~? zF{P=LJza-yY$?0mtAH5K$nmOtY>EP zQ;V3*V@S9UAc>%t&yO2NEDvwfc}*$X^OkOWPu?;=Vi6(paQwmkKarOmUyOk^hf4SJ zNbPmoT{A2&n^FRs4Xk?pXLLSdK+B93qj?AOk5gvfgKYIuz4RAKNlI1E&I+wCL&ZaK z#QAfG7zDEC2?s=`1A5H;7fb1r))l9Leh-?Qx$v-hR=JF%Q02-pw&Xk7Y)Hj2*B?WT zaA}jPG**6^%6>IznX~)E>KlIw-beRloS4Ww;nt8h?~R5hIJM>bj>U@F5szczWMCc7 zo&#v_TRA~SW3NFQnef+WLjE(F5j~_NlM?{l3eshA2-~b6UQvUaCmC^`8Fr?Plg+Cz zbC%mc%=0gL6L4t#>j?Yzhcvgg&n218ZCW&tsuf~LCuSlP5#;>2G3AhSp`As%g+{m9 zV|R3jk^4WcMt(%7>*B)!2&E#^G1qv{)~d~`*YONoZ{#!~q? ztEYv_))KjcZE((LAVZzgX4?2KR`teT5B9g)tfg1L`si4Mfnt zJe?0L3oIy$0P92$|F&ew{Fh`vcfQ-=M9XRia_3EEI>aq?4!>EX0Lf(UMq?=-lD2;+ z8tC)iPA>ukqnL@gN(umc%J>PP+_U)+5}IVVXS9a1dXz!6X!8`l{1Ozb%MD z1b)O%%5&Z^(98eTp_5OTni@jw&|U^fcL-BbQHOQQeIp2CU7$?DUlXj3tUdU=f?OAU z>|-d-n;QIkmkVVvP7bBSs8W84Yd-`P8q1WrZY|@1q~kC=P_sg zeN$!TJ9+CAgQG?eaTDpl;E!MD+HT}GG2-Id8{&tB7_cd;&Rp#2@dVl-i;DA{OJffJ zPwr9ih59UjO8uNWHCgpnYmu_N4qC0W@|2W}6%|zn_)~3VgBgH|8xP3#3Of9ncDpq`?f!#m6dii9EN> zBZSYY1OtDH`7%_Gl$PSa(`IN_4kF{%PwqwDvi=ELx^VLi~HqJL*7l$1- zo-280)xotUWVy=7-2W$4R2rx1Ye7d(N58-6qlPdQi+L$5EUjPZWkG)Bg3_Ym1%Uh>OlW*(3pGO?(04ZM9Nd*md} zCps2t`AUe>{iLLRi1Kp;j>H8&Aiq2-{~&O-V97D3^T)Eh?3wCQ{Sw&k&mxRY$(a#B zQi~#&-%`-dVi08g*_L&M#uh#J^QmP|c9-X5f8XR>!w0#uPo=hmTj6e$d-widC>V(S zJvcPuQQZxRzKR4A?*XRC(-AaM12KuSf-0k~J8v#<8#Chq>UKi+OFm~3S#EpG;eo;k zXBo=C;M@9Vif6OJMgL&S=sDzk$2wxi>`A^mHZoh6qMNKF-XyjeQW5IXU3MtBnjkHW z&#QPeEghb7N#^i=%@Jiv(QoS4S=1R$yjN_*wH`C$l^oLkfeTGDo#%WqjZ%S#r0jh#?P|#^JUxUYV?zQLNFl1S^&-`}dFRZ6-)} z@dS}Qe+GF%uhIGd82QC1h`p@_9X-Y$r6D&=scFkk-B&y zGw&T-VZimYD^vwpR8K5qy==yikNBw69oY|2c)|IvzraJyGkzj!#vZ=nKg_8nVC8Or z|5YdspfTz8zFixSm?(x>B~D=fxpKGJfkY&i?v{_+Qz}NgE}i(PFRp9fr!|K;P~}x6 z&w`%eiejMq{@43Q7I0vt-+bl99|8@`0cN_DEP;-&8KsUe!L*xOc@yklUF|OcL|^8l z=OzNak>HVc#1(e5#5zhn4ngE6{>(?DX8>C8C(Nj=Dr`e*{y`crRJ7NW?b69(IjhSD09 zaF^GxA@;7}R8T6#>&Ay4+99@smCx3!#ZTr$6?++d_q`Ex_71t?MZeQ?B@8Lpp~R=9 zEnJSD)gU3StF#Pef8{=n$l7{6J8WmNjeBG5?04%F;Az9aW!JeHOijV((<)uq$70@Q zAsf!VU%2Vrr0AisixGTbHfC4Y9Q8Lbx>vo;!u<^WkU^cl#<_YbG^M#3^HV4!b|N(q z$T?CI`J(NuNa}jSx0tyV^&C4j>1Nbs2&|r!2n; zl;tLhu}5XEsgd8b<7pp)ECbQtRD`wv70rb5n*$3=HaDAYp@}`@A)B4hAmKyY_z7d2gESd1<&6gUIz)n zG;HZY73X``>dY@K*%uExaHI$lQ*<1Hyd=2!x<4T!x}#S9Jyi^y~H){^GZ-8tO%S(}>&5Dh0Kj&f=pi)<260!>+Pc)%B2t_!g$%U?d z`W819h&XbC03siO=lbUV@{-coJ0R*kqAryQ`^HBYd__TFMYL@fptm1! zZtNb~E|UO!MkdQK(PIcw;{yCR{`Y9aZ5jEf2C8vp`Wb-)v&AeKaCW?-i-zDL-x}i`wedjE_enJ z-o1);Pb5jwnnu2*wp3XoJf?a<>RB&bXAkrLssdz0K~yF%6UOLGP$Qq@0oqcDYXz>@ z7$i@K&v6ei{Kb3+hpN~h{nQ-GUF$Vy$&S>`Ge5sA8IJ}KeQtF;i@3nwZV)jGI@P$c z^-~R{6{ov;0c$4PrKxxEu!F#sN#oXK|1CWNNDK*N-XZ{b8DzD~k5R;-`dNR3 z8BV7CI50b_4j1YF*YSX%nrMMguJxM-=dE1^Un39;8}ov7g^bZ^Bk0sN{jxkj#}px>FHWnE3w2>gwiaXeG4kIj>sB z%pq?>11M8yb}S1vk@eO#K_GC_2?7ajflHLFj`W@ge3}3gB7aE`5B|M~PHnRE(T7+l z@pAJv*Ps8l%LFy^0gm^rRnTOw5CRDlJ7F~lJ9MxM;yx^cf<8(&@>FaS8pkz!Yu_Pg z6F}HF9KL9M1M2!CFh!kzn#S;G3{jo|rp!+aKCNg3LHekz`=LC||j<@wG58z66MpUECEecZdT;gJ|oiu(T8(ki5tQ`||xC z=4?Q7&cS>F+>`S&lT2DeC4Lv}D#}2)eICYbe9>c@PAZ~|@YuBlwzq97)eZiCMj0Ut zT42c3pntA^QUNpp1!bfYf2k@@1h7*)mLePkZ{Rbb1NB-`I!UoQcTw1m3H_Wwq@#tv-M zbIpH|@842mfNYo6Lco>l?_G?byBMXqeE<5z|L=A$I{~CJP;1&`{tX;F3c#Ve<#R0l zt;XaL1pW4BSVVto!FwjX?%%rrfKQw(1C2?Y787RVZvrCw0OTtU=6y&^QXroMMY~&C zAw*Q+G- zBO=pb-lo|X1GKQTCXAX(N)HkK9rQcbU+r}&3utL4f?yj#4?c<2=l=I#>Fff~*rYV& zGe7>DozoQHjJF$i{%Mzh4JIa3hGZ?8$QE{t@Iro%iy3@#4CZbLH5^8_{AdcRM6Je;2k|GVC5bkqH z#b_bWRw2qq-}turLm|` zS^q%&4dMi(oN&i46uP1WcjC_f%n6I7_jwMZY31b2EJXF)-=apEYzVV8BNe8S%!sJl zhCwFo(;u1-+n{!-k9%K)VX~G3QA7^kA5%)Iau9@}j%+$Py{0FqghilQo!F&a955p;*{c; z_G}L7l1fJB4}vX?I!ZFPS5E(+xP8fi*m!ZuF`529Nh&xeJRUv#kO*KG^tx^?svr0D z^t{MKA4{`xaMHtXx)MDgau{U-o$(kRM!~zG(!x%9x!L#3OawCHIPf?RhB6oSV-+0WgU7mIm7$U%~(3)|& zGQFW4l*)Jq!UA~J!ap$#3>8exr&m^Bgbc}74TbQ2s>R;j5Q?n2P5#-CoUJt-Y}r=w z+m&OhQf2P2m@^e+&ZM$5)q9@M-#Afq_)rL<%qGXM3&f9dk(Xp&9o<h0mwL*rgH^~ z!J~WL$pfSGF}uVL5i+*&$mB*CPzT^qocsMGD|R|O&^-udN_>N}^7%gS8_Gx|X?yN* zb?Mjg9%_1CGw^BtvUXcU0sKlm%>hD>1z{||Si7n#Ouzk>Hg+xtj7)rBYj2nMeD5kU;Umy}YJln~>IV%yzsIEt-nZz*cht@p`QDZe95`N#2D@n z;doi&#|87cmHB+;7a2dFr-kOpS28Zu1yCj6_d%XJxZ$rjtrA;4Cz zCpm_Wyt!afdXX8nueYXnBFTkllD!p+Y#t%uxJ_%ptPT^+HN7;c6W9ro*uRH_&GI~0 z%yF#-J3ywfg0t&_$j7Kl=0X2I2QL6JVL~m7zf=PVy(+K};A?E8_%R6QHlHB9@fr`4 zO*_!l97&<$8^jhjAT1(CPl$e1>FLv=Vka;1O?JQM zaC_MQ^{cn0I+gr2=Y7}nGOO)oeznoa*8eXsfynXn#X2=UxVn#$JuEfXN3r%Tr&Iqp zaUBe4)wg2N#S*2-gD;lY{m`C%v;Gbl%(rv~<0`}+KI~Q}c#*E>p}LdFK_;Wk)Cb92 zJpEsc$GV4!AP?bFHOUjp73d2kDK^0N;H3|4MG76#m3thm4Gue21W>Sz{;7v4z7aSrn zn-isLNoQa0!Qil*or@Q$0b`jiHe$tu+n{{->W)XQlVyZc#N6iSfgMzSzu<40em!7* zQH>li_J2|5jsO&H-F=4X{?W0z?;zhgRJy#hSNJ}fMcGaX-JP!A(Ye&^>)BG9i&wW? zU6S+1a@xM64p|{L{lIq@;?loy!rK2U+h@wnSRC#s07xF2**;4XHT!>|2OcdpTaynU zlncl{q~{eKLKS#_M-*bRDQLT0SEKV&$QXIDjikM$5jV~FGxyJX`h~DT)px$!P=k2P z8UE79$xlBKJYirI`iS&YjkrvV52s&81&7P#rdgk_bel0N*9CuN~xi;J%ySK~zXeO(V{AdgB zVT;N+5iuVe57{C>eXHLstfzx%qEp%9Y>FfL>_N(?xT|zOCZoj{8(Nmj-%D53Rhgt@ zTl}z$TU@AJ(q=NR6cWN>xA4mk>-tG{oyB$;-{~@2{H;9=hGJaC$eps&;sy;hiey5Y0gcF~|7g#pGsVqTx4xKJh{l~>Z=WZXB zd7AR6w&}kdmnLK6U%J2(0OI9_WIQa0MPMO-Z3h*=HYK`yXmvqPl+AR+Sx>{>Hw|G1 zWpO*Kc@Mt{=D72ty9@=P1w0xpw1}J@m{2xH=MsPd)a1|Ij%e?DSojRe`n+8_NvYG^ zV*^qJE-%EWfbecqR?KR{Gmawu56OPpr6ZHq)i_$q7=F4Pj@{TB;U6y;_4wK2b-xa@ zCMAODjZ`_vLqa^<3c_$UQiUEnADx*e1XHCxMBszr9U%Mve4Nm;Q1iK4-Qk7J)xk#J zOYUK^xVwegKQ8j;1DYZCrL3_uJmd;GhQc;BlRC zlz>suQ;H{wGcjuFdB@d|n9H78GzuqwQ)W#!jE>m)kz?gyN@q0rlDOFkWivliYQ6)! zm3xC=OyoPU8+>2)O8I+&NvT#8ua&0sh+{ow<#eI*(uo^lpLW+5Crw6jno!}}3Sg;} zD%#deMy0`sCSU)w;s}zk$Ihu+fpKm1P61ZJq~saFK{22Yg47rK6G%(>Omp|=kxcGc zS6wahCd2^g6AB?24wN`f+Ac9aKvZSZ&cjZ1cQ3$eB0_||QnOKG3BT4jBH_38aKn8Z z$R+$G1fTA|kJ{6+wc1*uHp@`~p3Xchf=4QxwK|Grs>VF!hnT0F!>4B!UA$F(Fn6G~ z+TjL1vy8@A+ijnEtw+>^L7m-v(oYR)zA+Sogm&)oHXavSa#JUSg0$w0w9U&BylyZ) z8^2o|XXH;KZjaIH*`n;Z4-lk_hE$|BKo-C6t&mm79Es@xJYB%sdEao2k1)gKcxhU8 z_9sabdNK<&gjf7>eAHun$D{RdNdYYlsczU=TsDD1UydZ8 zQ5a|Y=9mgkI-lUhzCrKe6(OjS=4S;}-`**Wph``*Yj-9pX*9$*#ffNAQ|-BQWrc$A z@JQkl=R59A1%{2|EiANYm}7j4(l9>YCxnY|A$wZ`Pdd_=f||`Lnr+&H@UL%OThOi?yIQZ06)0OfkF=WAd`(h zUt=;*ABNarmw>Mq)WB{U)F7Za-i?v-K5(`NSm_h0OfN-D%9--55+IU>2vGY2;%V_H zIP$@?;y<#U6m*Y)U&Mle7@(H;c>RUz(=CBA|A#_<2v{FG2*H@r7J6+u(eC+b6nn{G z3!RhXQkOWViaayXmpVpsM=vTwZ6YEi(vX-@#H9e5<1OA~8RbIwAbUEW<2!T>=FlOV z+09KR@9&1N?8KCM^)k|G*rRv|HyXn`#Wq7uH`MAbm&SD`fF7RDg-q)+DiWZ*n%$!7 z^xwVOdrH4Q-a8+CFPF|5N$)XD~3>@9Bchg7CN0oQqNIysfg`mjAan9R&*3V zI_v#*RnhTH?>qgo<csBJIX6ICiflyY z$;y+HL&XqMy0QOWCV;K2bQ3hpD^Xe;e3Db<&ppjjgpp-u*NpcQDka@LY?ADcG!u0o z>eWJX3hq_dZ4Ges{!Oo3##8`&^#)w1eH9V~Y@=jD?yZ3vTywiK>~b^WK+q2$oYEK!VL z?YJUl=*kH=g}ZHAzs#p!dd*9qZJTJ(j2zZry1B^ZBlW`L#V0i_a{Rb4?%A5vh*e|= z9y^azwt5FB*x%=kkf#e5IIlb1hSPhcaE^Lpan_@Ax;R-L%2-&rE~|N5Gg}G=#2S%( zGl?4DzV95tH~PNzQeDw2y)!arYRO6!F!V&fzIAl3T%jcsa9VE#3izu>x@&*N>}dfX z#6`r;casgf7V^}deJ86p zK3P$c%8X)9e<~t-&No(fux|eH)i?TecuV1Hn4O7wyikjH8mZL+j@}h*-PY@sDy_b2S9vi~WUBYJ^32pcJjKQhoO4lQf#!A-hF*dA(``R?T_@M{q zBHNB8{%AB;t0>(XJyqR@v}Z$>73jK4OwTy~+CNaCNd>JLYdPD*-#U`yP7LCojt^Yln$st1{txWCHsl1}5lafrhP;?5$XeOqs)#zLQ+GNtE; z(RB$!JlN^bU_jNZl0kz__%1Cbxry!LgrynF06FiS6>%A)DCfW$tuGa)pI41IskSHM zfniwIFZM~fGs%gLJ1Zdq_EZtiPom0zzNc?{PU)Tk*AF9KPr;%w`R?^3FG4)kkZ3k` zj~w{!y^Po>rgK|K2s)hvBf?jQ$hY(5)GZxD5@-Wjj8cGNrA)C_-596b%Ldy1;1UwK zHg6`x@2~`U;ym%fn?$}7q~CYm2~vrFI|jZ?O5Xso9c^SoDZkf}^lR98|B{*ma_s%v zVYLHcFW&ul3n+5a5bU#B%kI)r6AI3OPfQD8cD4+OP8lr#4d+J$yg$r>ibF#@pULlb z^H@PybaNfgOzyY!;rXxD0=`XUa$e9!*bLWrj=)#&Ai&_x=27y`vhxranaK=iStdz{ zucW=R{Eyh_29gd8#27`)CgyLdOcAi-a;98B!l57>o9Z-!E(j}mIl*U{1f!8?o5YP)Q+#J zM3l3fqH`sVE<1e*xw;!cwZsV=^WUX0sHx@%y^Zlydf}v@^+$6#v7YtW)8W%dLxRK^ z>a!pfElXsnLPjcJd2B<}W)61~^)6~5=Ux!Lk^>98L{qRbsxTQxh=qOUGV-XASsn2g zM{rTIf4z$Q+BYpTWrHR}961n;BeSKWQr^;rRf6yBpC_n#9_Z=&ir;?BK4qiqKUc5blURPNv_moeU+~Rx=;BH4c zUGyYw8t-RglQx-8w=xWhY+EExj_*jpzh;5wRE=Rt%}vTgwgkAmcIu?J+bwUCA78S# z)9SoblSZR#%C3201vsCYTP(lYfAW|0MkksmCI*a&O;0-n>u?8rJ06yMGCfjBr>tS!4a;H&zz+*u{o#Cc4hk^1|X4Lzd|&57_M6;>+?K_nD4Dc86hP|qd6_gb5!Td zatVWKLJrzpOJe{+y6cAP;YsAD(A1Tb9N*2~7;i*{e3q<`h-~=p2snzp=FKKE$DQ@e z=MEwC&|d_-6U0HG#3_fZXk~5zc4A5rFaoCW9dPrzrNcQnlt-0o-M>Xxsl)jyLe(V#;C`>nrw(KxY-zMyI7l zuubp;yp9vDS?zWEgIxzH$V)aVW@d$5zE&eQ1r=x&SpY4VaA!Gi+7R0=iy$2DK9KOY zol-u^ZtCML))Gw&6|DSqk)J$@H879-Bae7pQxq4DE=g_StK3VaDtA2oS3=&b?x?#k zPNlh2W!<6voUE}T>pEnYV;2;NzAJ$cPDKy%AApJ6=qS2p*71*75LEc#cH z2+VCE0b$7c9ut?H8lt9C_`3Tyb_as}M7h8Oh4QlDFO~I&tlFxrq*8p9x;&iYlO5~o zmLwW|g^uK(9_q9dpJriUar&&m+ghUvl5VN0C(W-Qc;#FqtSH_s(Uqn5^5H_BNBwz+ zKA1N73Ffn&zE@diYNbK%_mIXk9PA<65W%#DCtwM{Dl>-E2h1>TXUw)aZTpEV6t3C; zf5c<>L{V-(^pcj6@gVt4qZ$%h5_VO^fTGJ56dQk?v#=lylkJ5J8IzC8m)QLQ4mlaC z;>j%ExH2E*PzXJ5)q(ZZ7_p2-LDx&7-dg@(0FLWbQxust(UonXu~XJfXN?0&T?s5GPr;pLD7o-!Lgxoqx&?rnpXe?0=7G8PB%kHPBr`FgEBD=K_1r3V)sNb zlY*{&PLx)T_z8oXJ1>SPH3VssHpU30eh#O2=|X5lcj9E^sawsR*o|+CtJ(%T&V3E) zvvD^ko(9{bmXC0Ein9bqt?a7tIMJWAE8?ZtZ+okeUs!H2c4zP;q20 zCq~)?ew0~BrnNz{ODs4tSTyG?A*B=36pJWR^($wQZY5Mnks>Zf6t$Y$SQ^78_8L?1 z&KW0~w}ju3g+5)V#+V$^7APXY7O21peg^!g`vD-_DTNewwv&jzn&v7xuQOn0FhCm< zLoKz@toW6Wi32I0t8!rR~P0Q33xd}e~>?8NGL-;W5q9Mtc%JJmum{r(WbPh zosa`)#u%;@qSxj`3#jS|&-8_Ql%UG}0x_y1L?Q9yM*t-ip!Et-<)l5kxrC23O1Wy- z+nIBkubx0G^7U7ix8GP0jjm7VJ6Y6&Ges0K;um3&Zy#=1rG+Zz3`pUfI6J^JKA&|D2u;cBoNI!wWzJ;2a!C9m+Yz}2| zso_VtXNYR2bGQB(FCJi<6Xyd8E_$RWC1eGOC<4ZVR z(}ulO7wy%bY#gb`Fq)CiYxM#dr@3sm_IiOi`Oqi|WEOQBwvuAQL2a$k{@NuAwK^#9 zQzxOJlj6z&@9l5(dvarD%OQ#=b??_|gG+^c9KPK-$RrRgHk_4|9eY^l6kB}2ZVkKp zBKu_Wu7|A7H^)wR@^0CLSrCPalg%jJlQJCt*LboLe4nPTQpRDUT83m+N1T1pAx|X} zTUNaah|!i)Ph9@X%y$Bpws+!x=bio^(%vd6uC3_;JrEp%yGx_NgS*pcaCZsr?h@Q- z+^unUhoHe-f(N$%2@-YP|u=LQVXuJrm`0k)gqC`(-xfzKPP;i zLDs>g+CJ41P-Hn{`M5QhkFY8^NilKUkZSNvTE%40NuM(OJsXm0|G>-nW8cJzEl*lvd+8xrci4syIiD@+JphkNoCTd9|V}8%eWH~A}>Og zMY3ASX2VpH>l^YIuDl*;RKxYj0zasHyrkRe1TAC|BqHIq$(6N6tyW>}<+|F$a~9|x zlOK+kHDtm9-9Nvhuc0Ol7F4L%P(7N9t`OiI5@#SAe@vu4R2}ILICGkaLkaosO3A?S z8;_bFglZtZ{Q(d1y7r(^xBf_A(0qFpd(iv5t$U4Uu10SsEZE4Kc8}ak zyNuFR`?dZ$x_2j4^uIHWDUlTdv06knMkV?(dF98p9Rer?9$)fp4rvEzYQJ=B)4$l4 zKm+`4C<;C`LSy8lsWg{#aA8J200K`|h5sQr>I4bdJCSe1=kw~a?c!lT#BPYxtw3lM zOV|pFnG!>)lajW?*T^Mc%?SY68*WEabJ`gAp*&#Y%*5B92kT-&x4gQ{ zthr_0a&q2kvfM|1mUF!#vA1<8-)=hSh(Ybbu_jHHgJT+A*KC6z1IDg&Omh~n!;Ubm z=2uuAuR3bJ$UONepvBzP|UMk8y%2YF35;whFCBO-e`QP(1v;5xrHsbvk7k zD#H%t^{eCXV(tJ*-W*;S-8n1@agB{slP-Pod^f4cHm1#o4dv1 zbqLSH3;mMC#@zzBvQKx#>;)hz^HogMJo~@*zjC5)9(gLxG9X#q6^ir{d$EJ1d9Fi$ zC&Qyxega=nwUE09Nz?Wa(FkhwcbLz@`B+Hhok)iUZ;I-jO2~=XNp-?%V1Ysa5m{^) zAhmS8(R^O^R2QF{G?_4&S}OKVs#g6dGFFG2QyOFc!5~p?OU%|;-=%Ljga3T(=`{u-{Y*$dLBp7HKQ8SaGR>moRs#(bRCVha; zz>yJ_VprciR#5P|S~cmG&8O^07_kfCi0XcD6jlbN|N& zCrhlz#4?lV;2JsB7xy@Pxo1dox>^pqpB=EJ6}B0delxEU9hU6`nX~Qk${9&oHq*k|{BTwH-E;%5&$t^0@@cZ34_YI0KSR{qO3}`?PI(8^{I17z=E- zPM(t{*}uuc#cK`HJB3jFo|QY6B4YUjqMK#5c_dN-gq$><*wB|&p9f5 z!jy_dw^vVQ5N-Jqw^F#g(HKZVdq4EY#XIoxKcF;I7!Nn9Appx3k|k0ylf6a@=~SUq z!VM7e*m3=U`PbaF=SR}e6piBJGlEbcgQmp{6FtgdphS7_;PIn%$Rh`k(Eywik2gGQ z7xJT5X1ncX8x=DOM-~r2530~R{vF}5W;zlggWxrc7f@**?=PIXG&h?l{C`t?v7}WG z?Wu|BVlN-?uLbO)XeEx(M_^Q$?eO>K#$0ylx6|$ibF+8LMK*1ooPKcJB)OPXWM^_% ztt_RBgCu!>QLYZ!%fs$phC~5zMj*C--+)i=Ca~$y`nF!#FS|Y^VfS10m!FN1tB+|M zxj$4nQ_b~)bBMDX?cK_JwKK|ApNhDQFCP(Hf2M>0CFyq)Q}yx@d_uksPClB{gB{s? z5%1j^Rw%yCuvs2ouu?F{>nb0wL1*jmkn%!X@Kr5o*r!!Z1f(W?vxLlHjfJ;Ffo}M8 z;%#z}IpTwKbzccj&2erA+JEPaji99<$$_1H8~1-5>Cdo`548slv}W=`-NAM}u&mH_ z1;7@;d4%X==Vtd2Eprvf@!u79;D&EOug<=62kgHAW@C{@x-c4QbONzqF8HEHr*HGE z`B#3glPU&N%EtI62fl4&M}Km{fbM85%IQ{ddI`nql=?SxjDPhY#Oo@cjPH^1i zqn_TFP!vn=gqXsuU3w>Te-ku_qxNn!as(&gyE#wqM=DwJC*$0u2oM{=%4*4d&AqZ) zsd%!hRnY;NWMm|OjZOJl!$U9N8L26wp%l7@bdFxlKJF=2EJo?4c8u_XED-l)!PA{OA1+145D*>(exBo;cB^DL(SEBMe} zsZD58HR{*NLv(6Chr6Ibxngu0TO*=M79tf!FcpExQKeln#Nf&Vs%s84mO`%58QS-7 zS0Cx4sBJ`%e^k~ouz@SNW81NA4KqJ&#xH(@R@b|~nKB1f0wtEYIG#IfQK*B|;C?*A!b&lIKVW&U;%`;A#&^=Hx79D0xGd%N?U# z$ipbS)=dL`v@kBM@tZQViCyGc>P`_IQvV9@0pvTP{YVT%L`He*2}65Kl6iny9Jo^% zp9)w|uq#;+?G*Vdn^WGAuk`lir6Q>(P4u*G4$bBSBaG{Cm~|Uy_*JbAW&DVT5W*mR zch8?hUMI|7ePFK|Q)^V7!OJ?lDXskA*PwF*Q&(uB%dB)Wmtio@2e&UQ zT)jd5sG`X|nMyttz$T3{AhY%rwPT&mdi?10*!mi5UvNqK5IjvT1 zx@2?ILO9+%O#ILw-mpqDeX%}2*}dWA4Em#%nyy#DW##+`3&qk7MSS(HI5L~lu}*m= zXeV_V5&>Sh!XG}xi%1V%CZQF03 zG4-4mY`hO>#lB{sZz>4!GP1Owum4a}e;SIf}c1|ls zrDC}80A=W{SI((1atnp03I3zE43SWUOWA8$3rI@~r3y#>W_RgUdoYf&W5z_|JS6w4 zc6v+u5ZWziXI$g;TCdu=-K*h$JfFZQNT^q1g%EoGGQ7(2AWu##25~1-0;Bf=hms#P zb3faz#>(U7jzH&MD5^StXvC6oNL3r^x2puvzwK0LFOJN~M2SRGw=ga?2o));lSKqcroTc^c?Y#9 zn2ydae12X$HwKKP;cRBdW@;V}%&YF_AsweH>8lQWu{bh2WgCDvObJLA z$}*L_EHRD^0MYyU2TEn8#6?Tcg52#nOhObQHS_(*SQs8`^0H#-B(%ndj1wMWO3syV zL>E}MF)=B=TxWzy<@f0mM-Aoj9GU<+^7Io%(Zl>>Ef}j8|FgB-K+8@y+CgB?2}OXq zZUuJnZE;Tv?pxmFQ)HdmzLT4V6Vt^S()ejzs@I@DtnQG>YXgZG<$Q|J!67gB6{yv* zlp_5voBa-{_QUGhYlSCNbkVu7F`w)_HIHBg3Ew}3GGbFYo$lO(my;%rZZ9IYa3Acf zgsDMJLlzhbxCn^QsX-|03L|>yFZv%*T;$CbJw^&6p;H0Sfi?itIG`(?AhsHKCYe zBn(W+21>Mk_^zY4m|hW!T3)PK_tt5ij@f&+G9$jcd7Uo48jJjxL1J|2aTF%=9po2t zE~nUJw^a{PrWW0XvSnkdSDW&Gi^dhI}LcOULBt#oeR(Lw}i0f>IpN^rL0O|}`w zIoCP3sB!@YcC8pNRlGZXsc4Ec&~7QBjAvw5*?+8|zNZrW#MN6A3v1Uu92{mX$7L=q z(Fy~6I~r<*vEad(!2pC+g{5!S`a*wM!6-3n=<_-6d^zpYRaRD*AtO?0fD;|>2XnjT z#b_F`j$k89Yee;`RFv833}&MtX#p-4a4gr}fHO%oBs#Q!;X?`o-_(&P81~#OgGrHS^ydZ8`Y6yvY8Xb2oRGz_+ z%?YBk2k_Y*y3_M^+6+5SCM&U)bRsAudTk2yi{>n#`ydF@DU0%FA?`*=@0S9JSTh)| zXD}Kf2SvG*i2-`6(TzVpmZ{N?(2FpX=r4*{Ah#aZ8K1Xv9^CV6%Z`qqk5SG-x7PnCx}tf)v7F{BLX`XzAYzV3&=J+~!aq zwWTsc|HG!_W(zfsm-bIC8hUs(WHA}^H~$S?@~V|`BoABVmf2Wxd3(fTo$$z{TxN43Xy!lTx!&%H)cJdsUiP-%^Gi7FVfzgm+eO#1+Y;tFhWQ+F6D zq0Y*2rmp=O*F8Ck^+>0dOoS#-Ao& zsMm>MzqeE#t)}^TypL0l6SjYj;;+$@y~CQ5S=t@`IbHDe_kn~bV#)p;@Wl>S{i2-f z_$#~vVA@UD4@g@#iYb94&X**63>~b{KWuD zeTHi#@z2Id7(9I#5&nhR@SI*-83G`<&HMqkGwf;~2w@KiR+n0TIMe&-WGZIc4XScU#x)EO z)$=k-He9sgJ3I=dR-jn9*ww%QA=B728q{-#1u)?)Sd#dFn0KVf@L3{Wb(Q_F`?*Lh zkwp8Et#q0m2_ec}t_27QbRTfY?YQW^zBVjTh#2=Maq8pT^u*9UKk22L)jl5!dtgZk zs0Ib=S2`)jXm{}29vjHV73dM`r@mg47G{?w!KyUisb`C+0gWcZ;Mvy}m{1pEv@9 zMzSGeDDQ=Q=peDfo6dLr?;IcG%3=5wq{iUIi~k+VpTT~8g1z81F-&1Z!Ab!&5mt?% zg3#Ptx4iW2S=g({51v;!zYArV7U{cLd<9|C&*Gf6kWZL|@N@-E1|7l0Go@QdReYFK zh>@1djh7DP`Lk$9PDUeU6$eFX?=jacE16(Te(mr6@OYdI^!~zTQ5byx^_@e6kl&L} zL?l$-2XPqSOk{Ay__&cYzloT78iL&2wkM`Gy%_0i?`}BXDCLj#558R?)(T=_!Ke>- z+MX`HU9J?*VBVY}UO{@!V-K0eoHj`rq7zN^pfQK>Kvey258%PIXIR9#VPt4V&NQ85 zytKlnq)j?4A$)k$HM;i3(TsJB3TgHEhUVL!CG9h2_U5LeF2QJ$gRaV(%8E#%-X!`F zsfTd->3?y>IQo|ng$QByDGLDZgj%s1vJK%7aZ7?ve==wr5dTU4aRd`+E;LK39 zYAX(P8_c+h(;+%(T7kuR47}#@P(cl+$eiOZ_?WPhv_Z+P3;7k-%riAuU8Gl1y7yD5 zcpJE<>SCnrdH6oCL(IOljmsi?+NZIQO~2`C$80{U)`F&bm49U!2ky7Sm))H5=rM4(6Ow2x}{>}Om(ELc*4opmT7L97p zM|%|iFsf-igj_Rg z_~7!GoEY`nBMa%5-2e|J0~}{i?GQ!9;d>?d9ya&yU0ES)^jn~WwkEyec`cF4zTL$T z#JWHZ;}AX35SVpoex)bZT9wszO=k3Y#j%?f6|+tfKoKcWG9fg8`kw3`?_VB;E&+Ko zHi_JFO1P^w)=r)yn$-zGM;BHG^CPVS_suAGQM<12d zd9?$ntvh&}%NumOiR<20e_@-W-HI+jo|k2Wav}gb*s26(On) zia6?XRVWPp8rKBq;)Cz>&C&B<*~EA0hFR3H3e~feq~sVr?AAxU_;V|A!3)rXC>E-Q zUR0c-Vd>x%8)hgNMRv!_@b{t8XZ6XAHaG2-R!setVu$a1Vbq$xb8T|KhdRqL(_l*s zJDIh)#{TDc)O78Ccla?43m%n&!U80auNue~o>TKXez6zNjmB5t`ya4^-=VZ?Dab5Ma z%;?S+@tEv9L_EO&-{fJ15gF%{!Gj61@I>cluO5q5vYpWAXHn9uZ_DsGL(m`x4o^;H zXhw2{Wb(W}r8lHO`W|p9rW+kyX>V5XjyNgpM<9hQsW{sJ?7K9cE{- zT{s9sxO$<^zAhF1QEncFk%^{&H&01AaF`ae-YXx~mTLMK7vB)w9gTe9S!qE1An2^5 z{7`E0~72njI$~#A5@lfw#-dQ(g{Bb^njk(HuF&wSgwDM z(^G1H<_T*nIa)|S6MS5}Tta~m3+GUXuRYzp(hcZ*CS}n)Rax0DVfUCv#XAmQOFZGy z((P`^&W4=AkxN!^_a>)N5axE93QCuez7TkSVjTJ>i5~0V4tG!QTiA^f8S!uE20Q~O zmrROp>z-$hE7d%{!JA85lfzqvIYnm5F`5Fi&8NXPA}mhD%c4?JtPfE`BQhU|TV`)U z$j-qBVrGefHk2(v@PtC0G z!&WGtMmboxC|)N96u15a9)#H=qw6A9xD21^E~4_ zU!%f*{BH4aERYQWjRSq3cik~_JC02$Nbi5(rKhn)_;FA5?^eXfMFj%bndz&;#xk+3 zdz~pACX^rtVR9xYUb>b3>MrcSuOc;;POBYtyoO`oTfqb9>CDncJEbGg88wAlol>_0 z&-GWqLkLtrfH_yp&gP8h4d?yoxQdbK@;zm*tH*UIS)FoSy9tBw(6jEOq}fofuUiMWFACLGg>gI*Tu?t`}dS9TjhV zUN&tL5MFzckBf_&*}C^v*No^E69P@qeGR)1)vo&T>>yNA4F4#|%bK5thVM zNb<%M9y_`sH@`0Eb+j6!D?bUe2yL47a~9sLpB_?wNtbcP7Nknv880u5e7pP^h(+N( zBN5>sM+?uGD1eO2gXqE~5YOMcJLw7*l*M``;g5KF0ej=pBB7VUL&Q`|@ydH|@4Uh* zKI}Zf{d_L(_j%jn;`#n#Wv+Oxcj93EgF&*cc1JhS%a8a~wj|WHvxlFy+r1;Ejmdj8 zwMpxoBK;WvW_n~~AQIAMIzAL`0?lyz^#2u_B@ntrLdnHfr@g;@pB>1|2e{4XP|8$H zggV+c`q^l!$cDoLW0W6{a-VoCM*O{Eo_F^f+QsQpw;|E}+J+gjo??%3gBOfVR85V5 zn#+t6EpfC#;V1l;wVx(|=|$MlAE_YgINUfV<)z-c=TW-ITsZ z(y#HvBGXx1*`$SdaR5YWCK7E(1T)C|<0-#r+Jj$KrK= zPWr>T>!i-Y251*eu&?#n)I?i9ydkuQBLR_=Kb#w~s(R65yjj={I2*}XPmA9@M#g^qE8*4l9ew^i1p(!u8 zMcrO}qO435Dh&O4`FvV8(|IJ``v~=X7pCI}6{uzC6Ph9RBThkc#D6w@{rv~h-rG+x zTSM17zlID87&TqFBer@-Xi7e#7p|#BNGrZT#526wq55sWUWz77ILtv(l7eg(LRfocMZP>9L< zWw{cXy$#|igfXsL`(bMr(6fq|HH0#4piSw#z`#A;*+_*CN6#-g)`e+3`eRQiwYuog zpa;4QRiU%)QjQXH3LAI6Cp5=@xH=iE^8(Fg%bJEK!E2JA1?$*CTH*RX?H^uq12_Q0yff&Q3d&BU!Vmc3}AZ2F5cnyjNVIX-UL&L zdAi*)(-D+DoBIyaojAXElY1N`@>mAci#1iqgo=e+mB=Xh=v@n(gpjlJ>mqXDz4MY} zr??q^pvK`n{%fjfp%}D0#+E&x?xPgMGv4^KyM|;K^q_O=1dq zPEBc_?=RwJ4LW}HrejuFVyeH>Dlib~6BX#UaN+EuFpvVG^(66@?+9&|h_;=i#@-e> z`bX?45qYghGO)8lr3f(9IUi_qXC^=6u`l+o+ zuRc8Q6_aCGyAnJ=Ij#^>=>B~mY?#@rGyvBsfFRzFjMVZRWRU^K zuGG;E=hbt&bnOh<4kpHCVjuW*J;R49C!ER6;RV_K zHX-Mh>?Kol7_&O7vWSv39V-hyRW020EGCHcIb)}b{XuqLi5}{Hu?ZJ7moT?F~ady%OdNEBU^$VTNySO_zUZTxaJC8&%+5aTaU& zvn@WwXp*X&xM|IH_~8weFtOxV1IMLinW*V>M696Hqr<1Xo>%mF1kgR>*d}B8akj)} zA6@yx$9!-8G4HvALhX}Q=X78XvVvSqfvq_8*w8eX$VX$*dOyW^ZJ9yQA5cN*H5ZUJ z9`|kDZE3}NrDaQFpzP*cO!YOAI3av&$Yk_HK@;+6{H+?SSNXt{+0_t2Tbs=|INwbQ zfjkcg&X4~Kh@U-Qy8{N!zNCL0mOvKrM+x|TtlQC`%R!Hm4v0^3SLzTCk4nG5eh}<0 zj6*_lprLrhPCxTe6S^8E-_?$Ev!*BbD3HqA)7}d=_0jY)CdXDtj}`dk02$7L#*eSN zBg_Mj<&If%aR`v7^RcFkk723a%)OuIWQjkvapWg)9KE0SQdC)w@jB(`LFuAKR#^%|5V zdLpi4+2Y4)cjk1^P^H{#+Dqy@*5c zc=bt^XmIRKx|ETJ07c4r0Q-_pX*g7oYo-~uLJ8(V5op%mfKl%qrswFt^UEJ> z1IVet5Kp);lwgENe=a4q;Ow|=3c^8NUF>5R+x>&x$fNz z<%`nf#u(OO*xrjoe_D66_}?dr1wI*c=S&i&iB;jpw~rJjL9Q(Kk{xKVzYMrQToME) zNxMlH9WTJXc2_dI@BU)gDa&($>CwOAWPc_SDf|rW!o#AKeIfu~&YfLrFT~LYyAQT^ zeT0^IjdlQhiSvFGa;2yT;jeHq(KykKhspxi)zv#xwHYERkP3vJK0Jm0|DA9#X#T2< zeuhZT0ECet|EauD6vAF|;dLjs>H$?Ih24>O*L(x&gHK5rQ%x!&0Ky+nz_es7M3Kyc zqV+Lj+#za`P;O{ds|6Abs{tUh>z?w zT7DS6Y%lV)OBRC&6FqErW%10*J?6$u-J67$#NN~RP?O&o>7Yi{DvBXKH8z;NPJPyf%e%NFF2_;gro7QVbPq9wet6Gw1-xj zIg5%R|GGDJEZ4H5*Bo-g>(J}|(ufUKxb|KI#Tk8umIF27?iUstALjcp7pj?O%-7iR z9iKjFaV`HtRPXJNROA2pu$a;O`CW{|Ru3-VqWmf6>`1^Y zX`jlF-pd@U-640OBi9(Csvjrh`s_=FN>&fA$HHosZ*<3yth7X34}1!Uy1!7h8MMFP zTMVx79-9Dvl$V}ZPAl38vsGVFs-JapRAAHCDD03rW^~i2UQfXpIp6vexBR{0{%Bt^ zlehcPP%aVYy!eYKzq{ZSw)c0@qVWAChGeF(g;*Ilea+|}OYd`zG+Wa%W9NL9tS$J+ z-199Q@KC$V-*ydK?KRYDhq0ez;TJ_{RjjEwNUx;ct8K=`L6X?)&z(7WaswDw~ zjYq*L;pvDE{tsOK4VU&2TT(kXSgqqQCJ^;8u$IyR*MAf|a_v_XT17SfB+Cq)W^8pu z<2xh}-2m$s^YS!QrytEWr~umG{+>?2t`%Rgea$W^pQh$dFu9Pbym*u1VTNotYlKJacK?& zGvn6fXat6>VoLs)B`uwh=3G2_J742uOhBli>k18O!9AlTypSSvpb&iPsyy7_w@TUZ zA*>xzA@~{ZF^ZeVM-?*ZBRsOnLSCvQVodaZL_raGh`q2?*Vpm?OE7~8pM5U6!{wOU z&17}WpsZwnI`99NjaZ>xt?>_O-q`=ZxxCHmqtor(Z+{`yAmlm%SZ^`K^q&nsPmrU_ zLCIjoqZ0Fwk4s16vE&j2mNuk`Yb<~w3MX4k#WpY3>V2IQ11-0OWP?ZMtwjf6!;MfVC+`O2adEW{XU&S)BK=7ll9*u~wb8lm~iBZpttIzyY zyjPZi%|Hfolq5A!HHBpNdt>z1CF`#$T$Cs8%ccu8orwT_3yCoEy`6~FwXjiwm%Mox zrB`L^Z>U9Bp^ht`ELYc&;MI?mHSJl^1JG1H;uRDW0<)kZVZdScDtDQ6LYmPn+|9cd zz~cAezA07`IsP0DmcjZ+LW?ksyvSjFXhuXzvBkCa#9N`OrQS_XDN{u#8d5Qz&qgJ* z{iqp@VZcW91oJVj-Q}xA4DT<;l^Xl`Y-OGyyC54$SEpeMtI<`*R?}Iu*aYCaTMI_lok-F2jYl2Nvovd zEyF8XmBO;w#}eL@;!MWc)x8T2M^Dg8_AAjy@uPV|O>`mMXZt4SEa>cpw5?a=AGwli z-bWLk4W!*-Tr+x=P-Hu`R`m!v!gd;<{_b}d+8b?Sxy6G`ok;a)K;(E_3)AHDFBuGm z5=ME+3^m}g1}m>@-oESDv5?)<&6dty7OD}>++%)Au&eP;U=^j_39sn zY*N^@TQQU}V+3vkzdoW7A08AK^|Zb%r#N(a_neR$n=P-*ZRA%%XeS+lEs^1WLO&lr zkv*KZdN$koPWt;p!D%Z9%1VojNFQF^m1L&P=le#P!@1h1`%0NFn0Rin8ZQB*5m~9Z zUSev;oO-rWf!e$Lu3Q{*Z9BtT?)VD~w&co3%*rF1ZGi@4-P~i0MXN@B8J`@#Ba#u3 zP3U9jAI#@ec4hv<3__F_ieof1@uo10DUHFl5{nuMS6-SDuck!6!uxO1A&BR->7w|p zUHd=tr#nM~0$nfnWZHAQt_~C;BL<=8lF_KR|A36$*WNx)-%qRmL%vNAamKkOGO_c& zvnD}bU(rfSS3BpQujGX(Q4yJomJVh>1_lrARaJLp$u!i#S5M^8hXL~VK)xaPV#Q2y zNXE`dZzT*O{Q}~pXh4noM-mtV>*ilOAbU&W6fH4>k+T7hwm*@%`L@3}<-^AM*!J*+ z1cLE&y29V$nH5H|7oWrdsXEZ1Or%c9s>k$~|&@!wXLRNT*Kx zik*6nSmTMJ{P*9VW7MAU-+W;WEN)#)5T1KdvX_sq+@_G|$J!1F)r zJ^{o*<>(1wkT9T$VI%i!b^46pl4C-p2r~@p-?G&;YKE?15nlUvH z97|&4R25qMAFvL{D}eMn+X>7yRXd3NbTu(6`PB}UdAwJKSV}{L zaphq^V0Pj&k(>E$*^8^ARl1yuXa)Qb35;U?=gZP$Osx_>DwRy zz)gTrHuz(v!q>MwD;2W)S9S6;jLBeaiQ>++Ti(QRBRa^#AFd-jsjBT1~%4kz6D#$E`|Gd_wZI7WQ z-(S}WZH{IakK(DYED;g0riD7Ln+ws%RdydIA{`@bl1UK+Zd$SSJ*K##WY<-i)sJE% z2z>D{o>vu=MK?tRQUl0wU=V;OT4f9yp7;Bt#RQYXo-hEr933klMuKZbVpl9-f;rcK zpwWQU%QDXMeUqCc#Gfmf-oibjL+IcMg@Hqo{~;9egVz_$>ujI!AEY#qAkPj2h^3c7 z;t_~^^p7YokOOias8=H#1^yR~L?E)0f!Ocn)=~c!;sA(7unGtT;PFS{6Ep6!83Hgd z>W^z<8Tv@cH1WzA#*<7EWg6d}O|^YwvbG-;^HE?zMZ0rH!mvmeBJzCzr*tsqbQO$X z_w0zm0(fRL<`F6_rAOp@T(aSH>-R&2wrF!0ch*kbuZ_2+-HUY7Ch40PccrBDIlbZi za6U&A-l)^%o=)b@Yqvse?4%#H(p4D7S59~Qnn$W@ZjtCa5voY>cfPRwTmhTx7O`4U zeQ8lVXAun&L#7=J;m2io19GavTXZ8yE*BL;JY67M5*>WPZ?S0L=MhB{Etp8X$5(|(Nfh(PRWPXOOE#BJE> zo4HTpKN&)KCB7%$NQ|9=ubh2Kw49IV=jS$AfnFTv zDPq<_Batq-4oi2LP3UYus;nuA5x%C>S!?;I6^a4| zNoIfXF@B$($elKJhYN4cX{~0?$x!t*%KT$96SeG#MC^hd$3iWr7;jpH+q%__p?@-6 z+iZBz0QcFzF<7tEL}GSei?6c2V^tzRyEV< z5*DDJMJ_u8u)!H^NQIBvg`c_OtNZANVatCiBZ5Rp5W6~i-~I8L-cLnNxZ@|cSy5p< z@yCwb^-s)TaE1}W5D&RaJmNUlQodsg6sZsQNf!UM3c5u>S2^a2TI~U!n!c*Dvc4|l zPP`o0xDA0yixrIWD{}9Y{BsEK-3In$3)t8~NU*wpQ}?|0rUxd@9O3%VDNVQrMdege0@M8^+Rbf>wFt8<+OLKjEnWXOG2-b+(6bOfd9svz4Ys`h zKZETs75UBe^|wV$!y#$1a>gV&?_p>a?7DvJMT;^-bu}}m*=R%+uU*)WddXISm`m;`YZ5ap6B5^-)himsSl?* zp$h%Qbb>BM-`=Op7^M?`8^vPOy+Zp_2K+dW1oYk7Wk^n(jsxQvFYKs$T+z6$KIs*w z7HbJ&S9YNnb=<35tELudP>(07(Y@g{RQLk_#n!(-PDnJS=+^a}xuUhIx#Hz{ivPWV zkbHe%F1s+j^?ZtXS?9omN_q;oluOe4GS@0uf)TS}L2j6HKY8(;?q+d2DlHwkafE?Z zuDebbsJfhR^aS}fu}`FbxG+}Ai0A-oHIL0u zp2qnz@x~WSrY(ha7_y9z4Z5vs(KY)ccnUct`8Jev3$Mk$1{AfIc97j&wlP;QMgxw;+=1n{K;yDo zDaI*tmFy-P;_()4k83LWl`$YHdp{=n?#{CI-Nv}TsI{mLDc5qUn9+h>xF`(Z2IZ@D zJFV6tIl~!eGf!1gFt~wosvZW63flJeW6$P+cg%NFrMhq0)SN+IV2c}Nj|k-0Yzc-N z;Q#W9_|qfW{-FE8L`dVnDI{-D!Gqf&AMq_16>i!d``7Ii%r<5Z8tHeR9VpkFAJ3QP zFF8V7eYDQD^%M|o{^4{bGic7MazvmJB{b-0N{bQ`QVL+#{z>+;ker=tN!9=J@0^Gc zbp7jnPZiLG|8HgoK{mqyW!Mwe-_rbBivW3E(?k+AY2;L6AOI+Ddq!Gkvi#w;PgdI> zx+`n}Wl!G&Sr+dQhGXaN-9?H%5Su;2MY+KnhtaCZVj=8c4EL)ayuw{_`NvRh)!3%- zmQF;b+B5xq&wyuN|6B!Y_h6SvS<)^^XP6YL&&gy!5w({B2hybDR{mj52`LQE z;5`bSib==T!PP>$b7EC@OYCN_*L{&ww3)Mh{l&a}M$G-6aE9<_%|#gWwgSyvx@S)% z(*?%#)EUfcKzQ67+6BzyL8nkDpE=FG)Ie@hq?ehCd4umV)yvw3_@}0daxDEjbgAAE z%r(@XY^w0&+DW8k%h10uzu~MaGfOuzg%al)YfYwF%kQpfNKG!flycyra`?`C{oS8b zzjpTK+i9$axyTeqUCX5?wD6i`6_41h9_6;6yaI<3Ep-I$K$w3dbv`OJe9dS+w&3pG zS#3Qbw!Y0nNGsOf(77$dd^buo_0k2_dz z8EHK5baXk-sRbtl?t<_W#JZ;c>Dw<1D^o`*+c;!Rn78c`%V(S!u6v1ihvSQ~Ysfe_mr#W)ZZbFc*A+qlp_jZ_H6p?<|UGFfYXg$WX1UNKIv=pOSQj3b7LKufOB;tBYt z?)4mOt}3wHwnmIvT2;KrrwI z8B92%#eZH)NUv{1&L~`bo<$CHeG3y%A8%mdnw$6qu>alVxVVGO02iBB-4tWdYsXio^N5h7 zz-B?*fkBpKQu1p(r) z`IhXDuKw@WobhamUWq{b#b@T`5=!e#touhRqXK!Its4Q-gny(o72+|C)eO?D6t+W^ z_^j<4yp{iY!5#P zi%8+1u8|}s+?H@trKRG#O;ml<>J`>PoH`h#*dNYN;E4Y>=CAHc;ktRj693V3-9U94 zStAV@om~QP<=Q7BejNjzs$+{^AQSv*{knUa5F;p=8QG~DGbCVef&rwxCzG%Z(Gz=J zmjEDyxDTeC#NTxWkcZ?NucZ}^mHl5JEdqM&@yo!0I{z#=M3g2^58RVy`-6t=HdoX4 z%Cto?&GPj~{&b%qYOFw~zNy&%+Q%XxuV0C-hKaq_YWJoCuHS7i&|FR(&GEKwP%Zr_GHI)e8Z_-d4<@k5#>Vz0fBnX zqtsemo@|@`2Ugbt%gpYOezXw$z@bp7@4=t!WCZ>Hu=bWgacxnzXdpPj-Q5Z9?lca; zoj`DR2=1;S5ZqmYySoH;cX!u@*W{e@-B8JTX{8R)hy+X0l@KGd}U?h;FWFB)iRn zq+VR26(BcdkcR@q)_bz_5CbOpFCh2NF_D`hu>Y%9Xwv?FVdh73YXBCB0EPaEP$Cpq zz&i>a?j6rl8{x;&SkTGO=HKe*8`1e{(|qi zCC$aE!wx5N#$>~TjH07(zZ1~ddzxP9L9^C8L5;QM64QDuFGFn0k!6e^f&e8|f_9{> z2CNj*?HEV@8ECdP0bxtm9z4Xwg3D)jT%iEU+cI}_6!Lw^L(?U@ClB=rJBzlFt{6bi zasDe6{y*=7${CQT~6IDdmU ze_f~A9ZMHwEWYgim!}~{fq?-#jAwViI$TG`EZ9>4}qkfbYc%Wl?Am z>5>Omw-0$7z#Hf*_Dlo<r|omF)FgSrG2I16c>e>Q^^a(l4h5!y zk4($1z8*>v!gA*e2c=OP;+=_lsnW^Q)tO}F9eJaF!`-NI&24Y`fzDXRN{1V$O#`yNlXx^^r68=e9xEJ3FdQndo)R{VbCOnCe{G8_>FS{_OtbneuJ zB_?DC;$nesYxnH~;9o?dKSB3F#AkNK_`Ddk0`HcG)+eP7Hj4&&z^BOVc4+?r_zWCi ziy-sHr`w1ar+;+1@27{H`@B6HK@0Bq-iBhh445~L0dU+U3?dbx51kQXy?jB|bO#W!}4pwUa2or$wr>;#1LP*Tgh8@plL4p`z4E#XMB8H!A z`cE^yQ(k@BoK(Cg` z5f-1%4Y>n&YHP~i3-kRIYuK~4!ze`O%BQ7`k&qm2I(RBUR_I9bPTn{* zkU~NxUYwx-;{>8OIq)gWPs}^mO#X()wKCN&Z8oF!WYV0R#EiP@dOM%;c|Ilz(`nV$ zby2d|K%!rqDar^hy-Lh^Qzs8=x44|lSVO|^&v(4CM-%X3o*0HDAO-nmx zK-Z-C(SpP1Ki^@hfdzn1>Ayay<8<$vsh!Z#(c0et@0odcc#I9mi%l-K`jpiK)&CpD zK#U0?K!DqQ1wX|mSM}~cNA%Bs5l2IS)RZC*Qg1{C!-L2~?&;(ZCyeabkH$X1#Kqdd zwe-KHS1K2SvP5}RbK)dk03)KJ)-+;>I7r4qOKj#iu`i1*-&T6mX4LD1#VB_0=wJcX zC~yGCV?0Hcd2GEo*vV-0fP!;n3gW1x36(|g)gs~%t6-1L;Av7ij6O1KS@QaX?H02? z-}7j9jOoFO10nMKKY-CxSD{*)%@aCVF9&*kTxtUekn@m`I$z^DdMs$DPc#CtMazA~ zroNED+Jvj)r2FPYB?`)Mg2xEHa>-}#aT|XfPhp4Gf0#CZDcJ5v;Ls5IqImHy%hC%P zIs~vM{9yAhjtW|aP(0A`Zv|)I{im>J;s<37Z2SXJx&Lwwy;ND4v3X|mZx7r$zdR`3 z8T&U*jQ4^%%tzNQ@ZUGW0s5JKlU|B{SJwMIK_hxzs5|)wkrt1TKrjC-a;al`mpayr zaDX`=0t*-vaaX3O9>1E~WKJs`c4cCJZM!hwu zYkkqht;mmO;uk0!0LD5snCuM?HLT|O1RZ8tKUuvrET6PEHSoIebJF>;& z#wcltx&K}@YWau$x7tzwH1)8L<*B%n``76+)-+TX5>v>(Xx|kKc~U>FuCJ@MTT&;k zby~V3A;}>jZT~Hm(%uuWeXxza{}M1JzsC1~Z*$P}^}mZ|`p0{~cUyK0FmQE0p#AMd>IVB|vVH!04ED)T`z1Fm2PT-{zK^jg% zqTP3e!X2RAX2m65?ocO7YyrCH#G@h={_QQten-)RE4Swo2?tdY~Wu$YyFHAh1w~v5IGc)4%@EHX{thS@#)4b_pNOqEdL$$A`XSF&KF9`Iz%MO z(D+4`Nr^J$67V?#Eum{4wj;ra4a?A|k3~^zcni03tC0@g{kqL#U^-ewbN$!fxNxVe z%Plifb=PdgEs@j{K895`*kXxMy>>2CM;W04V!vY@WZftJBDx7mVe!8ipqLB5aFS#- zK&f3&4Z_7ji_-c^@t~9H;E70a435v#HU?StShVY_#Vy6Hmdwy6RPnoFJ_gML5;_oq$&mSUC z%zD!p6R&aYWGjSY9%AfFFpQbfPeJT==INqDSx6XHE@jgNbDfXXCFG#|CM38Lk8V6D zzB;GsK$(3xNcW}A=)Nl}N*o~IR3X5dphEOX0-Mj~OGI8@s;=2<2)PRXE>?+<^skvD z6c3^&aVUx=&stB>0h3kDn0iqW#A`JhS=Omv;VshV?U9zlG`#WZDY%TH`UkJGc0@;| zT0iL}r~D$DxB;#{AC`UVz3R}}+V4(5Df6e6j@wx6qk0j{t&h_nWC9k@)k_8z1R{_9 z|1Zg700cS8&9^WBh&;||%|L;WNUtP3`e6$4)vNW@gNn?W2Puo@YH4az@UW&nO_jE> z0wnG3Y8g@Q#A~IF(g(~%&+Uhq#2I1tcC@*ArpWEn&1XTk;Iu^A^WSN5zw|)&S&H5^ z`>p1?)qv^Pb|dj?*I3u zDI6xpP@HKnx-xWBTB&<&3}6Q=`hy@=+T{Fxel_8vv(6=Fw_+_)6@Gev@3SUcpcjgDJkR24D_FWqG0SEA2D$2=**~COf8x=i&AqX| zm3#qcgl}xYYHSp*!ST4(M7=)Uw_kgh*`9J7F0-{l0wPyZABU(U^yvsC8MWSOK5|6K z8i{lrv3vs-5D>Im@wmIrWjpyvg^jw`qKzuJ(mBN3`=W>+bLP5LC?$c>1XL%_J=7$} zKg@_FJBXd-w>l$Gr!$h~qy!Tz%uo7Nkh$dZ8%6N<;>+)_s$NE=T zBN?pwo|{;?kks4gKn0+N;g$CE5 z3Ko(3anz%28-rlI%Fyv&#k;~#!Ew}>ZyH5u<(@=D3DK_wjmMVL)toR+9-oECGc=Ls zco)1_xzh@*4IwYbq(37G`oYZ@YeVdmEWvjlPt6t`HCUk%wdoYC4kWe_OYxQG;vH1h z4o2^G5aN9((flsq0CAy`{%9b48{+>G-`+-v6{G5>BL%e$!CYx6bh7UFr>KY9)U*ET29<7yD8rbFV<0ON za%iRN$3*mT+tt!!eD048K>^|8L=kjF7wd;UD`|u;ZV7HLlOsJsd}DH~UBqn%92r3i z&Eyxk2}snwbbXsn!`r0H%w9AE{me`DXrf7fV#l(QPNt?+88M*3#!sC(DM$+K$N#W( z)xWn?rr-T!o0N%geoev*NF%ys82xjMGNWL$`I38%OFDN57MT<6s{lX+Z9a-2AqURC zS&G!VVxF8}L2)(yEGc;9O38>8I7YbX75UP_y9$jx7Jt?S@)0_-)oPaH61}&s6?4w$ z2(HwgI$xh^i*3#81Y9ZQ0b#X&?`=Rl6Q7}cf@+qLhIJRDymUreKj4yM;;K{pq=BFd;kF()6n-cbS3L#`1_^k!0ILk4tSST_mP+Tnrt!jwwt7UUSu$lsn0r#V{yYE!)ng7 z6x~mCAz(tgO8hNyX!sV{f3{6fACGp+No z$Hbj1)u`gs!_Tex=;GJYKwfrjo0F1Ki5R{Scc?jzIfHME zqlxl0t{zw)rH>m($Eo-h&D0J#QdG6pKRSBD4y&O*^bdFx3`~ZMWuEtJlHK`UfrQk) z{UzVx7U!dF*{vj>jXrbHn_wlm6z*m{SfngI8=sfW=7|l}6!6vy`ZBcmSi#M4W(AsP zU`0GXFyKBDUZU)QD6ht`GEYoA2J^IrbAKf#bnKNfnnfQK;`C&H<58ym4Z z_C5{gh(y0;%C!uf_c(#G{ip4(L)ZV_+;2b+9tQ{5Mu`zyepfkxr1+28YgH5LGC4dYwE~cuZB)jBXmz>CO(}Z;0&6j z+{$~b*xX4)5{2{jxpKiJm$PVRt9+@kGV#M>6h)jGcs@n{0 zYSvZ%l~m#GPkPsS$85p(;F`M^m42QDiPKBm9HBsnvN_Zz7T37j>JD|-RKV6qtCj)a9_5GAcW;R8VUq+&A|+7f6LBb zZ*IpQU?U~y>D6gCv(J`B;vJyE)%m zt@H@sZY{sh4O*%XLqj0{V*OK$c9mr%q+qoCU_L7Bn<0*}@&+NU@EQ;5=pNq3-~1)R zza-TAvN+l!Usz_!U`XN*Q}$zqKyyy>cJP$XP~Ooo;^cQ$RyKFY>tA^dyPqQX)VEN{ z90CxC3i}_3`hO^HD-aPvP5(skH@1y#)X7)IN#&&|d0L`WOI2HU=(`& z`m{$ntfg}D{7rJC0F=~_h9NtFL*&)!sMzQQmDPMWKBKH{+Z&$%;VBW#7~GYo6_O*C zIT)Qz#xxdZgJcxq!{90kn!Yb3KS)=^PzA$;Gqwp9ZW07770%KTH0nCJ?mCZ;)Sn8` z0$8Z$o_95d937S>jGS`~s>Fv*&=(Y$z=lFs@<)`II0<}Q@Zwkn(caa}_mXgK37l%g zso2sZ?R#`XQY%6Be7h88#(T~qHyE?jv+Fjg(RQUZ2POVV12*rpn~yLY;=!DOt!aC zW0R>rLdxv*h{Bq+$8L#?TdGks#gZPxUMQENdefI4E7ni!l-tb&7Yt*5acyD^oalR!0f&#$QVF>?01lph>;Vl<0*Hx79>Lkn8hp2J5SL91uZyDCy1%Hzm(R zfGs|XYq)r!ROg+@e#L6@{gxLgh0q)+1tKqIbJTwx4p!$OQ_>KiyNlle`QbzlAjl(| zzt#{2pL#*3*>Lia5jLI-&{)O;xXHf}Q7u{2ck=AObCiETsH1Q&AvcfN7J|pUU+{$P z3OY*m|EEVuoYP@$Lwmi=PmcwNE$LH< z81mHXw@_yKyLe#g_>kgD5`xED9n{AvbB-;wE6dDof0?m>iwaefus5s!A)g zVKqkK??;OlpOChv*O%>TfqM8?R91isLF|UL^N#+FD&U3&9NOT-!3&OuImNcd{hFgj`F*+))nV=?aldV z9|ry|2NnJ;Rz(EUNLWMl35TH&?EPNxmAP#={8HZ5L1FmZp0+ zS*{1u9cHv$EV7^#HZ>+o(5GUg?wx~93>pGwulL;%kS1N8ZX?Av6XRGa1u21PU=`fI z22$mdt^s(!r*(yzA2Li5KBr1LthbnHYx>zjmopeZ3a=qrHENd2&06^94VjjuF0SuOY@8?Q@LS z+l3Ur0NBqb7r?elLPZ!2#>>`=F5;}~8WDk?1{{EtMM^B5oLtX@39QXmEIH^X?~h1{ zIGuYDBE;e!4G|m*n?HzyTnVnGAEmLdy-W{w@o0|anVUhaBjb|oq#!kNdnE7ECxD3W zR0szKI4bq|kK23~r&g<3W09+Rn|~PNo9qXHW9$P~Y^nSllvvMdq?7c8nU!+uX#@X9 zxEd=JgVX0?1pDn_6d-& z-BNi-(?P%h&zn8jvFqZgTp@Ja}@1tKBVt;MJh z&{XTRtcE|U#rv{dF%a^O`>PwWiQKpCcGo4PeN0>N+fAJPyX`bDKY35pqxu9WTv{Z< zBArLt%U;^}M4m}F>&D7D>yOHD5&3kM-j-etb9H<)3ViQ{1%SK0>~Lx*S@lm;5fn9j zcb)+^Rhjn~r0y30T~7;8a&%`JoBkIse1%bOJ+tRxB?iS-m-*`_ zn#~OBJ*$3T%kV>O!+%}?=Mp^pZytr0E_}Q{cI%PnJq+KLkx38iPfWnf@h2Y8DgR}) zWr=CtlMnQs!SsKyQV>hT`_2+YJF9%2B#I^?jiXNKdn5%`dEJxpJy_vGf+n!UzAvGb z=s>GTD}fxeq2k?KW@fKyQd(5p55_IO<8FAsuxN%+bu^f8_J0*`IsVlEk1p0-mi?V;MT=c|H}c3VZQFOH9+8i~=cN8?w-o zbe$39`L4*)A}i;gE@7RQmN7Jy=^ij*Ww8g&9g4iKUk_kST&AV`rxh?J(o z(Tk=-BsmRxY~Bve&mE0fheRK3#+RKHX_jHq^|v&MXGQ(R@ags~(&K0~iJ{J=e%zf2 zc#Nx@ANJ^z)bS6+;9h+0$AHij#z1O)ye>7A+4KJ~ed8~Yo+7U*5lfu~aO3;=in)jV zV>;&=ADckFu7jZ0*9P*Y(L!oUP^z{8+BYze1 zm+)Ueb`_I%1qZ(c1XM*^lW!mjw1k@Ly^)5`n!xsdl%jnnN#2!+*Srg1Ksg$$i}~TJ z!h!T~qsy1}wW~B6PX6lEw&P4>GJk)g=H6wao{o}|SaY~N!X4`cjQ1_qIzAWQ)*0S@ zwPQ8%CI->KG*$kOjtKbF_!c#BUki{qR>D!2`bwwuTLtq|cSxbX(69436*jE;v7c_e zm!BA3NilE>J8?#D2{{s&5vzUKpoj(1_#lE$4g|y&HI%>nXHfQpeGj3$`uLjL#&u@b z#kTC<`k1V|sP0JJy?JGFF$PO#~rvfBl8-_;6R+@SSR*Dst*)aUi!A-sSeF`QHCwOl28C$3yP-<(Aoh+(vKuJGn&UzX66mv;9h?*3ETQ{N{G#&h$j66aC5O zQ;b`c3M~n+3P{Cn_IOTV{}YoT<~jgHuxvwH325&WZi1wY9fthohJ3H@)$)MesJ3-M zFg2vhdCDE^IpWT?x-881)5D`TN6tWW+HO3BXuE5P;_9QHA3?+Rq{ZpSU$$*?F(cGU z;%wEi-#sOCZus$FcMJiK*tTVF&@VHYog|eLV)wEEA(CjPTe4i4ka@{W!>|hj0EpDD zsKF&K-N>I7sY_`?`u5SJ+O8%>lx=}~PG7ss=F7S8|)uS-;uovM@6vR-&t znT~=-eu68f2%D?dpoEVXp|y3?c6nm7QB#JsT}{OlAjm0x|HmrKQint-K7snABsWqa zpq*cY;R-A3GbaxXol3(XoH(uWW;$M#l^@(o5&WF!Kpah5r3B-hc@W{xSXK+k)Cr~4be4!B)o!i#HVPX7Y`A&Dt#-uI)`hX zmE3Qj-N#eZxg-w%!-n|}#e-u7>KVIa&Bso(E)(9g`pEaU28b`9NF2-aLh91INAD1aav>K>{n?GO>12lmIs#;sSD+xX#OFt ziUM@PjOIhAwMc39Na79s;Z&wKaK1E}I@!C|<0S5Lc)l%uao;Ll7jWm*!bOr`R%FdJ z0|g`|cOy`A+^O10`G-7xhlCE_`?U_)_3%Ic7cm>(1&L>($nGDwZ{6(9aW(6?#H+=B z$k)fxgnStgXrkG2t7Mk=<2{g42GK zlJ!kU;lWdc0J4A)={v>+!!O{v0wDs+(F}K+?3~wwvuE|Kz-{9J_(2{?>d^Lu@fRcv zp!w;ANQ!5%5ktJj9Lbowqx{;?M4=Z?`opq4y?lPHxbIuTMw5SSu07nPPjLNRxLxWZT_>r;vFb-hTYcW_za! zQr*MYnStyKc3I`ZtTfZqjZy+7~?uvfuMh<%z9^}6y>eK(4td{bx(OhXpdr4C+xdPcC1L5V<4 zqbfVd{UZYy73mVGsgT0rIkK4KJy=dpiD-UOWm^Sj1f}p0WK7DS26C^C!?$#?*Zdpk z%1zgcnO0kn|(4#3D7}e4vN0r|Fx69y@Q+8QPlVU$v->MK|9$z98AIF zwnUbzP;Vu);F$;9QXSZ(8P6a;#r+t;*wa)zh1b>@dJVDm*r9zHG3E+B{__sq6WaLA zh1CRj>VEYpDkD4QvI9D!tHxOnPEW64{>`BG`QlsGUFH@lJkb7ShUv>-_T#knb5(;O zJe1SEyC6e1r-jVrPdbL4M@rk)hY0vWy>k)m+uRgZli!QM`J7`x%S*ylZ2|eCsm$XJ zB$35aD!ZB8&Ra}~v+EI@Y$9;TXEC|!X0OccR}>cW#yx93|tCwPu(algST?Wj5irvK@w%C#!K4={WD6_i3`EOEXjH7qP+c{6(Z@kR~^?|JgSC&LQs*g=f z1=9rQQ^{)B(6_6E?+iqFhJ>pg`(P7&O37+?=GaUEs zt5M+^56ph%;lh5%<#M!7wubX)iNMnURVy}Sdi?r@k)eW-2>8sTk6vN3L@UM^jnAhy z08+@szOvhPg~w&RYs0}yB zjnfF0xslJO;$`(m2mF{am#9}<(zr6zPeDQn?VzV6y+G{-+k*3$8C$J`6g$G@82%~O zF%PvbK#&hpaj@f)lLXaXsFj!>fgPwP0VSjl{)-=^nZgZt{N@y@Ra>c0@u;|nv^5Ng zIQp<|8%`-^frp?3eh2(}Hw(SVFU+5lJWiR12BA;0;PEB2qb6NEDY_osMT>Rj{g%VN zJ3gEANC>xUnylm9TjacW545dACeLgV}`#L+L7h=f*Ap$(| z_i`gujvZehD}W7D_RDQ__FtiH?Y|+Pzg{5=e6*g9T!|i&;CmJI(f_PcZ)HpZa>~AZ zIQWQfQ`-5XtGAruexn`1_!lRYoXs0+Gdp(9Rfh-ggWlppzz5hrM3W+AZZ9M&VyA-* z9s%dwPo?W0_4C%*pHKIU?r@vh{`_q=BBko*_zMV|v7i?(Nk~7gheMwi49GdYj{Dxljn1hn=|lVq zg2>C|mdEYp`v|N(n9fIf!y4@D<4TP_^@LCcf)BW~8^34&lo%6T4o(hMhV07rBC&AK4> zL}?;LDoPJ#j?)$QLM{@hXCa%4*EM7{M6NC)X{75TC#hOP3;A z`rcJ*wu+`TdthXR@Zl4;F@K;wzgo6uM^5vaYMo@u5crqc-bT+HRJ(~CkTt`pgWE8F zDD8q6S}fn4ms?iTh^tS6-_$7kV8oD|s9*{WZ-wlzki(=}o6b&GI~AeOJnXrb%7Vui z4E?ol;H*D8WiexWS!oZs6^f)@5=_wrqz>Oq&$Tq*~;P!#VJ9t zPkSdg5l8h(<;zyFJl?Gj9=J8~?PN1K$z-rJDKNkHDnmzm*k59fcQmgYCV`eiT@(Z88&t+P6)*cJS?e*r%Gm%{T2qW+w!C8q(L)W za9B`jMn8Uy2v|=pom=uk50T_#wn8v2e#_2qJr_S>=@GP@J~aFI?Q4L)w~;MT<4%Ra`SUQx13&kKe$)@PDvp zChLS@0+0Lv`dF!hsr+zQk*-CE{og}7OAxy_Ro-=*sKE{aD+B~5Upk;f@$F#Psp3|t zC&RIo9jV^A(`}8?JNmAHMyC<3s2v~CkgH$hI=xCP0XR`qos55KSW-Fz5LbIRu{jvq zE2_{2)hnooEc4b1ogzJ7x^ys0ytS$Ck&v|0spfl!-drHrS*Qkd2a#0@ld68&%5$UB z!{O^QLvPvqV3@;Ge7~ztnmZ0SvZUQhON(-O^FUoB2l?`nkxOgE5}r>gv~Ud`2uWjO zqkOe%Z$*nk6YF*(PW9in>6yt3DRxE(zfTu7Lyuad6qihvnW(YJ8e=aZpdKtdYX_SS*FW=){rsf0I zRt5w$eRsCe4oc77uF0oNE5yV0d<^p7;Uy*Aut%NIPnH6a-aHpb;$sCxzKa{JJdg~# zCbNUb3lfHfbv88V3q>1EUv6$bj5-pBvvD!~jiom&hq81=v_)u}_a6A(jQGVU~q0&(M!M6RD8 z48l6QKC6B=4cGLJ1TUIPB*3HBm!SKmE>U8w5u>f-bi4>mTtOg%{xTa z!Z{VQ3^4trWD1Mp3TG;2USH#*#g}Cw)9wP5syhcqMuzlBOz^H+9z#78CG)_aUmIb$ zGl?3)e!fib@41r;;AF$8ww?SaUG;)l!3CQV`KCH1stu<8k?dKlj4M9A|!aRjgN}30hC}9(5v;MvTjt6V0>YJJLteVRf(TAe-e=+C3n{IT+YL6iQ9{%y zu@X|o)F8{gmP%^FKpr)$(S2NJ0b!A%*`1KbPdDAAg@`#ZcO5(a6hJ1y+oIj%qShri zFmem~mI};G-~d_WBITdtv}XkVA0#%jKR;R2xLXB^^X&k?6GpHnt#^GCX{F-lHIX!F zdOe{t?&tf#L{kXiwN*AgwyAl)A(&u%(p{~52y?tqyLQl|Co*0~uEO%Hs;tMaJ?tu@ z3ygIO)jQL5K9cF6+Ey&s=qk|F3!@jeMPP53Q(QL;dP^(_Fg=5OLuz!htNfSwl z3F#9PZ6~SF)p2ywh))2bQ=?+>gSF(kt@nMr0@&>X`H|(MJRV=wBuOd$lIy~t6*rJcA-?DX36d#qGHe8;5SVRmg^@bRA*)lllcd?mm+PjD^)?Z5}y_`<%*e-Ja}Z5{_yJMR^#r3lx%#XUQAH1 z!295h&IW6DGC2%TrfzSOY& zd@F|8qRdo~AQxc~AMbIKSxkW3(cA%Y>w2!kH!}YjB$g~g*9Re}ar_^-Wg|HX9#eCf|3scQozHW()ZRFCKSK(~sV87=0 zwXLlSEs*N6<8x>XKTA~O2tjj?^ESo9a*iq^G3#iWEgLp#4dcL!_11*=dQT`LQPK4- zrm5`SY}!Yepq9O0NPz38%9Ri{n1nBi0R-qH%MTP|i~W41SKAS6nOseVP*SXV1qzKJ zSwUJq`BNCMM?ExbWx%om*&5lO176;Un*@hm zsyvI8H@yu@j>6-alnf>LrjHmvO&fq<&jGJ!j}bPC)kk)JxsBA!HjMf67~%C^F3~&} z3Lm;?e4fa3bIqNfln2v*UOKAw-bY7_Mn;u1gXBRNpKfH_qwTw01x(_{s}pv? zb-Lk^!)N6-Ew0u)W6+rE7+Hb**g4Xa?K}GnE58oeUp4Wd%-W>GRTuC2^w+ii# zMpzIcXu>BXwH^eqT|%vRSs&!7gkbfTeLiYLV|-Lq$-c|%R;UJeLw)yLu0!P2!_+Or znyH{}SJoH2lFTZ^njK6IzoHw)=N^qIJX5i%P`31Ons_U^R?!?`bQ6S3);|)V_u=FB zVb19#GDm2Z^%35_dH^R}EF=D80UL14=CcLa`dv@oDi%v;(G{f;13WKO1rAFaXYIXwFY8dLlWw^E=Z`t7b(>e>M-lPsFDj)?plaoKzzLIwn2{FEnP z@}u*m68jk8*g)T~+QGyr*1<$J0xT38b001$aZ@)v?z((n#g`r3|}f) z?7m-^>p){c0E-Nh+dU*jCwj(Xn5*yViLso_7+fi4>sAWw>D`?}g4O1W`Z1Tg7W0GH z=%yc>k27uVS0li)HojM~WWkI+Fvz{?@?hHcUk(vy{ao}|l{ZVSPg0B^E95Jn6qd`hO&WA5yWH0mpVDeT)HQ<36=vfG>$UFx>)g57|FDPb(~MNh9Y zvhS2ewYS2@*E4InT-r-;x`xyu$qXk|p;>>pOMUob#Fv6xC-O1Ex;ZGnUPbH;E4Q(Y zd_R{KnI?eM^x)x$^~uO15Jf9oMN!Y0uA zy}tb{NSyp3T#?YgLRx7N&f&nLR27Dlu+R*^#$m7dqF#gl%06I;9u82x@gnvw`}xW& zg(AXJKCd35KA|CQI@<_X|0+HpZ8zQ#NL)0)XqK^*<#3Lw1y2*MS$LN*-?AwcfCd^j z%Jgj|3k)XV)tboR^#uz(VQ_%I2&z{YqGzB58N(1xr>tZ41R4#U*x9DQ-cD@(D=V|P zon*MDxieV#AuME5OnE>;!!d?1gN#=4fsptp+<1goQu?T0ff_ZTQVXi z7}Y!N^UVlUU_+1^`UIg8I_&8o*phcyylxN@P0oqHlk|tBZBCgVN-d6`Sp)@(*{?)J zjvU0&hWbaEkwd+?#oo@?KlvyEA(g7%L|Ypyi5M0Ti$1`5HQz-_bUPkx;ug{LwWo7G zjHpANIS-l4ONR8^s-Oe?zx<8|E)Hff^O9m9`!y9aJn>52W!B1)h+Zt!ave4x5Oh{P z@}CR}ns>?L&rIA-a`7nuhx{ouA7(>H(Ovi(2o^Jw%m|B#UP)pMrfktx>>Fmb@FcR# zL9PJgOF}i5d{?r`_(6%s6mJ-!X|iSgLs`K&hy=8Gwp3eEuL@i*b?bclHKC^3jrwO} zpUWNPzYAMGN_n=#SV2yJxhNswJ^R>rYHYP=B%+FTJXIqP$}VIM>7S8Hb;7H zVb`1E2ec8XWMl_C^pZ#kyTvvN1Bq)&e$V~*1;Aq9hK=oJX0YY$hD?%3Vh}(z*F`sW z;H1Q&c|mVod~{4gS*Swu59Mu%;gU_~MLXr!ZLX9GtsRy}pVAoFSpOW%1+w2o!<)~J zQQf07UjE*sfknzPw~j^=j$v`(PQIF`j{imU zaRbT`u!(&;=UH1Ck~=+MH2})+l_M9m8D#g-+j^#Hg{mFD!}UWNo83=*aN^){wP1}A zlUZDn1?4qfQ2bsg$9HND2W?VmNd}?U2agL$0n?`1TkC{gpSq@qzTa_Q@6`R?tNAdb zZW;|VdCzbDra69iAPNt_8BW|_`roq@9MGW4t8CaL4*E7U;z?Mu>+iqtGT2nO4P|h` zDAp8vQJq*8x?)hCluHz~gFWX(LGBU*#vds0*8&xH^OaFQOMyeQ1;3b+NYKJvg2B2X zN?}Bzl~ezA`zrbETXIkvnrZHA!$_R%jhyl7zERg0dC)~UzeG-$J{29p(dhtkP;6*m z&WUH%Vy&&&h+Aiu{xol#bmyNO3>Z?xTv{J)Oum+3EqwM5UC(ziIkc8l-lh+FqJ|Nb z^pQMG;HRN4#rewZdU0gf&V@yayt74$#(0P1Eh&$`Xys#SP?YbOX@JN!LxHKJ*d?zwtQTifEpRu(2jcU6SGwNk|VdU+XhPs6Rhl{=QwwF-4;d!bByTgjSMw<3N_k$TkqQ(`C~2YYk(WN^WJmNG zlCf85puqO*DJBraB(?cq-F7UN5lH4FlgqI=GE)WRw9+%fLCfic`l8VcPFAYJ!PE#& zXu)RTjkuix-MHmjh(-H9CPf>v!$t|P^EoJBqnHC)S^Wbg=1Q?hzY~i4k(5*?mbk`; z1_fGiJH_9BH)@^Ka5CW}U5ZE{NB$A0%nC=#M6IIQi2k_=+EG9&<}CoKR@I zkrONB9|7pB)Z2wklE3ehda>*Ua^^L+88`!Bm#N{^?jD9NO_g+^Ky(sZl4(sU=OjkN2T zQ&#SEf9DgD$#$@XgN^#LFKI4fSjBKd;4m!u99-c#D%(axFI0j>T4DDQ#a&jZHERD#N}blUE;=Nu22LdDo`KYm1o*@Ra{(9Iu7rZI@c=jW2{#-|OSR z@&CiuJ4V;peE-9Z+N81V6RWXpJB`!Wb{aRfZQHgQ+iYx`O`encZtw4J{a-vU&N^$& zx~^+x_UzfS@!5O2umNZ4Vu%ioh%wZBb~Y2EHQQDRHX(>6)9;pGKK2B$Y|^Ma+oj50 zwh6-pI417IzKxwQz&r^N#0j-8g&jbBX?v>cz2|8={AVyVxn_ z$=R|_hzv@^;2}M^q=q580kz8+i_{$47H6h#U=xkfBbhm`MsX&E0xv24r>*Il-dRCV zkl578T&!dlNUu{TKr9hO)uPNQ=5HPv&vdro`uVGGTaV+&6JOs@BNq>8syU^tR(O=Ca?Y(u z8X_>jMt8Q0lgkh!CVqxWlMNI*uSh@Jo^^9Q_9Ftk>9z}YXfgx&zLym+tM`KNkTnH$X-vmH8%pNNTm(n~;%6AFolWAMHb`*QgNxg6Cfau`iD z(ienNJy?V+oD&5rfI=ig7%NM|Xx2oc`DubMtWW+bpkOsJ_8XxCEfk+n9l_P(AQOk9 zS)kec;v^~k%u41||K)7p9O&}wx=3NjZtE?raZ=w$lkMlh>BJ~YM3+HF?n2mgW}?mY z21P(JB;i^0=eAKu0YiY9Ymg9)E5PjVNJI)#pOO@0=39Ig{S!gyZMq5BLVJxhyTU+u z4LiOpXzE-^SHYHK2s!h#v;oP=Dg`-bIDGOCVYMjhhVT(E9i zcU{yuy#b90)5sK*W1k6Wt3D^gJj4CQHk>W_#sI}`;roLP=@tMbFKY?)%lfyyMTGbO zK2J(fh^fSNG$t=3yJLtl;4Lq&m7Je4&W}C9H#B3X7i8{}eFlJD=#Vz-fI)zPJO<@( zk3rZfr}J1+oQa7a33iE6od{VOkFGITd# zkRcq4?Atfdt#V;@z{ixrkUhTxBko`;J1+L@-Q&}r92CQ8KiFdz=4N_Ss2@F|b0SH2 z^v&tYdwn6m#PbS5+eg_Erdov@iW(#Uxn@YC5LmwKQ>ksVxDfie6cQ?Xs={q~m59w_ z#dV%2rV{60?{(CPTQ-XMkC!a0D>&C09+*bB#~8(SSbc>&??#f4-NxrLC*;;CpA2v|)m?+;cw6cv>5sa1NbPtm^0!A;f*5MQEZbi;#ZbZ$#YWZ>xiK{l# zPGskodk`QM5i`Mu)MHz5EjZ+pcyE)I&OES4AI3^Y9e z+2I!@{giy40YC>i62Ylzv~OF@vBd1xCD*qPUb8AH*}b`)-J^vq-w_Dpcjmn*mBd~l zvG=S4KO?c_FM`W>N*FAKNE|7);62{^i#_B=tu2CcDd3VEE_{Mc0~j)c{H+InF=iabcSGn|8P=C6Nc0Q=+e&mm}Z>#)pQ|=4HiRgk9`2z)w?a z!aXQ1%%cRhSV>5kC{NC@9c(UD+?Ou(4Zx?KmXZZoQ-)}yO#F)S*l?r}Z-H1tFanI2 zK;8emNtKRVFrXh|4daV_*JWweT<~0L3?h0oUL~y*xg!0TRkUDWth%I`0QY4D(pCdl z)nb*!qNqssgPApgYlgi)8U>p%oJ6U1{|&?j~a`C0tk&b z5Tiqq0>L|tb`g}J;od~1y1lSxf4rG_Js^2W-~_*H!`)(~l3W%?tmgG?4{2ejnzUt) zQye}r@S4?`!vq5AFH_(XmR%+YqZ36qjU&cQEv`5!B3GDfY{W2**J_+bnZL4T#B}y! zaM>?Ev4Ibm3q6!nY7swh2kKI)v4#uc%OKWIZy#@*Bh&92l(=OT1UjA!iRz3PgTz#h zVLXrgjSGGOWrYSoBUqybEW!OJaixHPLEvVutJ9^hp4JfgRxF4ZVPsK{q9OZf212ZW z95UXJ{5e~F>6;R^!b4(Lg~@_UI~8$tH#Sbs!9%Kxks_Pb zg%%R^mn1QamY27BJ!2^Ta-bKiYwy}AIoTbjUgAUR<_41 ze5-*DR2l0bgwCE3dp4+(S%=LmGw;Glv>&d6ibFpc5MCKUp;4bUwJ)@frthVU2_~_X z4MW-#ZIt{yPJEdvb%S}VkXCUol&;4Un+9?d`{FQiR2ZB(bI)(G<$mJ@EOGc+$NVyA zoE>;&z5sF$u{C?NV(9fB*CtvKkeVdL0w=MgxYTYrnJtJd;X=p9Fu=c+G>xR$ zMtRHcbasjp@IhCjhW*|Jf~fw0F(>azhzIV49@^XjF+zKkj9}>>o%V!5O(@;z=hA z%9eKEcx$Y#7H%1Z#sYNCNY5DN%__(v$?@TF9cMeP*Ph&*6M?#`&>%50s>0uU=_GWh zX}g(c$SpHISho6OTwCZ^+bKuSTdK{cs0@*FwnI-i;OkmT-0M?tOmRv$5Eb6deYvNYBfrXVmhit*1tOy*sZn#;d6Uc%yMIJX|mExEB)S!xiY! zny5{&RMNgl7LZ7VUVM*$aK70erx(1+uLrN85h&4v6re~JzJ+%#Zu2-o5YN%^$84@P z_$3^oggHN&7Amf`AGX}Tb6rU)Jez%;o04jKi8x##_7nB>4IE*44K1;r*OXGI&8(jeWu1k{38l^-okfT967b^fvR$`>{>)_=QKtL#glvh%5O32 z{f9QJQ>>oWLo{x$UkY$qS4+rV#iH1;`=hmu9U48Lfc13FyR?5pOYg21E0}jdjp4bP z^0&>z320^v1r}&7zr?a0(hx@-u~{Req+YkcKGrdW(gb4Pn~=>*?1>y3;$SU+;frb3 zs?VJ0056h=J>5(TKQ?S)kQZ0b0G^0F445k-e+|=`A=ANF1>lb$jH+3Y%p(^smWa!{ z#xDFeH=+^$xxKJ6R9Bq7X=2AN?uBZ=uKId`uBdM=e32|$**EC3A+dd&3{2}hb5`8k zWu(uiB`zjv6nxb_1xF^plvCAEuNbH2|@8R`^;U-D9d-f{S5Cs}pO zBlY1#jUIs+k5K^`Q4#QJ)}@v<+AvQpVK)0SQ61+V3s-))yVp*wqEogHU~krK^NoI- z(dy9TSIM`iXiJ0?mXWk1sE>LvK;HQB1 zAgoKKEw|vW>koS1XE30RLf$tjAVy)RpafauG-&oQ+@h0Jkx$Ju5Ez)~q`{;o%tz=1 zB)H^)nzBfBE(oYYXt4DDfh{72W;QXUrUIwXEnz$zB<8leKedBaI)@00@bx4AnUoFnU8= zBsN~2Mx=t%o9feU(A-~9Kq8Qi0vF4fhY9(}Eat`gLMaS>gm65+=t3$cRhLWz+6?_7 zW#J%2hF{bbm7&K+qKbQAA4)ED-i6Si$sH1p`((O(tvxDO{gW`15B6WP(Tiht;1! z^Uu>|u2;<)>50*NssI@52tqv1g*ZY9iXYK&7Kg0UbY8A_i38DkMB2$d!c*hDRA_wFp zu^Y;@`a^`&anK2F1{K!#w|M_SIIkXlZ=q-fmZE3{11qAta;~lM7WHOE4OTmk+)VsC zedSJitH2jbwn6s*6cBM1t?#txm0VB0Y09D;c-uS~kr!6{^Bl~Eyd>9A7TbxuB4n zy&domO76Yo8dyABjE4z$1$F*RvEDLU7)J&R|}lMi!4EnUa*8p5T)`@e{zl{e8c_mEr~}kj3xqc z#0HGp;)topLTM9twu98%5UML)JV?0a5N7GZrhHl9he|vVPDFW;R==MFfjKX&7i2=o(FVp*?WteLjEs)C_);#wWm z9m?rU*b!Ud_*w4Neslp)IsCYNvgVQ8YXd|fSomMHK{65vEF5|S+CG7&ySP-q=+5W} zss@FxS}5@vv^^`0zHD3M(ydwkz8?OnCg#DR)|wv9PF9$Mn_i_Vyj9x+%8@*Ix0Um& z;_l=%Ad{(x)yg*$7y|Q+?V+XrcS#<=K^q0U{iaYz$P~k46kT0#GjQ3)sHi{2+L(rr zYUVU9+TPjlgxmqEj*cg0wfzW`sgDpMvILexmwP6nHm) zE?mLx_znGY2f4uE^n_g> zBgFKnqs8w#7tF5ukj_!Wyul5fpNR}&5&$u(gH*E)Z=vW|9;W)LPu|^?0DfO)rsYW# zQluj33{ScLV1N>1h9C#N&{o1D6?IO@6tBQJ*fEqqzmjx)Aiv`Wp)9O$;&@Ef7}0pN zdFdf7-KoejDB_xL9}!A@K~j$M>bACYaEy8jE9|*39<$_{pWgTumHycBe2wW~pNNj2 zZ8D0ou|v?6lv@Z=TwS8y4-b>Fr?%verfYMJ8Dl(Sj%3(hMjSKarrq|N(Y3kj(LFe# z4)%NGch1>BUEJWrU24Y&C8AD*wI=3akkDa~wumUOqF2#^M3x`d4g`Ru#Y7N8pTZl_ zG|omZ-P~k^Zd+aue>XGi6I}rdK4k^zK9PiG-rTt*UoHjRTGcs<0d*D-l<^jE$T;GC z-kHmx#sY-?sS8|0g8D!zW=zBuw*|B{QS_6z6A96$Oi8B3-^F8kwXnNr7Bc)s9V{K* zKoFgyybwvMci|YZYTA}(QaBk1(g9{>yTd1_dK}tL1i)yTpJ*MV7%C7_$ogwMmI$7` zE}PneO9nLWCm^K;uTKA3Cl&uCo`|L0QY1M9qZ6S*dygMaIl;~gFsc(Ky1Yb8;2PL^ zP0p5xXp-`-V>O6cr;`52z34kguXvfA@~? z32enqrNB|`yx``EBlW#J5VaiVT@~0v6G%K9c%pV>&gzDOvD>i6FpfVgeEF43{&n5Z zgw=&IxJs!)7QH&ijiZ1evM^bZ=f;k6&#gnBQyS zdw);+U;r|$^(B}K{4cP{8_@&>=%_VAu~-fIPaEy{M;qPywOGZ(db`%fdL)LOo*)62 znj(TNo4rBv@u&?bA_?=7(T{AfW|feLMpgVEp8{O1iAa%{^tRmRG0N1NbdK~t6rfoO ze6b~Q^N=^W*)~O9#)w7zOXk!2=3;k8P>6;L{!kdWi{v2{@i1F0U)H)i zRH04F(qoKP?u6`sGb@eu zn)bIO6JJEL4CR~i_C&D<`?7gK>NhQkNCdhVe=Z41iZ7fdhlF_+{>K<{%OFyT)+T2tq`-nC(l+UE z?SQ&`qq+7Vu*VH^qoqG3~_1Sg>j( zsp{PnQQunskPWE;C97>}&0N=+oaZ3dYk&gM8e65YDG$zNvY7doH4j)U7uOVHQyFe| zQPlwL32Ci~1noW77vyMueS{IdfjN6;Bs}cf{mJ%*U&bDHA4#lpiZn0Pl`72K1=dB) z51z%Wy%ZN(TT_9)GcfWrM@0nkrQ46e&f{tpkM)N^Q7p=AYIfbD_vCyf8f8ciRnX9x zp>iiKDA!5tU0A^bVUxK^#T0x;t5OHrhra3dUarX277ObZ&IknUvY>f%RI}PIL|>en zp>a<@0jBOToDC*GAXboBJUYSksRc?}qlkdQiu2FWI`M;tiC7o?J@rKdk%1@#rF7n4 z=onvd{E2YLDD{_9!O`(AtRct91l-b~dF0FH<}harGHFKODZrb>_;SJ>WLMFoGrHwn zO18UjF)YEq3g9R=JkHH*>}J`^^bEiUiLx0qMm^AKOICjlLE@niIhX5x))aYd!doQ0 zU-y8}UP<0i^H!1q&Fh9mCs0N;(C-fV{jn3UO09hh&M?h|pJRd_;>mk*kDu#h(X^H8 z{v}J>%2T2zF@!GI7$;b-c2X@&V3(typ6mdjmuwZDUTTo|JTdTxl@&Jhx@cBr*NkGJTG+DgC^PejDBJ?iq|9#%HzX=ZD@ys;LK~CJTI{ z4x{-KEB)EnJYX3@E>9fsk0wKNt)xSsd96m2t^|LTzYoNdd_XYlEMHd>$A8|?)qmno ztN@-2a4#I(HmW(8P#Bc|9?32zhKpQD6`Tc4Q zU~(g(lGCrqsV_B6>T$7ZnUr!9aA1JgIi23Z$dY67HoYSzh6CL%42VGZF_7U<5 zftOxG4Ip3_i+rbCp@+7Xn)wjzFCipTPGon~R*TK2pJA8_TfV!%Nu*zH!G&laqJXNK z>c7gg%(THNm?WLYoXOygR43yl;+L};udy5w?S7T1G1p4{11PkX{H|>N3N38VMiFmt zuEtWB+)rj^0nm@CYo-W1<;F_=BPZxrUvd=7@|{*iLb4dRgoa9Gy=O@r;in5kDIKXg z#d>S$?Wr4pb<{8}xmjZ6jy#&>gm5TGY=_&4W+>$*J(zV?S#=&NS5tkB18J*O8TwFJ zI7Qyzazg)&N(CJGG!nEDnQ`YL+xt*5rgwLQDBwdq0%?w&}$9ST6Y?0?(=Z1<=WtxI7jgs^KuH&xcxr-VsJedwBOkzvFC=c%OwuZ#>X*I5;x`@QDE4 z0`?yAd*bJv3!-6=(*n&CC8ZYWp)0fXz#{gm7P-x)sesygAel&W;tiMEEQU8V& z#x6MV1{hEy32s6G%Cxb}e`H!!AwHJ}a)Au$^R0t+QnwdHBkZf_ewtA4mU16so|<&n z%M88H9l}fC5Cw+i@V&lFCVj%kXuu>!F(gwN5nOxt=u)Sa zK5Q+4M>_{QJUb4f_)OY?{Zah-QU1b4+_i5dFS~s{6%3)|%n^P0g?d|F%t@{2TdcuGTeO%WDklDw?U19#dUb zjhq5UE@BIQAsJdsXwQ@iBxWM~=<#V_v)d}M`g)4qY!*eba4)`A&*nEE#dd0>5Q1%IGjb2uBU7gA zxx$xhrLqR){ZZH@EZk<4Gcv>PGt%>ev;+dla?UTZyQ-B3Wma@}lSyDOK&d2<_a31O zCZ7{H&oM7iryNR??nIT zq4Gq-f7Vv7T{7>za@c3Yxe}VCNZ#L8FjwMLXgH{m7lm6>k7Ow6l3a{<^Hqm|VElyP z&38g@zdZGP1!5rJGs6oNV-To&%z`C7t*~a`lOai>SHPurobsCx3`q=u{+C8COhcfH zlx9pRM{VBu)G#FGhJyj!2+G>z1nK}4J@BF%eycC+b$$jh9;@RS9y-e1`E1et50sBH zw5uG6z$b^mFzQ>*51zJVN%F-SI1QD;EN^Yib0kJIT@N=xYEmqC%{BkSiM*~bZzXB3 zPmx{I3;u&S=ourUU4v8V;P}Jz{xsf%0aZd|(f)!c6vo!132@Rq6lU1;ZbTn>8Ic~` zC{zme_&tl?)NS$Rh5Ga&FPaOXLm6fevxN+hFjWXcoyc2~ zm#h5NtM)(;TFhs@`Q`B*zjhw#-h`&<%RzB;P}nmBT<3$m`3_Znz)j>-XUbPs3#$2h z?A&p+QHHW5HZ5E-)MGt-%`FT%dL9-LS|7Y>_h_T5?}w9zFtm zm*+Eye5V*U97uLOfAEyQ25?&&ZT}t(SC7}W@wlNC=`YH`yC;`Xkwm*(&nMae$o4)- z-KvGy!2aB5auK>|3K~6IR%i7iF$mPo$oxg3;;Iq@7e^?g(HIaFRDv;)HeVigxPjUl z%|x$`=nyPslnUkxBfnl+Sdkrh=P#Gi3MZMFR&X<8xaBq~ogai#uJi+<_N?2znhC2e zW9$)OLX{r~ZzC&xQ1hq}hG&?My=?~8mGqz{zFcq+ej#BP-zyXBXPk*ME|Q!u;d+rG zA_^4UUe4VAHcss^UWRtl%8^MUw^Jk$f|IXQKwp*`@K`$=NM|*M*Q6PZRYBsyyRpAm zhVA&mbQ#r2cdER$aI8wr(E)Qm?L0Q_czj`cCSMQbf!`sq9_UdA>6j_!$H5L7v%6&4 z<<|JTeV73umJzl2f-TrZ>Hu}{<7e>%)w9CF#6F?nPq+}|Jgoqm0j;2sE_3f7HaUm+YsES4Ep-z@*FAX z%w7w@uL0WZ1F=I92LL5Jp6_n$)`s&B#Hiu7I|!L4|BdKR^>mI5S`t%v3BX@YA{yJ~n<%8qTy8Kv#E8?D41qk&zsGE~c zjlO(7n9TUFgKxZ=bVGt>PjG^1;0}FxtVTIhOF)cG0JGOWxw^p}2)o>FD^l1v{)HS7 z-=D4szuo*6;+j93MUSHE*lP{UJ+BP%3c?M^TGpig&apI~d#kda7N@y9LZ_J;25U+= zfc&h2r-!Hbh)Y#f)z(bAUc}T6P_RJrXn*nngdREdbJ4w5 z@|C7ETbm8xBH{ITR}9qivqW7fS)qJA88?_=C+pr{_X)YGoKj!d2vMPif+3O8uyALS z5MTX1fy4mBin)SU*G2RjeQmjr5(;w~iAIsq)!5aq5qO_EuRCsmDP-JClhc*cCx9A? zqgll0f|0&k2He+$UblzLZ4SAW;x`Wcxo&x!pYgVtpv?*0_LUfg``w@@w0IcT?>wfjYp!Na!AF@sf|KNj(Hnn1z8ucJpxfI zex?!^kWV<<^tik6#LVvhSiQa(Rp-86;{>M2oUK;x|4;9?(Fdvuhy54Z^MBj;Y6bss z1niF17ol% zf;NRGl2FTmZ%rG!n#|oA8>}Q|{@kl;2wYuA4%lI$RKwtD8tY+jXXXtpX?q@Y2NTt_ zW}Koyh9OF_=$$Aj%Z=ylL^S?%u@ij#yfrS*NEGiGp&%ELwNh@v-i=qeYIZ?XP#hTy zQGWKFKUlyy3xlZRcu3&|KXr*4u>hicd=|1YT&_}W-G1p>s|$^AZ;}k}NLG+Yf3Zt{ ztr!h;^v{B6Mm7!CkFeL%ZQe54sl+lhjLWP$YAjGBYqE}u4T0joGiolfiU+^aBhi4T zvHx41kT7Axtvl$W`h3ypz$B}Hx}e_b7DIobF-ZXxr5JpJO5<*B0Vn(07^d|M3G~;krh4_3%cc#b`1%`?-J;MZe z=`4(Qt;DKdt`1Qw3cz5jr|)M=A$l?>LG*wr%(EqT#qxC0`0PpW0zhvI-q8D2;^-2t z*-|)1vLiPed%Ffq#2u~nE49Q4ur~r=>BFcm?X(+WOpBuH$rr4w7iCyr^;_!UQLHDl zIq6YlxSE8eNMcwK!kkYXBlIs=GzAeEBl?qNrVSj{(fXZsqpTSXFMf$GWBeRXA(!n| zzeA>Lpe8scT3zG8t89~xVgEGXU&nYnB^=C_AHT97Wr?WL7?D+E0B~#O%Rx5`H?g+} z$_2>d1_$%lMj!Yo(K&&{O#fKpG0a$_5AV}HmiiR_Xg&$_e&4l?>ElET-J=;)qc)a&>iq{n&f$TIV7^+>n51-cJOy zRshfjat?!^=Q;wD_0lvEUf;*f6p;wD-V(rh-zcCwg~q)39=@t1`I3~^Q}YRs2=9xt zrK0XAAeAcBTk5LRk0ixLMe(<)>YAKtBWD8YbmJhKwnY!m>c(RM~T?sld z2uCR7P(J4Bi;33dc8n*+ZTQZvBM5s7sfy=N*`COWMP^PB@(}WQ9J>BUg6~k&LJ+Qn*e4*fTFLj z`a?1qgDCV2{5)6-zdJUK1iFcKOj%suUGa4)HpEx?G*>{kmi@}6n=HKWv0i@2cXMak z%OoJR;%*syMi0;vOi(Yq--?X>!m?_jAo+snDaDm}>RdZ2dGy-_iIL4-FM~}+txWK( zb4W*aDp)Bxax+d0!#yaa^uctSc;seKA|YTeoSGD=xhG&0xrEtLQ&ixiwg!^10$~BW z`7q0Vzxt$EvHOJK4*ao?CHzrzriBM48n^BoAJ1Y}+2-$2YIR*ZU6zRPnV0Vamh;~z zZP&U#-U{R734mZ>QhsKIm~fGLa%1Kw1)+d-pb5UBi(WI%>vm2#F3V0*D=T4bj28R^ zb%SOh#?&bYsl;lP*miX@%HeVzs|L&h88X!`#QZNKc7v3l3p6kF15xTf39|1fh7@F= z(Q%c(l<{w5$Z|2W5cWcu&f3wr$^Jl>7a?8V5*~xN+$p>!&&Xz*HW)SFp)NZ~3zz8K5E=Q>K$vG5~ zXTtq-HAJ9caXLz${D83rn;Dr*;b4}E_t;OK<*NDy${@C(M3*3fO>lRQPki-t)9PAD zxX|--p6Kylj(CQ*zw-3D5>*BDeBc*^epKmL=&u68J7H`vN%&)KHd|&*`)%9Ix{9L} z3|7k{ZFUdsfFK9Q^JN$INTn_Rjt>wZ?7 z9sN&sC7kG%PoV{()1g}dUGh>PkQBt^0wH&x)q!pc(?xyqL$KSo0xyzYZDWQ%(#wfaKvjl$Tkpa z_N2I-F8YPUT^KQ;omB1o>Ems)PD$>ghi^_d4#L#8)q!l4fkA<&S~#?JRB)}igJ z-!1b|%I@f{l&M;vAyyr=yXjt4IMbVCovI<79=K9+x1mbv_it}Tc68g*6Nc0)ChyGb zgi9{JZ? zSe#pJw0nw4KO5l9UyD89u&a5gD<&-y1<4O^8()O#)Z@9YcZk=ich z@^TAIWm?cpg{pgvGzfnICUArqfW!bV2FPTQS53QOI!#X88e5XIT`l=xS1HSWAc~EdfQS(hdM>q2> z_48*xD@l!|1Z-qBpv0FY z`cAm@7{@cOpN@ld=5ECJcuC3#+jw(|9tVFyeOaC&E=RKM!c{f%5Ge_ z^yq&tP!zs7Q2w{g$0+av5D>c-_nm&j<|bW8UTb(FrvWt@7z4C53B(K@fCf{Dx4DsV z!LVFGTzld*WQ&fsT=R=J3o$tF6RP_yPEb;0g*jI)JF0K)nQHYjsps&+g~|nb2a=3& zP2)U!#mY|7fR%3`S(e6tN|UP0r$lIZw8!tnU(dl)$mk1ia(aIHQsY#|vc+7=9dQ<= zhs3Lb6~a%q&0pgQ3!X@7xk}whr-eLwD)>JyaD}V?1wxsD8;_-MPDmDBZrYi ztH>4({ePSEwcmL0Dcj!SlMD{k1rs^MYI{0ip7!*nYh>TxqvUxLHM`W19r-(UWIna~ z*&+41w=4~@(s6`ihBomKrlb?q=!eE_m#-VrRvkuhLIvFtl1lyZ||NGBnE{khua}ei3H`EM98`>&1=*6 z?v>}VSm%3b2XZ??CViC7^E|iA4Le+IkrEWsZFDU>kEf5eIn%yEy(vS%CD1v>NHq33 zZ2le$E_8#Vm3#X^)_kNEq!CHYn<-Ps3*x|^zv9bihyasAnA$EN%wfVlmDDT?v5pJLWcwA#^_-AcJg?J-!j3SRh-xQo9z|VgXx8% zR7+5WVP2qua@kjH@R&lrM7VhOORUbYpSvpkctctpbB|+59_yc_+^tCzSV;jPPQ5UP<2I3?!GU*a|2U#B(^F8x7HQ^Qp}MU`hpw& z@>-k>!Wu9>DEYGbd7I4j{RKA2a*~buHp2{5l9H(9GpQ!`Ax7skVAu8@vxlLEoU&`iuvJu9l>#&ccg%6NGF6dnV!qE->P` z!)gCBgW(;6HMt5Dx(fEGws>@ZqXJ!!2SD?@B76Fv*XLp!ZZrl1G3Nxw5cYnYoUC!@uiStp^@5AqjD}46vu9-2*e%H3Y z-kJ}@S?{HGaG_oe{-&j4fK%*pTS1BZ#(+wb*)%R=~O@Chq3A)O@J=v zACm&)k#WZiR|B_Q4IWP(*U!KLde^D;cz-JBb&c@@J7oPN+Ry{Zg#I2O)--B43cb5QW; zf1Y9^_+lrB_XLbN{clJYLBMt366l5a zA9Xi?cOpfl$!-35%I66Y!+TI1Y;v>E$7HGoRk^Z1-hMsD#u9COVZBLh7RW-iNk({p ze_5!D<{ft0_(6#USZ!(!@}|e0iZq8tB8t%*N@i_Nf47?%;+>`_Mm`>Y?8FP@clIh} z9Ps=Ht7&r&T->}?uLxJa|1+ep_aWJCYW|;rd%q9-L+{exr@CN)g(C8>!J|_DU_O~N z-g~kr%=k3^&Pm>~Km!$!nMCRo^DiG~1;4*Q%0YjE_J{pdz57D{6nOf_ex(A^&#Z!R zV*9_E2w1_uUa_EXSoQz1-qAa|yr*vb9f$X)_xS)p#D9HVNWdLcEG`Ac+5K-O)2!vId1nsc2iOseP*q{}-++rUFg|A^8rpm6Wx0ip)& z1PK^Oz&cOnv6M+#t2TG38|}vb;h7DUcZzBFP_FzR0q_spx#NrE3GrW&P5QlyH{=u; zC;xw^yHW8@!v67>>-`01vVAbsr7&Y5hMbzr@@b~g+iuz$Oi1n%GH9bGU*AakB z?*XK246#V0DW~j-_6j2UxaYd+XI82l<~fY-_4ahnZgdExd;|`?VK3wK$ym#oeuo#TJr&Z{Vc+DNB+OY zMp&S{Sz{?`CKdrQJ3>T9?3Rtrqvwv%xpfXt4e*q*1}->YJ&pD5PoBxf`1|JLAXkc`mv~L!faj6_r4v(L>yW4mboBUe}#?)zqT26sA(`?En1FSOowbF2M81|ujd1PBpD0;2eo zl1y|fg}xqf7$5Tc3(^Kei?Dz8N9I3~1Xe@ona$vpYQvv$tAfAMhr@o}47p0rOpqSV zr6Uu-{(68dfPuf{xr#AIZYT@>x`&zXn)HhVtq>r!ORkE3Ps_hGiBcRF8^*W&&cfR^ z+baJ_m-o%q2B!p$M8Dsv6tW@cn~~WJ?Us`80Scq}HwF>0dhD_>FT+z9|K({?f6GH_ z5vpnxA;{~_z~QZal@kU27jjwg^>zHNqD2|%@fiNiy07nSN|n@v^p{P62em)2?T|K2 z3Gcr~8>;uiqg*J_k`YuI-)9h3N7q0J!A#a*JD$u4nA5c+DAE=9E^pow$NxFGf%-+> z`{-j+;4&Ra^`t@2ntKM{BnJ=i{cd%hPvp+LxAiG%COIVxvR80H4(Ak6%8S5+wTY${ z4d{gRD^?l8-1EYHXO_aZDfDJUU~BzRAj~tcAdZjZ%T6ZYzn^_bS>0WGw-L`e*mn{C zDq=kE?DSk^HO)<8)l(A(!Y570(34?HGM(NP^Vr&-d&c1cr^^d5%o5R!O;^iS zdMU=znE+%4Py2D=|0F#I2r6*%TuS`s&BXBe_|BOhyMM-G`lLqkDG?#XAWgCZ5Ikvo zj+yzCAJyVK>hS~7Dg+$2KEr%}0om{xu`yUh!Lhp_07lVz1Dqi!WwvV9NvVXSP4p=7 zLh-#DPStzi=bE%New4iNgfc$cRasiHs6mO2Zarc=%zk)K?98#e%x2gi+v&XzjAqww z&M!>*d7k6E^Wy^EMEG&8h(jX??3#?=ttlUi2;Df#xpDsCIe~uB$%;~QW*Y$oMMyuF z#n9N6gA5zLY)vA(2eH+KrRA15)u$Wc0b>YpF9XnGQ=XXL(xRikSHg}Us7&W84-VxZ@- zCigw=)xv1^b7~}3{j3ljKz@zZl6EsTTmQmzBG|?8R>+|Zy=guNwE}-7=un?qXd%1B zK9Q7QV_JT+@O4|oq9-cp7o^Ed2+_S&^}Mmdo5u81 zp@xQLX};5(XjQ>ml1Ri)oaF=N75XT5`2qMV5_n8{HJf-~QXy6mw=IL~mBp z7m^hD-C%c=^vVDo=#8yc>N2UdA`ROIhv;u+?LlseY!lt_FE@V z9$Mlp2^8kHD=N?6GQ6O-15qg41ZS8Z>^9;mJtWP+JxI`bi;Dp|g9TT@S-}`ToOA6DdU?BMJ#okHj^|!$l_T-}Ehptir^1y-P42%q z_7iLJa@7)qd|tdH^Jwps%49cEqMY{FxOq@Ek?HdDx_7U>R|Sp1`_~El)Sh}vt#USX z+UX0|)qSxXyJmh}x{jY=`DIiQ`_C|9g|WGZPhT>%o~A)ulwLX@o2$J=q&wb)jYmPX z)?n->mvc{R&iR#9u#@zRX1BTsK-QV~p50e8Nt!J_dc;*h-sNR9O8XZ1qXFOOTe>A{ zTGF7SodLrrb-$jqhL-lrCU=%@gYa|xr~1=9f_$gqFfI)ay zFwTjJX%cn(-P*$PYRnuH!&>p6)U;JGSVf5Ez397IW6Dzv)n3>4#CWldeiwe^+(2=7v71zQ9!dN|l#JgsR8xHWi(5 z)XPqidd)rs7g)a!sP@crHo4hdsaw+t8h|ma<^PV9rJ31!J&cMbh?2@}*Ahi#ou$~X z%zJ)g9pVt8dvWJLaM6d-#2oT)tS8)PIAZxiM*J+I-YCR!{Nm>we#!X5HtyavyY_3A zeMz8Xiz z^#tUj;xY2Pj8>68!o$-`z0ILE7Inkxnv7uKl?486JV}+di$GA@7m-RrA!#GcvwU zVIL5sr2Pz|4%5z~uH_zosV$%R>@o9q&YOV&tGuN{Lqlc)tqzgN3CA6CrfzN)DV`~F zhj!&dtf!uO`o@pmbLYt|R+Sw4*O6E+s;vQMvZ5V`K!>bCi?^7L4hwo*WS z@#uK@aF*|Of7A1*-i?y@_(Z7CH<1VJ2PHn?MB_Xv+`KB+N?F3{tB)^p9+i}yYdEYH zTA{8sTsVl=D_;`MA4_jry-dPw-}AaYnGbHqoF#jO-N2%%-peuDnM7D`Ke@?VVy>t$ zuDrKzSWA7JjWw?POQPaCQx zm5jJ+w0zMYuG8K14=?L1x@QX;YmMTv3+a6viXJ1_G)JjEWHlJ#`83`P3eC;L@ z8O&czp)Dvp*LAxFiK8^U_KC!44jLw4J66!aQp5y198ry+t;t&&cIK{A#9Ek9#bR^K zc{Gu5&-ZxSV%h%)i|$uYz&+dX^{PbOip%O|_^E5hVdW0Zm9uXvq=&1J_=eColm&7* z+!oNZX7H!jJelp}!4D(7ETv(6@GYxXMCMZX(M?muPsj;uH13j75c*4 z)bv1Gr2@XEiEk^AzYy5a?YHl3ix754=09T^5cOx6VerpNeAIC2a3as(d&B^~)A;?9 zZuYdJBsDeiRdM=V_hKIVWSe08t&Sc4@_^j#-m*jGYE!8sUKFLCv5k2H;)_CuRDBf6dz z@z-rR3^wX3bcJg!bV%+LSA4lOGnZKATTV27D61b|D5<31^f@MjS;KWPcXdQ??d%&_mKk=QtDH?|J%)#qIEtuIaNY+fx^v*ex7u68Ql zuUna5H*lQ&=Jl@XV@adR0uOnapm)(m0k1zo=vp`1s+hWIN0LctNqt=LT^Cn|BF(|^ zWGHxKb| zm$q%B^Wv9p#qQsWC+T^q4<2tJYSt8|~~mI~8L^=LV`&bXDyG zUikC#Q@kTe9=JEX={Ozhy>!*Wyx2MLCV8U%b_!vswLes3;hn0mVpVx*@nGCKcidPY zHJpeHvVr{G&4rOQS)MI6jx3b3{rD0}^kf9@u&}cNh;NOpM>SrW=G$pb---P|v@+D? zQ$;`KYTIkF?43@SH12;aQ5RWKzr`gU!#ML^EAB-cEW=Fc;Hcc}(+ko{9NA#K$IqlM zWI7fe(Is`}na(r-p^7f-@m|g!@%e7lWvZm!7Y&8C_b-$HKmuN^-438nqYMDzKM0(g%z*PrrZ7PIPUK$>DDH+T5E>)TTmy z6msve4HjDcs;W9u^VXq9i+c3xf(yKc&vfObDgi5d?PWU1aFtDrgK$PAA+MHcI1!`$ z;QQw;{2k>&-BR4A3Uk{+-|KsF_AB4T$4M-Q5;0Qx7kbXo=OoPdwJH>F*X1lGygeVH z{q$f#L&@Ghi6hM=>tt(2Sg86oh1MokZ%noQwmS5qjwAbEiE!88r;%yA^8UV{TB@Jd z;cx~d?2^FO8m^Rm%EStjk{x%6CpluJ1^xFC1?uba&esATq6{{pxf~621P?EeLBe*{ ze$_ZUmoDgOSRH)5YXIYQ(W|T%>NmQ|W+T>02-i$%C{Yle`+*~i2$taXtgJ1bdM@;S zpOJiP{tAiLL!?U=Kc(;bGY3)L%U5`8sTL04EbNFj-*+1>iu)?<4&yH^f6TThe$QFz@=RSh|n!?aM^vBD!xHoDkVWg%d#zm+HUKL5ScE%%4g3=v&S( zd^Esc`JmkJ06FHBP1VkR!Ep#*=zV;Q5h?5y1*KOEdvb$|LFSmZxall;O^5^^rS@&) zUFA%AIvTyJt)irJqd#oAseE~IVrS!q9?3-3b+s=O6Lc(7cHS#|G)5u*ko@X%_Ssuw zskzR9cUtlM-n?&&;y3=ZE!iF%<~Md-#m;M|!)7;s@^5*T zjTf{$EY0}Y*nR52zSUR%jDemUxSfyK>4F$(FseG@;6)=N62s7FN=EPksNv&ROm%DN zkf@Q4x1G-=jD_H*s_=_QIP*#>Bfw3%eoAlP(hRdM&gB@mb+Q|HtHf z?_-Hlo<8OswcJ;P5M2&V&Zlqh8kbF&)LT^^P7!eib1#^uP$5RV^pX~ST>OV{^X84TTGVngAu_v;CeTBP(g5TmuR7Md*q0?Tj)md9p4nI*&!h#n-2pJ-f5h zCS8oBzEJ5^-E87){<3W>eN} zKZ0l7MyXK3%_>^0h%oH#sUJebM)DpX@*RA8o;omUjT>hlIe0(dH}bGEioUms)rj0s zr@!Xq$C~jxg#r(k@g@nwtwPmLiU;!3iLUuZvPEHs&NSGS!16249b!aqlLdmsc9)ym zP}F0AhL_bW*xP`EKyWI4p7B+m!<>&RR{n@FmGvY(OaEm{v0FR{3h#mBdTzgT@|v;_ zKSCJ5J8-U64Xc}J(^d}@-7~X~aaJ!nf36ztv@5FK8KiD8k#pWL(cCa`ZE&n;r*A@U zxrL61$DMFu!Z6pxC70V_7n|iBK<0Ly&&g~opGpVW$cCLpJM=A?Ip&NkNO-=DCP&`c zOn|?bOv5^s|rEiw%~7ROj+(24fmk zn|Gu;tLE-8`GyunmaW5Tpokfbb~c@aE7-SopWo0?5=hS`j0u70?Gkrof4kNq+q~rY zFaYbd%(-V4sDKFY22uyaT;T4^{tyL_kj4q1sYjxIW#bb#${u+|v{LvCHv5GxTSqq$ znaypT!K`i>EQ|)m2h^Org=ES0uzM8y&ZAshJ(r;Bb@wQ;*BX-Pb*v5$ZdPS^M_tqWV7D4XtI`NCRd&+@RVxgM$OElUPg{y zhb^FL@7*}DQ@Qz2v#9dC0K!*U@785!T}y3R8uf}{RZVWg^aNQl7IlZYupyz{JcZ0K z%>x#s-&yHE<>BI>U6$ETzz`>>p+3)%G6<8JmgKr6lxXP81^Z ztxbz|?$31;Xhc@&+FKXv+P4z%a@K5rV%KI0+iEd%40dP{?%w9=&=Zk>8O$|!JLDD9 zi1a~&4DoEZAXxdcFGjIZ>t*!CCjc)lS*nS8muI{)Uu3~a!(JHZNtQh1ydww^v=CsI zooYY)^!!e-n9yA8MaJetTb{}&{#1&TRCdEsRe@8Mt!@z=g?5LVY11o`pA2g6#6hFh z_*a~r@y(aK3#DSIbm1Og}6GQ8H4Fd(DtIIu7+k(X{!0(eL2o~wT+3#2)3DYA&Y5M zJ~*oGp+dIA?$JDQF5besoC_mzo40=i6>-=){!XH_0D+dW4cUTZzTT}}Mshn|eX}Aa z^BXNU%x|P~@lsLG*+zQL+;IWnsZ$M`ij zLoA&2OI0@gG@s7~<4ie;5>c?DVYOcs)x@|Ayq-c#eF#K>7q(|K;t@BU?MLf+JdRlL0zbr zXsLUzz~t_iJKCfMCTUP^LsR-^YpSpO3{A9eCfdPex3=3A_xQ6>DnD9Zd}BW#x1VFIHe%5sET+RN9xn8q7SHA; z&^u`#3QR=f0)n;i6Rh7+eiGEANb~a%zsmX8gU)pAb*HD!WOGunjo5eaeUMWS|GN8a z8ECX`#6-UwY)9lxTy)~UQR2OxfdduGOP6nWm-)V{_OsYjKc%fvVUg8dVqX1wmoaIP z^m<9}eBCYAqzd1Us_+!gP%%Os``P7Uflarnnc3y0++~Ebi|IPD3=TCgT=MB9i+Cxzvfzie_PY|`f2dip`xUwvT zUSk-b;ID!jMlNw&BuA>Op_Se%@yv*~o3keh)X7e&b$HPvRZf8{W5az8hnLAAiK-tL zi~Xu1OogksB8QiqYY2rR2=J5dbu>snpWW>V?^CXX59vNWs{XnWUXAZamRA8n7#j_z z>nI&M(E4Dy+f!!tpy~F* zuO!9~n>c$mCA2!uYY8FDhN%jVAmm&~i{Nmg_M`s%nb}-Bb8?UUc08lR_n!9<*R-{u zx>CuuzeZ~g=ktXs9Jyzfsr5u#iX9G}I~GP;f}+nQc6U7-=@c@mcFNYS+l|qNJ%Yh( zi3Xg$(jl#e4L2#HZ%icFxzsea&b`S{1A3h6Lp)maDmN5jtg5stF+VQLN0MF zVs`XpKWmmAgtOFc{`QLjFEd1^x3$Joo$>nmZ)I>KDe0tb`rD>#?BArY^IZQp^P}Qc z;SulcHk;eWE54%QtzH|MhklegFP+80@uJ&)A010zwN6aSEMnjf!srPswsl19GFl-6 z%P2ri;>{Q3T+!RzyHg0L&uV8nVajD*!JeMPBm(=o)P0G%YW-O1!t$r>Pm(?6Acxfv z++%4q#UJbOqAC;O-x%HH;Di#v$|x3F9X_&{HIcK=u!{Z~ctS_|HWb?6L9R4)C}`YE z4k3zNL!!V!i~*7ug>wDTJb+@$B-LzQ$CagiHxNNzsUt94)r6-J8FoqW>QT|GGHse2 z(`CYo%nXsCdinEJ=`=i_o)FEc7&{L1EO!`-H4`y5+ve)VvnAAjvL6>ItjgiMExx5= z^+FfB%|1(7s0xVcwARccY5h! zB|jHuL|HBjx_ld+YXZ-7si&aHZs-K=Md7Oxl-^VrZB9# z7$h1WPdhDRu+;Gd#p!`NZBfhY_~TA#fjhO4jPb?Ab4EF=K-a=#TvAVS|IVlDe0rG2=L-*aDe=1GIi0u|u$0;Y@QdH(`^j?pJb(LSr7c{qbH7hg#v4$Vx-lWEf$u zf%!8&H>2-#k#crWdUpaDTB?O5TrU|NkMZe5;Y}Rjwckd|-*vIs9I#sHO7i`TM+@;` z@BPDHBzaqEdXwBOBvfM8SGr|^B_}>##d3arAx~YD-+By(uD4%ko`|tZJGOV0nI^{l zb$ulJoFDmJBWl*XnR=mG@vUTuWG7dJ!{rj;sJ>4l;+(?#Mx_-|Y8;Engi1<|eNvl0 zI%Q>$uV_^eol!=86Vxx@?#d<4A=!Y>>@!n0F@P5j0)&D%)@;^fK^jV%sn0Ujfi?Wa zlk%FXE&Qt*ce0}_6!liPvQN1EOmLV74YapvqcwDrKB#89D9swJfE4=YJo(}~4oJC; z7lY{i>(!k5{+@=CT=RDPQo*yI{dV7o{)87)7cCaLL_iI#X!G?Ot~!L+XeVg-(EB*1 zt$6TIK-fQ7ig$S2@k!pMA(V>uzmle5`D(xBJML+QP72LlwJtvLDfwh5ult@ixtq`+ zk+7~w!d;d-7UrQzcVE@N71XHEqHiQL)*7%a5|l4|)i=AWuNgNWA(-R2;@RG|s^2+m zbeFAnJdE_|QqgsWy5gh3LBr|oN}q_B!zxMFslueLCrO8b(~lg+`QQ_bboIs`wMFjB z*nSb1=AQwqQWwMHLuV8yp9>-uHmNnuaCF8IV2lctp}(yvSMj|@i`8Pi^PheesoU%y zW50D_@HcU-A#O)skMsL?^bW?wSCbDP;>gZ+PiEsIAMO0+`;k^Q`c?9TbJKM_Kcei& zv9!!g{kOuRsf>8xiKNPhR34p!=G$XF7W0I*xTg**OdFE1mr`FXP$b%YaIkZnZ8e<; zN)fTB4|kwymyrFq#op`b(FM0q596#6)?~R?^Pwz}h( zF9~zn8>TKRrCJB8M)qsLMRO-oJXGgLs}Zg{*DXVKI&36k+uP(q-Wj|Wj?-`}*`P+S zLk5FK8ay8KMF~$BWy?Hgwf**$V`<<;k;@8}5)`MfG(^u@!)6m?KS zcvfE|LJ(Ko(iBSCI%+>v&c5$PT(9F&-Mw(Wz%*8cf908edwdLKa@oFyQirgzf2D)Y zkXv(jqVW0_(G=`yj0S^YP9gWaRm0dxk7V`QSKIcJn1jBz3ec^EKgUN}WG z3)N0FY%geVTYXOIzLqc$QlB%y`eu8?NDfI(4q^BH^mrj`->axAx?4oQAx3EKO?sQH zeMe6Mr;S59o(uc?2AA29wqUClqJ`k><8#%-Ykl}&tzi24-Z3w)tKBZH#M~fvf8I0~ z;c`oj&V13nt|wMYRr}ISAsOqpbY{ao3cBx?ahdfTn$%QYyyz5Dx3~2kDE`haGhA)Z z<-SZ_PNNO#Z2UeRy1v=^TLT)qR+sHCmN%B%Gn&V^Rr?CKesoTwo&FNey;3a=Ewxhk$3}cRtD06h1xB4`JZ%}wu z*N}~q?XEa2d!b#*XCdwThc;vS4g={&`t9Vy=YmtZEeuV?MLYa5-fnj)R)0!av#6_F z9cdh`w0n|wL|(^k#NGFTQ}2nkR-&bZ+E_dJ)VVjJmpZFc>*5nrJCEBXlxF3e+_mQ9 zoH}nxZ#~o)GBj1WLg!TD;0ujD&ib@rPx^M668rCpG#r(g9T4ni`m!cWwnE+3ZVFu_ zcR212wirk6LQMlK^f^3;{1f7$Rp1Hyk-`yHq9kUXhP5s zhV{BiXuV$6kfk0rQar!XpZ)8}vL=(cK-jgCos2`TU_ve>t_xgJ#?;5fwfk2z9DG~_ zcUR-_OQCLW6_)iMYUdJ-IptPy9p*CASZXWlQ5lAMT1uEKD&A^0g0c-3C~60$RNF7( zKtG05KVlernsIWZ$}wgh3B65wGB2i`7lccoNd^L$K=2Q=@7B}@p0hk3dD`U1qR3Zu zThVO5VHPI;qeEXeBolz%aii4i1NZY)ER{v!PwZB(Ubl6`fG%hgN!Bb1jmK1`N!nr+IS5(F2;<;76 zGZjw+pEaw4xt^?a=bNYJIAh<`C_Xg?H4vHd4-S_sIr?9RN3mZ(yi-W}nCAiSCcY7v zl6Uu&({7mkJvx|KB#&!2Ip0yKWjvyG>4p9TBCdE&21camyVxz7q!r)hpljm?Zy#Py zw{Tr{3~BIl)SeZpurud6yu7%pVe2w0Hbxb{JF$P8g$&Y$sq>Q;lp3tvc8yNWzYKz5 zc26Wa?LieCf#~J82~@=~PFD<7RxQQOtonTE{*zBHx~io;iw9mMb;jHE6zunHPeL`4 zBx91NXlzp^2it{I9iB7`#R*syoQtAQvnUoMS^QKU?_iMMb!u5OE3)S|A8=e+daiyi zs2bV|rE~HB=rHDzaW3*o+UP8$qh2+%Tr8FEp^JsCifZgZVrT?;$5-k6{V!GT4-J;9 zbBC1J1I)Bdyvpq;bFLgm6bHBycqo9%TIILrgJ|LI$pFg#TiXv#QL`l+B^pzhv!>WZ za_eoW(zgYx)-nZhRoxwKrSe@G=7n1ii|;*nGr;n__gj#&g;7CaaUz|d)<6mXpuv(k zD(h3f+(NA?Aw(UohNLnB4K(*N%gup_pe#4PyZUqV)I{?S4JW-~9~n#F*Szx8&g@wA znmIfjQ;6Nt8h?DeGyjJ2&}-Z<(L&p9xIxMFyxYlE{J7t5ays!}IHg|dw>K|rg+&!>VX)Pc8vGgP})`v46&jMo80=rnNHJSHukUV468 zUAbx@EkeP4RHf&QImv3)-WMF;GIi0$+TJbYzG|5|K6gE&7tL;6mM?hx- zI%C>H7VM`k%zpMk)T!+Hq~>s)EcaGXFq}&ynq?a!6}>nia^1G3n~~U9h|7zlBFS;LL8NfJK{)+c zj8?5xM~L2?GXI1=LYj@9(l!AF+h(t)X63lD#hx0w?20qEzEfhGi8O$B`A)go7zauw zTL<#){wQotboBz#r~|Owhu?tj-b);rq{k^G0}pf~5;<}|mf3g6985C1L-GuA(WR5b zPy&}v^V18}+essBhGbsv5vBn3h!|qs;1Ub6A?Dw?lw*HN+473iIku` z_4aJH-8>-fpE~64&52OYlx}7>5R$=Pkqdh#P`v-S*6$0{uF?SJZ5#PUb1)=Prnpvo0ODwfD`cv@dM7#h>tU3yhKisJqFI&6T2VInfyG$8BbNLK-`Nwx zbEI#!=d_0GX{`@wb2__ha*NFloO>xv`^Sx>Wc7BxB@fvAxIRa$-GaNTtzlZ>G;sO& zxJsoE5m-z;U)x8*WA3L~LFfYWw6b^=A8|XCqrc}>tBUwZ8b8B(TbtGv>n!V*$#8#I zGX7~{E<@t-HH-6sNCFVVM_xu?GAS|?9NSUD>6Y(VMe8N(Guw9NJ$;jKp+Ffo>Pp&5 z%d%q^l+yl48N3NtHA0;=@9;xre5q++MMB8DmXNr1L`@a<$JsKHpWFiAiw?8nZ#3`i z&T_q*^sCQjt%Nf5IhF=Rt?OyEj_Ud~JP>*+z9_`&xVx%r!BtEXuVs3|HEvi<%s2gW zSWoYJB5RR_@4CaA#`TADw+9E&b~ikusSd<>+y+v z+nmx@7SWzGcdzuuQ|)ys*uB(|a>6bCA%z-Z+Faa`c&mZEnr*offph~H0Mr7gy2DGf z>&LK*GdD(6(ZKO8pCY3%n7Bsxv1Tc9EVz&ik|qY~JOd#- zP&CKz)BwZCWcWJ{!7&c;J?yNx-qGhPOp||C8PtJ3u+8K;h#Jt(1#tW;4nB#-t&GaB#|&F zE&TK(NN7%=?9lM33PXp-Q_sY!-GZUrgw>%ZF{Ox=tL(CfSAYjW_=VG|< z^dC=ZM$~OzoQadyRgy4n)GuVH5f z*Iq9vv4ucjD4nI0c6jzjf}no%`^$sOR?Gsk)KTMZ=hFrj;8%}TKEgeJZL<2q!$OOR zB5*=AE%c2Ghrk2K4Sc>f_}+CCSN0wNia|Q`9?Q*c+v?eRkw-&0-6PLSp%X5I@v~1r zi4-&d+>8Rpt7yUet6(aXMuK+^c@R$?uTLdX!hXweGkx|rr$H?5`S2%ZoKUb#`G?`Q ze~MnRs2%bpZsB7j<%J4_NWW_aLX_};$2F=GF7y=Mr1< z*Fl+AFqgGZEXR4-hvD#7U@{cvUwlDRDPSMJLrp+GE6O6oP~Ap-ftf>qg3zEi@ccne zIOaA+G7TUZMJf`EDh5DAz;@Qn?nh!~>_8b-dsHv_#|s=FK=*A)NkJ97{7_SbsAWW; zFBzex7>G&7=p$bA-bD;}1wg7nrX`Pv$(_K=casf_A$LCvEn`O2V6Z{OXLC7Z_Nx*%ad$+r9$%P^i3^ z#?08S2sYSpbPGL4bUk2Uulv%1AZm*9U{Jp+%){t${?40$7`jjsqg3&KV)&mJP^tj= z|G6~;hGA!&=6Sxa@Us%K$DF$}{#7$$j0A>v|IRh^5g+g$jli1_9g+2&l~HVTfTb;a zISXK%R1+|@rpvV$V2MVxYkjT#M1w-YG(TXl18{+IGz#E>1V#PBx7cx%u{{8(7DZN1 z7B|N97^7c;`yQ5kqCx{SHr_+5P|`DagaWM0cQB~pK+9$g42kdmY5{ffGiix3YS#r04kpz8!3Yd`(LsG07|*o^2YYkbNYq(`4+Z8- zPF5ML)7c?T0yRt?1Gut1t;mU9=V##UDB7K}bx>MD1~3!fgkk~r3_fn6>Ka%1+c1b2 z{SAdc9%~Ci=w&CP3bql=3^99T4ESn^PI#~ae}knl&q=eP1Oy%v1zQ#G>_@w7 zGDUFD=W9A%sH!0z@V)9}(lKI~B?mxwz?xsHWnRSW$oe*zF5-M@G5X-69~Hd_&CJZA zIRRSi11KgxXWODUfraA4W3mMt^j1;bn%S>X!qBTKKzUkxvfMvn_yA_W5iWDRh~axR zYHb3=qi4sXKnUmlCxrhA0qu?dX9?&e`~M0@??f%)#=Q^Za$SgddCPR9Uu>q(VNh8e z)VkTUHu^D#9#}Z}sOEFzr8IO#fQ{$F0uIV7aiv|*hUo*Kt5rZ!N)kkmYN0Ga3Z8ZvkYc|CmpNzJ`q_0_JSh_yXS5k7;hGM;(<$UE)Oh zn6+F$`A5>iFpVRB6%t;ex+3a{Eiw1y$pcu@l1M`1xGd<9$kJ@p;66jyzt8)~)<8;g zGnM#C2c}ga0#FkdDTy)EUzx*8l*eiQOp9U9WmKt;jG7t)ZJA&Wudg}mm}_KfabOq~ z;`0%hT(I#FSY)lZbQwC&YfJ^yk)T5v8kHIUs%|ra9cS&`OTyfj#*Tt-LMM6@dl9WC326E?>u@S6n7}jJD%Yr zs=^^*V1SOLRinTUk(b^VV6H(LHBdM5U*p3F4|^4r6`ow_kwEX$AGoBAxliCtn&9Vw z{otDX@s(~IG_?nA{5A6QOy~tb=vvHQJ<*r`B$Iye|nM?rZ3Q{#%jG=`PxZh@!luj9mfR1p{4DLEYH@wEGHr7`$4b zF0F^A4=QCmZvS~CbswdS z0WCnmJ$(YK=+TrVigL5ELMP_|Abk1S@+as5SC}1TJ1i%k!LFm?6 zV=DOrW14GOD1;%o5P{Zkf&c&;SQl>?VgNrBP;nnL*wCiH4@T7@4?DZ><#n*ob3`HN zQA7aIO8y1HbY||1_+XFf0m|_hh}qKxm6cSPs0uX3d7!AbG9eY{Sp08_4DyQ?<#ItN zw*iKDU73k)Gk}bt%uwHW1ydxz!WlrfMU;U){tOnOn46g{6^KL(7|LD z=<`*V|Cx{GcUmGCwSchs?7pucCZxa(mPC*8cL@XFA`SpeHb|~K#$=B`4BFb2DKmn$ z*;*9!BU5q&z2m<^LX#V}(;*{n97&rjqc1L>?@Pkzp?5+Il=D|!Lf*!HbP%!uPPB#H&Ofp$i z<$@wjgBitBP(uN}_xD^+<}#WxLQyk4>GuDH`lq52T?>Zzy?iSaB^T97z(C~S+Zwbe zflj)EO?kT8SzX0Ukbp@+mq@i*W>)&(@h26Qy;vKu?vCFbRTQEeog&hEPl zfV~+qMTQzBfE5s)Pp(+uVa6oLLJ5?pJ0H_2_IE!%Ha7s)jg9&4d*~f=U!XX)gw4|X z$CdE%@NJ6IF6G>7YhQDYwWdW42B9Gg!j(OSH>h2mZP3CEG0uFm?A#|CP<)m%{=c== zZT2-6xsPEfeu`32UEN(Hm5Y`dhY=hIq`0tqD3*F134T0URW)xs#*fxpu7Nuw?w0WI z(*C@j<+ePWuk`-jtgi(#vmm;o0?6%O((up%nyw@B!VP^p*pMOEeu1sY3c8am0MZCp zhi?a2AVD+I62(Z1TUG>UiWDy_)n8heJ|Rbxc2F2*s8OQOhd9ZDhszT%3bHR@tVOSp zh;iB5L&oswkJR#2lMum<*0whFkqY}|#@8;G#yD83eE>8#O=XRUY znUU=ogC(D~M1%G%pg|s>`uAF(C0H9{{j!sDsahMeg~RKUH^F zy)~rzkY}KY)NNPaiN4y7*%Z4K-v0NdXhMvhLf< znFFcZF5j40c{PZ6k}CfU#S@qz>RmSCb=z&oZMn3oP;z(ri=!vn$RLcI`0@=$1|zGf zX@(5k(1X;TPod6S^#+3edzn1W&Sl>2`JGmY(@B+X|Af|z!8pwp_&0ZVenq_>$de;q zzT1H7@bkmx6U$0lsjP1iw6?2tyV{Zaj@Qs8g@v=gwRW_pUpDVg?{tO6!orHXw1DyK zQa;sBYd=J(gzVIEJFPTwYYOpV!fR)H5#CTS^5*T^+@hk}=7|jnAGMdvB{9wm8?OL3 zsQhYmgAlYIz{06O?VgmR`jPQ%pC4D17NdGg_IR4tA7f%rD2}k(Y7f26_B2&Jtd4Yf zQm6@3_ED;~!1Td;TVVL^vQhkP1G0rJBX733(eVDgC>{1psUP2+O5Mk?4{gGAABq6PRx6WeZ8ejznao5qBun#8lJ zb+ranu=3?^fF~!=!D=8ouv9JVuhwTK^pa}L8*3q-wz%*0-0c12UvkzY_!#|UquOi5 ziyj>j{ZX$u{ies?U|Fn7471?}6hBMrRfR1S)z%fde%!XFKxTLnS$^s3(vCG@iW~Av zeK$t$r6}Ng*z7!KP_h5KOmU75GbllpbmIYMmlwO6$B2NqG4ES{Njy7gcQo0P3Jbv=&pOlTAXiqetZ2qIn_7TkAS!l3PQSyc)3g0%=7Kt+*D^}^MvzHN{3-zKBEy55{3t4BhR$YlF3&yR0tw;A7p`l7aG>M*>tA`x+I<9%QLPI$Wj&PuCC?%v9duio8EvH zrTdI$5c;t3fZx%(>imUBfA`Q~dXLBQ!?Cj2c+r=8e(C*@FDm4JLv&U?gD@7ObUfJD zAGXfA`x0qv%B%KSQ;LERlUgBl)4m2pxmBgScI(Z44js35?|0$IJF6ESP=9EWZgxre z@I^-4aRkZ7Sm>g|UuTk3gh1MzdaUSY7;XAmi~QtkjxJ;_t9 z`Q2;7pP!c}D}J>0+->RSl!0S#)Nq-#>1nLBHh~$&EE(zWzCp#6X%WRv)i$tzuehw& zNX}51@o~q3u8a4S{i$*?|3SGxlcVUdJXUMp7s+Xiq}PH$glT+{JmT!ZDq$QyQbyg^ z?n!Q8^{2>G5~R^AVaAb3;vbQSURb)fNiO9*>i?R>@a`fZbOmZ7S{a1IjFu-8C|aW> z;#lk^!lUIqNG(Ju@LgO{skqi?uuUe^Zluz#XLZMcR_p75AsqW^)-7^$k(Z!9HDJ3Y zGy1KeMHnK3i^4H};m(S4a0o6N~zFT#N z{jti_FiRyqmY0hjAD*Mbpi>kJWAva2YB|C%I5lIZ+R|4mpvxM zmW_@hG@O8YwAq+IFt|H~lH-mqa`%zlc&ER~;`Apt{nX+-H0dFNH zK2AHqck`{)M7geBZgF?^F1sOl!MDU3omJFw-pe!BR4go~Nl$y4Ll_Fw5eycViL~o7 z)~#0R=qxLZ1xI`DmRGX=!a7{rW)+vTpw-^oSD?naj|FDjV3eJm-C0M&Z7$V9lDzqrFrOc1DDF%6=rfb8Ls#AQ-9m=snb?7-A~$k~ za}@m{280-+UxA3$tp65TK|rK=0gpLgYky**|I1rL8vfUV6v5?})p1!kG7LO0IwS!6 z?6Zvi@{Jg+A8Ni`y$lSAMgo-1XeWf~6;?ff``aA;t|?nQ9e3A@?)mi5nd@3nlER6? z)rj}^TWqU&=Td6;HdRbB9ZZ9xcX%2W*DbBpZeyB_@^G;iySm+)2^fy2W}T`%mQrnC z-$cE4tiHS;K3H#IFkcR-WKl}|`5-GPDd}HMj3B6Smcg&v(AV3xkZ`J6Ktk4mcn9s@ zkm`?f6Z%T&?@f&lGrWfP!0XcO7>-G#v06o+EuAF;|53Fki^F=&ALQ8auDD@LtiH)g zPo<@kB}nsgFfN*BBM%zt@`{$0hVx(6po2xm=qG_=w%WEW z&v)~GckJ^h+zZl+jn7V+FicB2t(H%K=^^AUuo~Xggm-z}MveoK|2RrP1W>5QUBE!6 zjY{JIU6pd_#MYbNp6^L9-6-k z!UO?W1U}p2HHltR`;=Z!V9(4%U7Ni2@Xmmtob$1HWRYjx1}UZ8V{Mnp*=>>~FEe`` z-P~QK0L|Rm(Q6p5UIy}?II1s-te6lXPc)DbKl*n3+_$l!N(T7ed91;SEM0P{K^s|% z&S$LVh;f;h!YI#b&Gz^hJCXnHrIY7n8w4FLE>Kip=X;Vqqq2^!;bCD<5H1bvWx6S* zX8iJW$sPlrad|%hquCMDhzK!+pF5l#@J#)-wVWA6U9Xi+zew84^TV%eI;xL-?I}9q zV0?hR=q|drTPr%dTlZb)Va)rZ4FOi0U$mgtiJ~gQZ3misH)cLKtG7dxrOMZvzZlV|#*Z)2uoyhM?UpdH`^4Vg zXVQcYs@?D?SH$-f*6ZAmkZ+g}V)6nA1M07^fY)ZCZVPb7dqxE*F*APe++dX8;hz0n zQB$++3#ycnHi-XhW7W`B{>Dz(!=RW|+e?YzCsE+GAEqQ87uW zuJ+!pW+`RTk|=RGrQ)mk=k`McTac!6wt{3GRsTQOx4p-Xv4_c|k7 z?(Xj`^lvHg3gED;tcQz(u)YfWktB9rUfzLfsBO6|LU~Z^Obh&|_x9r7Ztd}Z(WZ?7p!S5Y%IS=osEHrKR1&!Th~b$64__1Z z_mDCAOIB{(pV?2G>fDaEVrBFm@*?FYn|cHmdiB+JCsOHz-R46W{8ZJ{Zjz0rJ-{uu zAJMiSF3U%#Y8qa3F-FJ9TU3v>hjJ5LHmzVr6}RiI&x@i3Uq|$t!99s9>1ouCSeazq zp#J;#OOOI3r4>vv3-JPF5BV;qTm%L;Z*r3LW4*>NDg5;40{uIi)6(2BK5Hf-jQdcH zZ{$`M(yfEn>e2a))fFfRV)ER8T>KroNAM2nWSHnIDe{Z{b6gVjX%j^LRDNy-g!#LJvIFXuI?xX-{=%F>+!{NL&WhT#~$Se zQmpp-yYjH(!)JWdgbuqZHGAsBwN7yxLLSpmJrxdPs9;!^gDJQYWGypub3eS!;;F_} zoK^A7vINBY4B`pjx!vM!%+Utvg{8i!JIQVX140gsg7gmN+lkUx`CiHff3o^@P&;X^ zoH~;~!B;W@XDqhSu?+i>t(lDAgI^36%AH#;+ZzIhH5)o^cg8P{?lP$DvONo{(DqU` zlQJ^@SO_Y=qaBCw(Ty%R`X~2QYs3y^6RNxm*Zqw%r7cOKd1yU~lYTmxg^HZWj{H;(0iP zfcq{I+w1PbZQjmEI1-tendNC6)`*tS^&4f9WS#0G5(V=m^TB(JNyOt4c@b=VcLC-!l$Csjwsa*Tga`uO6F{z1) z`o1jS?E{u&oj#q;&d#nK-RWYsq8%_L>_wNyzVt;f>sXcpxB|D<7vQ6co_P;sLfkT? zS$z8I>1~UW3+~zde0)B-$e;Bi2HMEX0$(qWZ1?3vJXpEmixy8FQ=o2|X`Z3SE>jx+F%VTF>J7q)U%&Pe*&WY+HnirfCZEV zqwwflp7rTmvU|aY52_$S?@c|~%H6_EYq^V0L`1}AKg@S%;NVa!9Yqg&_3G6-gfpfK z677Qj{FR5QnU?3b)?s!di|S@v5M0^9G&BTe0ZU|7lKD?&$&v>;*6&MedQjSgO>p&9JZuKhYm0893T6d+04#15YkQCE2|ApJA=Mrtc%9xy8x`Qg_UBQGm z0T2m5UH)=Psr#&Z;m&C8W1|k|i`=%hm9U|R9 zejOvvY!cC}ZjZMvn2_Voe$k`lQKB~8^j+y})2R0uS&JUIVnT?(uc%EsNE80&+}wfd z(=E1}vmG`Q^>u)+$wtY!l>?XeeuZ;y&1XgpBj}$tV$C(T6=}chJMW{%4 z=GLDIjtQ98gz&6R0!h;YdoF1HD)oZM)9Jb=FMu z532(C$e)JDJJhaAFW0QrTeCpi3prE`42lTJS*$?V;}!$zRYr%)mB!F;7rOtl-Dt4g zpiiIBoiadne--3G|Ic4X%Kt9r!1s>zy25Tt-6w~u@WtX95;8n?%00l~t$q!aA%yFE z_X7~E7jAi>LyG@{U9BiLsvYTk_TOZvHkdb)f1d6~Jz&Ou6iNN(T>#)=GXr$@=wD`K zeQRxhE_Lx^3w^1#-)w>jD({D?quSt1pv3vi>tO|c0P}vpga26uCs4V8C%&U*)SYwP@t%}wZlmQHp8))*|b@mF&=zx#^8AyvcC3^#Ph{O!PRH_(3jkD{FY ziya)Y6hNB({J*R!Sjw}<24@|t)hHd@Rlz#`bI$XxH3UAwVes2aPw_hd73EdGUE$+d zfre1%)fTds9Bbd1qUL{aHW%T1ew+&%b13L>1b3(^jK&j)X!SS9^ z(C(j&|LN@hcl1PGvu0oWX&SY#*(F}-E$(;Z0a6|jCFXgz5*$>J8$?LH^b2G|gY!L^ z14X)}W{xr#hy1U_qjt;q*NH!b5!Ep(m!^gaMM2%v71h|$nW-{?$#(ut(;441f=rv= zZRycrKeyld1>&nbHwVqHNv6pM24A2?&b&3QfD7;ZG=)wDQ)}zK01W&;nf^}`JVt>b z=0LI7`=+j)nd9FNl%1g~iU-lNIN{XXgQHb0_Q8R;=aD)M2U>~Y-N%Upitq0!;p5}) zaB|gcsndD>j^VGOGyjjJV_}1*AucoEo$(lIN|Xr75k}qvYsC~Yjovu=6?QS(Du+AE10PHzSNWra1(Sw(x}rf$;f1IM~?Ik+m7|ISWEW*qN(4&9M5IRzdoeQMj5 zsYm+!b^C7S6*hFxBmr(X<7!6RVt^+qg1T^Z!uxm0?w;UE8D*N(e{@f<-FbEr@h?!w{R0X44@dpp+m; zceix6f^>s4h;)Orzf7!4}d9n9wZ^F-RP#frajVIE|Rq+TNW`%Kry8c`+g4bgdkC_T(p8H8D z-??O0mcKt-#sc}6hV;uId6nhD-)Iv>InoCz+0n{bkGzeISJTeR37y^DQWiehe2o7% zK}eGO;lS6hN614Q+}?s}$pEE=DJ#&WR{sspP>`9bs1UvN%;J7}RSdco3O1mMN(Mrf zSIRO0Gf8Km2ncX1aWw<1@ZXN`4(@Svy4n|zH-}K&4b z+Vg#!PZuU0Aoy^Q4BgE?7=xee}TH*#D-kAvu3 zR+2}FkJy(lUwVr{+5{jMl>!Z#M~_;$dVd4(|ClL3TcFY5c4=3M0vc?NpUBg1@T>;G z%M507Z`_IcL$$%?-eN?2%ssZUD_iiqKb&}bJz?t_ulZ6F3n|5P~iZ5 zGuNTW`O=*>z476~y{deKt!NTuwq+M65;UfpUbkppf}@H2fBA zV>~TtjVx1TC0gVhEc8%w&|A4*d=8 z{cTk5z+fqayHQt~DF|cYGO2T3H#h8plmGHz{x|33@B4Wl-PBuoS-6Qkl{KwfWRGFY zYMB}z$e`Lwevus%PYOS0KCkVDGnu1l$E4{`_xW5_;_Z5Obsm|@>!BF;B^G2zMopZr zPF5Dl@Er^{{R|GFz>(DpMK{yT^E^LZ2rW5m)%-vyy+NUXb*W1u)$*HV{rB_b{^QZJ zVYH4Xobgx0EU$Mke-LX8cSV1D<`;TnS>$GC?w<0iQAwijHp`=IJv>uLbUh-OHOhiW zApkbwrQ7MGD_SY2oGF>)c=e4Dy3*fVYLTITLHMn~y_QlcIyuSXv}3Cqs9|{@|JiY) z!B3bpZp_qA>fAP6XbUcBNW!w}iQ}mB5d&!YyzmwO{8|;!lwdi}9a5Pqvn^N-j`xo` z7zIeTXr4Czd$;eS1y|oOl)LXuPn7>W7i;d8V&gr3#YW676tj6EyoYwn70pR^E`(?6 zlj@?$bWJ%(w3p9s3<=F*s?kxX(>+k1ymGUjkK9v{(H=kiWIjd@6J`)z$rJ|#-71G$ z=+4eg+mKQ?V*Sri!`j(I@ItP0l)qr|z5o3;I}Z_MPzU_)geMBZ-{J)G{V1poMW-(( z$)mFqFlPg3W6t9e&6wRHK>3aQw~d~2_wx2e0=$MEOA~myB(Tj9d-sNr-e|u1DM+d| z8i25Htqr7!QUD+ZN9;xNAMo2>F_gm=s6v2+kay!Y31|B_oYwfakq&@SVAMWcN0}X< z_%G2LtN^!m=>@=omyx+}Uz3M^e;XZRgGMWh3|m@Z_m}hF*0juOey{e3;b)@FkpuAV zEtu6{@M=8Yv~mIBy!lesb+ep%rwRFd1W{vM)9dyBI+3Ti(-#Z25C0BMUg@H8e?3JQEv04YN43Zz@nUtzJMH_goT| zR6c^@?nsG|q>-VaFZMosD8jW+q?f}E@xBN570=|~-FJeg-~uF)iZip|M-Ja4Fd2Q5 zo(cnap}8wDF{jlg{)(J-NxW>t+*-H$Rv7lFJ>XVD*30u^KPi^Uz;F7P-~BY(aeL-s z1Fhh&ya{B2`2=(--`uNKQUmR~nc9zQ1X%BTP2ha1b1uce#FW0^xr0!tf?*EQ)L0HH zoqNjG*jF(QTsoV^fb6>Qxv^fM=KrBb{erQL1q?(a|8(DgnCvlAtq5wW{UPeu>!7k6 zFTtW&q0np88ez93GN=QN8YY0AnB6ffqL)XWmk`IlhO^}-T(|4brypo+OL}cLUg{i8 z*@aFY8u!GS{#+R_o2n{iI1@_41vtd|**XZc&CX8`ChA=)?bhStNqGW>)5YM#C019=rx1IRDuOOHV9Y*OqLf$N6w6qz~psjuq#CiyX^U&f93ybr?4~U1acF z?F;EQLs=6`E-M=bW*S7ZB@L5UR?jrb`jGk##^;@2aQzfKdoS&VQkh79_o#If_&=mWK0qbm+Tk0)#lhpckEW5=T zsljiC&fi8FEfp+=B81i*e)Ios$)cEZ1?SyBF2wzZ0>I$nXsTr^-u1_uCpPsZA;%f9 zTArncYcBRBj%gtEJC*|IGJiiyAFn~>>E(DW;YwAf%jIQK$B)seA=dh^!3Y!uq-+sh zG}?-pciAHNPesiL5Jg@@sIc}wQLAPRAON}cqLN@S>9aXgxT8Lmb$@m<^ z1k&%{7?Qxh8InP%mZ1)WZ@yuW#gp0v=)KZ2*&yD+S_Va)%EhY`R7?#rg~G5-IeO6_XN1qIRqXXAYv z@-keD_}U&Pi|tM?YDD#pkwWpCSTzHSoNbmzJ-f5 zqcITcwxC!qxL8LMfiEfBxS28kwPKj05!JjVSo_pMHoCs9ds)N37Z!S}p2_hc<4Yu+ zBGt&qNN%^|Pki6s5zw(hdgGZ$67;Y9@841?NHGIc{>vGLJo(kWVRqqSEZ}NtYXBW= z-{qwE{|TCa*cwn+17@6Adk7gouLc1{EFw0R`$ifw3Q{ZKH{g-)yR1B@)d#3k!{zxl z_Wyg|fIiFmASzP)YTuc#eMiJK!&fQh2A=)4(XacT)n5P0kKTHv|G~7|evg9>D&}32 zlf6Y@tL_A}AfG0X+x$FTFPeOmFC za!X4U>7yhG2nb?q5^W8;(y4If2k`%D(|V0C82y2T0)7c^VZv8D7P|JQ zqyD{jJtw5blO8KrJwGEW%N#g|R`c#@w4e`7bVYTkqW7IY-#-Uv>WER4HoHtD<5T$W z`(yi`1HsOm)X~@A5d(z{xc(qjgwDanEzpTz5zvlE79HVsc&f`{&_!LSUD^YA-3_ z6_2JWfF_Wrdfb2`hZL|x?8!{5{8g#zpPxeul7a!HE+EF+Y@0plb+iYiiO~E<8V?w; zkPZ$Ga`W>e4z`^^)k0oG_P4E#ApJr@2>Uio6W}k_@pOMV{f}Z1 z!HPZx&w8EUE0KatTfnoDj=3t^v?~D#WLnIy`=1{SK=xW&Gn)nggu@e9!R_D+g9j5< zZYl~&h>YImM0svOK{-$L_jhzkIyxTqWP{hO!T%W4J213Z=o#S`xB%fq!7PleI`vhK za|~=eextdt(f^zlBR3K(BRCjqycpSf_2nHwEDNM}#a@a5Ehw!igp9`?nAr(}?szUm zf%8?__F@vhe~6@Ua6=Kuz91H0w-mHu!bO)m&J2M3?3%51$zqTClo@Z!9aotry& zda%Cvj{owcZ{05GOp8=mlm?A7LD0&oh?b6Sq`zO!@uv^@FF~q!>y`nig!%v6pcoo> z5_+)|KAc)3s0C2Kck7EOobXS6mj&1Js{ow}h^xM{cCI}+;g zGcC;Vr6lmi%3fPrmYd_HqC;7-swyg@*@x1Qe_uD^Xl#)kBsCL7c;$kKtkJgH}M`k|^Zm7+O$K6A%k*zF#}74!A}}Mm2k#bnTTC zFM(odCfC+#0&fIjE^D#Ri?%cj46-me0RCu&_vXLEpvf8>@NLmR*DI4x@e$m;8g2YF z`U@N1a3I-=`lpN&WeOhM^He>vqFu~$VVN4Nt7hIT)s<@-p<7JU#^>*YSvcz;)HL2;^8*V(ffru$)AxI~^_U;C$38}4VgqO~{*m2c-QB$n^-A6*(&5TTdL z@NWutY7|S1s_MWN?S!5WD7DJU%7;6ZQ?`N3EG+GL_-!J3Qm7ZFV~uNlo)@KjOE(3M zpS!PX&XNr65`&2gew6*dz@DDEIP)gT`TY1{D4oQw?dn8)-qnc4f;*j!-$sHv!^>_h&2#!av`KAM*6f-IvJIMY^wg{y9NN?18b`~rc| z{f~?BdtjYjFHP21E1M{Ap(Dzhkn(YBAu!TrF*M?EeBY2imk)lCdUYrd9bZ@f2$!^P zBZdU2(gX!IRiSOnHtNC()|uwQwPb)T0yiGg%S zR#tWlU{)t5WF(Iq|Frt6K>MU?o385tW-Jbz60 z{xjyallX0r(LkJxA+d8!c6dn(#5moy3~nv-!omN>repS%{SiMBy%V$w<&`}LHg22# zV07$_MwRV;gw1NC#N`CX4+-hp3Cl1K6L1r8U3wBG#@d~Ld{SuTws3D(?k*bzBR!Ji zG&Bm3+KfWoS2Lp4kjamms)n1BLGsRvXik{!=XZmRjjiL9W9ZvD`TG58HT4G#QXXvh5bOM{0}M zr=GgM8*JP$%5dM!v)x@?*Cz}Cm`x!{qUVJ(D5@wrNTbkyH#4O+CPm5L0X4i59Z=!U z&CZsywcX=XY5{2<;~khwCgZdg?CF`;7SpDW0vrfsHMOsO`(kpY*Mah{Uk&K z@96inAZ$vvL^m|;s@C;g9E8Zc1vkp!vhW_(hTIzSi*olO;W1U#4(hBX=!OmBv`hd>kVSm9Zr~^4kcX}_BGbkX{l=2d`U0Y`~KhCdYlWS z>V-Df8a#cY@BsGjOP_|^6+09iHe%qx$F~pR3xkK^Fe@#nhv1cTz5y{aHy^TTC(8oy zQakcg#}n|5Mhsq%hWuuzS~1kDJWOyu9B-C%`!~`Lv&1OBOW=Vo-on@P8RnKbbtyXG zE`(wRwvx$ra@hYhC}_7t=K!!X2*@K=99-9l1ZPh*p_e$F_0HV$0`=0D_4V}6#lJ3S{&R3XXuKfO zK5ZAc(4U2#AB>rw{#^Abn_~tQ3RE4J)hB$rpYaYWfm5w%*bTNqVc5O)k^kJ=cL1#^ z#53S5!o~kTLha=Qn@%H}_x#!QL!0_hxpQ^oj}VJY=OWE})jMf=h+P(n>q@gk3ND?5 z>t|T2Wx2G~QB7M(rc1LVX4d!=9C8Nxs0Vg5u+6gr;-q^tb;8Cm1=n^v>-ADf_WT%a zqfC$10{1qpnA|fuBBLHTcF;&9sHU?zzBm%*{8>JlYp_1aol2VF_VcMx`i*-YaY!hR!MU<^-P?}}I4#^hpU3w6w0;awyL?%Xy$=gIc&HTbO_0jV%Qb2p3bY-7p8)C`k(iihG6at6 z6peLkTTxNdzoyo;+aFU*obuJYEfviZi+3fqm`d;Y;v3VBRul2i9_I^AyDrV*bNgJT z&MA1j_rgk}PG!83zWDGn@nsZCv|Y~!S~VU13+R{Z*dB)u>(AgrWjIY1nV;u*GofiX=W#?M46eyN+47e{$Z<D--jN>}|&$^C}zEra9oHc=r{{{g1qJFj9$klJPbhGY?Yx!0e%1LT*-!DoO4sa>sU zvP^b+j4h{2g6#H=M4U$cpt|UXnw?w^T^~Lf1+zqB**+3IR(f?L73%yEjp%fmSgu-% z)1yZ57e6ctRfWx?Vr;@oAl>)(r~TJvCXJEA_z-QPGGh@2<fJ(#(+^>NA7~o(AdNo%T%F7og0ITCO z{Wvrjz42-22W`JQ)USsxTFjt2mCd0wbCgB}x_5Ega+DW{1Ap;w^)Nn&%Q}A$*N^dY zc&$HNWQ}i5fF&X!K*%EWyWY1HLQy#hmbvp;l!M z-@_j!)$5Nr;A8Ff+q*6Ce6=ISW*i2txIz#D$$}~PnG+~RpKdue27)FT(l<>Bjx#w& zv!1hC^Lt$kV>DUuw)ph}_+GoXCKH+k?*8?{L{k}4skQ`*So5P7mZlR0l}F_#MVX81 z5rgCB(;d#5$?}u4g&+F39WqQUopUH5hFw{Fla+a2u$ky#=kd&z|u zFPtE4^OW$y9JxW(qL8}tTo-oBlDEds55db+K55?4M-`nM-!(&180?1m-s%=Xnmbo%U}(c8r}G=aR6Q)V_6s zts4~+mowm^?*4dr@zyE&$aGs;`vt{f+50!{y>3;drS6sErrBIoLvk_bRD7hwG8hrW z*5O9?itQBUaxVi73h7zK$~>0VEZ7d9sLO^+A0SHvBQdNS{gR;ue1)t$3flKwMQM|W zCn07#oABnFpZF)2^V<8>Ce(@w_1<=?55lwr#10Xsxz^Za6 zl8mAs5fwNckiYM>g%hoX1Q3oI&<3y}E(@@NFKLFgvh%Kd;dxM&S6$k5>3EoqZ?tUu;i#e23q8yx(@Je@{HC!XzhRcH}ccS^mjyUTBBZWdnMl`o#VXLoY~y({Z*)Zy|} zy)y2MrRV&7#s4N6X?bNu3}o<|E@(AKK(iV=xvTO_4z`?-4)LFgZ8b#?yy5Z7lTDKx-+1$g7{9HLp8<`ci$u_zl z)e9nLu17V5KW6NTYAoGZ?Weh|wXHv7YE?=RhcHTY`I5~(%28(`VJW`XSu%J(R6y4m?aXLA%eb!owKEmAeO=iAY zYS8_C$vJ>{LJ?aF&1-P$!`tDd#e>c%0PMHcUu&+^Xd zxccvZ+R!lKb&F(<=i!?^&BsV}z%!HYYU3G!w%-s<7}Y9LSOZVhrhsy6DV;4Gq|B! znv`z)hG8l~g8eDv;t0DU`y@pII!kcAA(7Q}zCIb@**nL>7ksJ_w6n|z3m9gK)i=9T z{Swj5{R@Ueq$tcESMTF82&_(@-!l57UfQML7_tF7&+RWY*)cl z%)XKm4%y*uIeQKpjpm6v60gNXsa=r6mXs{Lu+R z?Vv4GA?Na_y=_9<%w$zg)=+)af<8)Nh?%Y4u<*vDZ;nTw4B4w)-;0?w@-J(x1>JST zw22AioJMOdR*htd7}Eyi(Z$TRtmZoMN^?|Zgz@S21W8({dbhHseczaQ>p#Gg>xflP zt*$Vrb%^;S?oY;E?3R^#HuoH=mQom?x)-5EkR)fwFVpODGOSjsc{A3aySe6-gK(^c zgD^%PI{a0aWlGRey<{I*EGj9g6N!QXuFN2TzOJzuARJ6 zT*YhGEYJKs2TpXcgq%VwMkqt@+T^5CmV0o|+Rsy=^YJ4MPolG+c%dFT=0@uqI$_3J zK|+x->jQ;+{=)?6OtpqRJ?kSgr%U{H`;PjFEc@al_gQpyj1E{lb>^x|Wn9*}wMdZd zm}aRUIc2Xvhv_%k?|E7BBW%#U8>KHtpWr2Tc3S5Y^xUgr;(k4B-{dCNHP7zs9u|yL z8Cy~Prc{}=rxC4T^yTKbV{+f=%`Fv;tyhtE)#=ui!E3YGHFviI!gQDbf*pIG%fx0ThPU%dF;~{Zv-B0t=gH~6r{(fcg{BGN?22~u}Yd0 zqS|;L=)A3Hax?udoA#dm!7J?Pfn^Fet0c+8w_EOUBPBo95SJXnGV2`Tm>(gFsdNcG zYu3?L|Z zzVwKyGxgsymL!W)%{7Shq{{oc9@Z}2=WiccZ+W~VJV$iq0%qOo>S z&maP$N0j(KH7M{6PlGWiL^iNSJS_$+QuC#@i$@yEc!G*X!HZadf z(LXhn4@ySM4zd7xylD?W$Pl3-d%zqRZ6r_u)uM)mnKpP~n%U_q5f@6Ec5MUJSCdL6gdG;KDpi*P4_PEVgYxTr9Ds4P>($qtC_vdYS-`Hru9xc4=fd;cp zOAJ3=A=O7%sXpYmgh>86o6e3|8Z~F+bX=~PogtM1Q2qW>BcYde(qE~oIHV1o+9$%Y zzio)&cYD6@GtK|2rFP=QjrXT2q|R+@Up6qHElI-k+1%2SjRu=(>_ART3HhJsR0HO==IxyFh1sTfE+lx$lwnKcze58}VxHC(#4P=AEididaAR%q)uOal)-m&-0rQV0X9@yO zPc}5|%RFb-uSTf;Mu(q(H2ovS@|6zg!9W?mv@h3#R4I_ec!AVkK}j3^GK0|dNRQ?g zex5tx?%R&q)&=29!?{#ukZ2>yY`YK4J@hOC^P$O3)mlc%+~3hK)p<&P5NI}|;%RUh z$R7ubI>&$9lN=%M{`v@=wr!5!}L`)y`|JdmoW#FI5j7K}ZRir4dUx?OKGPKBjmS(%@DqkN~ zs~YoO_OP{UV@LJ!0Xd|D@@J`8nA;`mGBW=Kj})2F(#nodhQQYBZqCA+d&4%^(BZmsR4(hD1I-djR|h5z+%KoG!n1+c)pSAdwY-K?}T_F##E87?M@vd4 zRc`q&z(kT;*S0A{E5V_5fk}4b~{x03nm4n18i(_rO zRwbLJAVVXVjLDnESoM**Bb8CM(*-Hnl%+T6e9xO|NYe-ClR)TH#r7+H4 zk)wC%nhb;Wy}d7@G*O@IvkkpcoyhXA^P%g=coZ^iJVlAmI_vA5ud3vcsmdXnz_dQ%O{fmqHs)U~T4;=@zZ6D^@4 zmJot@b`t~|D$$0~j}5i@=p9QG!aodeZoV^%WiMCDC}rL(K0*HNU0{C!#1^R6(r4`}mFjN< z9_GTL>YAGKpSqwFH=-3(z>DZnSlQYGBON^?CdA0CSkVzTea`f`ngdjVr&x8S-5dAq z`#0|6yH%4j*HT0-a|&@Tt^I{3QGM_*=ICwC!EWT<49QFa)wedTkk0NcPV881TZIC4 zj%fA^#y~a|##i49n0YXtVQpQmX+ea_Jk0;-QossPVRoOQCo=)sbk2_{&L=8y7`5h;XU6}9phljVPUcVHU?Nywcom%fdQ8OfC< z^=0T6j~ZKpmBUN3%@RwNB5OTLGpvh1+bjqT!`KFR(`#D3=}_dz0??ni-i zi+DsSAHgJzI^Hp`)%;$m%GG(@F{kE%mLm1%1uckUro?hwQ@D2IerQ9VZ0vYJ&hWdD0_G&REhsEgi0US-IFQLa0CQJzTR?v2f4Y z3ymY*Q+6#tPhj&;?3*i8PwBl5@PNV+!Kh@(FQ(xB4*|&shL)fNbX{kMFwV?`&-S(R z+{191aZ}oin!#~dkc{}uMalTn+*9u>@!kN}80`t3u|LI7)5vkI+xn^B+f+xvcQbFk zR<>_w*f)3fTQ?(wFOwhTZg%ptE?@rqxF_jj^~{VhCA9R+k6%MN*+`zV|GSk|cKW)d z!l`p$q&}+u4JT)**E!D~HvI@885-!vtr@t57veJEn@(CO(&s*hpzv|r>nn!Q8=Vj) zq4|#?1)xEkc}SdhuoR>MKvm--B~huT^nlqvEb7)#GN$)T9fiO-xo3= zrZ0vdgcCPURw>SS))&mUly-eUuls85(BrZ*AXl7navP_2t+8TdPO6H*^XRX3}AS2 zB}w+hEkX7Ncj7lQS?+e8FXXWr4S26WIENVkp@uOQ>Gtj0HJcTa8}xmg3%9gEFKpU4 zpZkN?uTzK_aIR+o;O;o_!pcH+cBPxewu=%31YDP%c(f?rMU&kxWkyIb{3)g=U$Y7j z3BJ44@FIc~W2B77amI%3(a?sPSqrnJYUl({_v|!skzBt?KSDS@nG(AFoQ$$cuToEf zg~;n0E?h`bxL%?B1*8YHEEv)hUomJ;bO&l5y%BycKTjq~@s&6eU0vO)kh;%0xr3yj z*f?%&!lzbPePD#WpQhQj>bfgU9(cP{&f}{A`6!gNf>Glc zORHxi(Di!v$69%b%FU=xj5*RW_NI1Uf|(66D6KC*__!m(<8ZItlE0aKpf=yPmFdDJ zRrpR=O6Zbnuj-MHa&c$adX0ZX?a%xsD(9p6CoYzmy^S$sUfm&*Ou8X+5}pr|SE@yO zAGu(K)&BBrvkSpRJ9Nrpk4{Kb@wdFkNNu!b=4xEd%VCK&O@2n9rFr=v>D#k}+UFvL z{?n=D68?p}y7iN1rsSMtmSc_5m*par=tqm&b+b@w5?x5*e#R5|H{9((H7mz$T!jmJ z*##m$OV>!BoSx0~DV06<>~kzL=w2?#QjwOtn?vti z1Wuqy;3o*$CKGf(Zx1&hIfyzUxPUsH=G?)U?wH^0i$-DCe2UYVqXz$5NV{}%X!E{~ z3Z@)9xFat%Uf?GRF;dUplJnhOamB|F;}m|js;I2<*CrCa$4Ts=$r=Ct{LNyWLc_&g z+6u?{w@~%Xvz}f?{}%EZoQZ*)?djefWsM81$X+Z_*VLl8Y0r0^%7(Ud^3S6K1mCiG z4?Q*+fl#HT+m{;1iJOSVtxQ|rz@i|CLr^gBA;Wt$Vd^cNDEBFAo3|Cxl9H6o6`hcj z`LTN}KU=n(vEYV!9(U=!f`Q3$MnHqPLv}GA(I9&0eRurCw4tZ&vE%yYFP;3F9R}F^ zCvO&M>fUp-Qq|VvjQ*gg^mNRNge@*O;uk|DhHPxn>Zn6|v5tEp`Mz5sjtBplZkF*# zU9IZobTe7vUr@_l5nhH~XaA(gfQo{%a)Z7H(AyIhvq@;fg=|62*WXuD;oqjDy}n<<2EQ9Kbi{J8gSNXldt z15X3vkN~UZTLKRfMf0Z@_6Qxs9|N$&U-Yx7k!^Ug-1uOIrUqT zj=)ZISbemC==1=a@AjtW)_`3A@y`{=t>rBaWYqUHJy?o8xI9whkv*K+g>Rm4moU|x z^%qYfc68f~j#y=>{oR_hHApClKULOFf&S7ei9xrJd*Byb5c0+hR>Y=#DxCE=$l(orL}U! zyZ-frA=MEZqoTNxduf3L&^h@&Nsip)5$X}e7bIa6dyHCe zit_%UH?<1lfPTbRYw20C_b=}Y+U7N(1RKXD56HFfx{sPk*b`CJVB`Hon^=^Sz?RF8 zFld z>sW?G8M}W73?yeV#FI^UJt5UXpSHxRGjp{lt!q@vc@)M|{mp_y$tui6#67Yrgu{v^f?x$?)Fy8_;Q@dh-dvK0`xb#sxvM6_^4xQsi+!S35rr zNh%1WXZDf-j7{rq6fAQ7{LW4Z=XxE$Y*kiLG6C~`+h6f=zXUxT!4ta<2@j9*#FNY% zl4A&pk)<6ID(WY*^=2O6=bBDufnrOG=j`nb;~#9#KLqHmgA|gKz7I@va(KBVhfijP zKD#1dW|gbh+T3ckjS;dXFgr`nrBIriXSR5I>ElBr<=%#Z*V2a1wt|p)k~moGCod9= zMO9=!rCHBC;YP!&Pm>u+B%2t!GkQxsCgGg}_`~hZLm+}8pO2W_6585>^%BxreqYDQaca!f?5ax&iXsr0>` z5OF9j52f@o`ly3w5cXvwrh7P8Fz*+*A6Z<6h>i-d`bri*VaAkH=H(l;I(VK%3uFt5&$Yr}^M^;Rl1@?nZ0FyZ5cyiXDdba+VS{%|eT}dYzA)Gs*h0 zv~p;pT~m!G1Y!>+4jqJ7AmIoK*5}99Rc^Ffplfrmzo6>(OHYAW{R_si*=$k2!ctd2 zSzJMYG5)Hn!AtxR&Q#>>CEoL#5?h~l$=R#~kmEA85 zILb`{D2dKX#OJ`o7f*U@yUq@Qwpi9w4--I~5+*#JoV~~tC3hdTw4N}{S_NMn_TRIk zLNcK4S2OT_usML3R2Gla6T>w$WBO%MA!{1ilQJ7DltL1UPPuvO=7;>(pNAKgOjRs7 z*S=d=C<#keVuZ%6V%u;|rW@w`Os`&wm9;3)F*1VWo(L9bXvf5I@d@C^nlkfrZv=kR zr?m4!50?rgcx;2>9%NwVkRro7v(aFvV)%D)<7@vvlOl_G5kbU-Ju>Tf?+Np_SF*++L#3M64|qI_wAH^OP*1u2YG2|6q^&q z-jdCWOl>T(yz;1-A%~;vPau$-aTHe)A&zM5gRk+q;osFC@T@!5x1HZ!lVKR{c576( z51=JVFQyG~;zAPpD9fleROXmC{y=If~d8Hf$ zHE$+M03JK0qZQdmj5GVoZk8@yWV;@gIdiKI?FkwAO#<{z>pi_Hx91ZIgzBpXm6UX7 zOBImoy7%!`Ew-e(;tB>A_HH|+<<1s;;2@%8zJGxe`RKJ_8x8w{+R!0;>~c2!cdmD6 ziRE!*Rb~$z<|KL&O%sQZ3`PpsWlKsFutZRQJQRKzOu3>$h-)8M$KLd#>Yr8*oChGW zMe?~KOXCoQ0dIy|JOCeu?bQL^W?;Hk$j5Tc0c(8C(m#+S=_HCL%K6z+j(#iybCFvs z?~&)^5A0_>*>a;pQRB_&9zKLBnxcYFhy>U0NY{!O?f8kbc2%Z}Mi(!pW3(_oemzLJ zv$~^lz<=>q)d85VZr&|SnUC*6u8mx+&6V;m*ny+N6g$}dln$I(q_;bHYY7J1h=}Ld_ z?PPCeQJ3d8SX?AmuiU05>BOg9*^uo0g)+Mxa9&?(>w)&p+@c~?503^gt*=dfI|&OH z7H@$@DP`rw%gY+jgQ*5dbT;0&#}(M*gkjI)!--6YM~q|RXOsVN>xWXccKzD!$*{pq z8LfGWcjala%E3bS4qspydEc|^f~*Rz)O7ufO#Sh=G&E@3V{A66@%bg<~u;~_~ zidL>WQjNar<5oy4{z6{Fp0kCIqe6b(*gi2y9X8MPvX5`@sZ09dIghd(^~0a| zHeAm<3nd_$JXq=s?euCFA8aA9Y)=o1^yiei8G^S#+F(9>A8Pqh~bCAO7p1Z?aGXNF7V2s`eFDteFhd}8E-dJI} zqA~?Lilj8QTg}-h)>!=!|V+oI^h_}y`&ih9n*Z-Z-+eq%hEUTAaLHSm0As!S^SA~pHxpHXRP3( zQ59p&@2L&ij?+etHP-V&382|JjR&C~jIC%y0zAc<^TWx_bzkLdo-#N{(Uc$v)uC9j`fxCQ+7Aq{TZ{7`yo)-s+cZcjZR6(B-Kl{ z*M{B(r9B}6#-S7+&T~d4%X{XZpYdV%ImX*<3%pJrb=v9igdI znCew2u*Xs1VD2)uF*L7HCc0mY_+M1^j$_CgGJR|j6^6jYm7UQe7x*l;Qgpt(aZuiJq)J+_Y z)QdLe=$W(!cz#q#3sMU_q!?~k^nexyV4+lRJ3lwwl3uIYlW~1D#sEMV=O=q?mdfh| zk!$039+4L7#V8ckOH?nuabJhV?`}DZY=V?xhKOhRvA}UsCr{7M0fROE{i*)AOsz4# z9PbR{n-AORT(&5`9&q6Oc!y?O#$T#2bC2vn9&uO1#j$j!El(R-Hw&w8xx-O@gjJed z!yDUz8qsRPr=QOGl;egQ+>EkSYU_8!Y8NyPX6!8Ws4{kHIEDE;LSy<>Y%Ws@_-oO) zp1Ek)ACIYWu02&6Wbnuf8amA0^v>H1Z}BsBOi~W~F-eN{Ok_iYP0fPr(n%>nbq0rT zti(v7Kr1s*H~t>V($;_bpYWsAtv2i;ot>JFk zf!zu7H8Gk(o^~oyU;evV5^% zN=^{(;Y@s@<_tQCjZMrjZSPTTIG{mBZkSN6HBXA=Iqm%t8K0s~)vf3u@d=q_Tl;L} zuPvxjItW$Bba^#<8#7#__14d8jG?H=A`YtbMnC%G-nIYgz4uL-#(hmIvb)4#X*btc zvs~yNRg9csF*=*k4T+u6Z3U>X=p0YwxH_c47?q(j>zu9DK6{#p@c$@#>$oVN?|oPi zL_$#skx-NpP(VUDlEI zPg|mM(f2QGW;~CYsUK5e95iW=Dv74*5r-vsGGB-Be%2myX+JIoaaJV}mWHKbLd%?F zSY>->OKvR>O-6(qGfkFQ6;|;ON@Xg**P?+egrd(yB*d!)`a^_0Z(CgGQe#5xhBrxQ zFPbJH8UO>V+KVR7<|kcazY=3V?!O?PpB=pX<+6ff^;YqFo`j*$prqoD?(Oa-9~__3 z3+5e0hN!3Sd_5{#Tc>AMHe)D+v5*Tbey#73ZerTEDJxAMu_sfW%Ian6`LMle^nKw? zylNHFuFAfz7LlfsPh}ecSp$VpzOy`JwRrD1@UFZY<=Kzq^Our1ixAN9PntXG*8qzA}%#M;Oh>$qI+3_g5d_ zy*y-Yy{vI8f$rC!J#erp&sQ|f8Wb$Q-!2DD8o16@JuG#H5-tmT_X!l8Oa-;xpi~j}Xjq5a?KdqhmM*9=S$r$fcw6E(ai!R&UONA-Q)uG|;MmjOJ*`F${ z-!H4_yZ=J85Q%#z;(MN1;8qf?({SHPNe>6X&;EHsxhh)gL!Xoti+HVNiPF%^!+r@NPDIrYnU%33?NWI)_+r81+ZjB%`QYjfbfUspx z=ojlp7=1xBEt@dsD1)A4>!9AAkj9dm7$5+o-`(Lf-_v*Xi4X?)GTDe^7ob3{v zrM*BEX<1?*y9x2El~0F6M`x35b9lBoFCc+>A{<7fl@28(seia24goE2ZT|iIKYbwx zXp;Robsr!B)ek$Y=yZ{UP&3mtI!k<7FVF#RpDIKcvQi-t!`^q}N@};rYO;u zK_0hNQiN!|Q2U!+KZQ(fDf=qdAYQ?h&#GRO?E}`Z&xBF8tOip_%Nh?7>(41b@ z1H`X_ZilU!D|1j^%Pz>^*d(j}f_g7m_mI$VdAA^cGkiLtD=<{fh{0TE@7T*$`=1Uf zKv&y&(T+z1ao;83CL`E_KZ}Dyl}yz<^+JpJ;*off(6d!aM2U%s0o8)PV)n}*cgA=f zWQs5KQ2uCjtZE>(_o9dGwM5_$xcxTV5>CG)TV-1-M=F))$wQ?sKpFOEz#`J@0(uS} z7-*6r0 zwdEZ)vE@aVU$&%q!YEWU*P*BvtthU1F>FBZ0$tCXPw&FjQ9V9EEp-pGA6g*{4iiKj zeh;4U{aNyhPuAz70J{Hp07D)#)n9!FNa%}S?LWT)s-P|&P50f@0BLUl2dqt>JBiMD z3L}Z<8rQ$$gyKTvz@wC@3wm-)!1C06Gok#VIr{{>ZGRhK`hRloyd)YZ&fIb5E$y0O zqN3KjeM(Kn8NFWh$=La>v^1bYd~nd9^YjDHNrJbJ4`s|1?0?BIKpQa_RT$?q{E_G_ zz)Okf5*-To;Cw#;n|b<5@8-p}`}5uS9_}UEK_yp$cOl-DM2LWebioUJ%sd+zwwvd+ z2wEeGtKwM9Vsk-$LYo=q^nV8J&IjZlue`f360JdKM@tBTiGyaGq6~a>a}BKR9wHTd zF?tVsJo`xx)Ez)+HB6qXQKQ1f#P*-C{uwr4;|YT2I97!HYwh~}?Rf0J>L*(E{ij&pMn>HK>=M4($vFwZ4%xRkWV z6le=1$i)ZZJ{KDcf${)`~foO?(n9DXQls6j2}&s^z3%(eog(||IRpm0ho(hJ-;r* z43Hpmz`5{*|CDFIb78-%mn%|GN9B}lB+`vmB;$^9LZW*n2gpR;-L#(#RoVdrlvU?2Kj>ugfLt=}`6cJ(05W_Lhv!YTFw`>WJrV1{+yo3)OuQ zzJ&jD<%3Upe0tQSUh};DPTrnW&I*MtSM{6vOTl?r0_r&$$FTF}j5G;;yg4ZE;~P~! z_5DMMW64=v>;02S)ZH927voW*S6X2ENAhZGCjp*@+3iC>|CI?Ydq64%E9WWbx35RO zxnVTG(YJK%YM`1SH7y-6lXdU-upaldL?O`@c}2Y)?qbkKpu$mQ5|uoIIeTMzpVrJN zic>w+MAp08stD(hFsje2YOzd7M6|s{L9YdQQXl=oA%Qvx5JB!vREY$V6=XPS@ z)P)7YoFhPR&sK^;PXH#($33q^!)fV821xyWdR@(sifdfCByZKFt!bl=lWDCIV7tne zOzT9bV#WSMGcBZJugo-Wpkvr`>)`F3D>0R$Ek7eN9Rzx=P-fGuQXI%*U*oO_U$0(||CB3xoOZ~V7^!ckxyzX3)?HkuYw7t#C33+(r{ zLBS(D~?&oHAYDZso@|A>3vNI)<7AhJ+q5-X;c@_cVImUN4_G$M+G9ul+ zW%Ks@yd;}nHsvPK5t~ssjq!dW5bv)o0HP$|J^nHXQhWq2(Fy~_;qW?-0;soNvD^1= zH}K5>r12#oiA#&`Yg5`Eg^68fx14;8&8uRJB>cpva_0NeW?k4bbGHOF-R_{(>oTij zST(sPThZxO&gXxF-r57Oc_=54F7E~fIP-&R+`xzNa1L<$8%5b$8_Uu>T~e|o2K>kn z8G)dn0Z+3hROB`w#S{kTVg2e8BVy}i;@IVY-Q;qZyUbN~PFEc`!r_tp<$*xMX<%gv4u z;8%d%&r>aKc+1OrfHQLrgY>N^MjN5wfO;xsnM2jc7pK08-yIucce0hnGrdG_#7&)N zdbRM8prCE1jj?m^32ohpWsGGD)pvvT9I)_MNI^VJP``VL^E6_@Z0*;BLt;@J;ceCN5gl4xdahGZHzd;Q&H7h|^JZeoGAqcR!QCzp9CJgr1 z#%wJ>HKfc2)J1;iC&&ZNoFuZzYe*5iooYo%fh9sgyOzK~Qn9fwlF{(uxR$JD^{=Ix z|9q5`FTb6N>b7pgQbpSL&Q1t@cb$kup{P|#zIH4}scJk!QOL|36OM?W=+db%vXjrK zpvf<}eVL30-R*BE@(-B-2`xAqe#{n1BN%2jK}bmDI`F>b?|ITF8gB;hl2>Gp=LmZ% zmVWU*hbPe)zwMTos!^{Kn;l4nsDrJdB!CkB&Cc??I6xoBYhscEw3>`3wh97V!~sj< z6mOXo2=9Kstarrj1q2W#N-tl&Ob2HKIXU?ciyt9?+f|5i5)&zcI8gi&6=PQhqk9Oz z;F2@GRXSw>F!M=Ezq=Qec(7{Y zC|9vaY~EDJ5!0+)7bg?EwDVv)lTTPu3&Fxeh8o*P6iB&Nu*K-q)~5Fgue zDXH6pN^)*ClxO(80K5DENs4PX1Y#+vnxf=66mbDt<(?~LG1Q5F%lw@Sk;gs#OH?Kx zqtb!C7BCa{dODUk(|8fRjYaU1VHz5V!?(-HLlo{I>zE`0ejMf!z-!O^N)-PSFM0e! z5QU!z{M~~W76_$25Y_;iAToF?0BjnNdn@$dre*3I^S=Hn0eZo}Zl@p5yl$jk%Wh3^ zC}l>K$7C<$6G?lR?^udT8y2yLB~*sRJaZmjXz_ZCm{v&uq+KMtO{VK!QQUQVx;vl_ z@f6v#?i8rK^$q>F{aDOZ;+&(;6LCg9dq$(U=u1{?27UCJEWy1FHV zj^cNfz%Jf+#U1DE+}{9( zAEUJ05T=m~H51eFz1zy9+au9pEjHedN3J_B?DQ<-@NMYSLp-%Cs(rPTHu;RP760D5 z!^F*_h4wj?X6lI{8)N4*U)M07*E@c1fUI+(!r<4#C(4C7*K*2HfZ3|YXJ(25g@Llx z$JEr_EupvE(!BS7X8;oY_YdIXBtt-W10q6FBeq2q0%---{dG(&^fRqp3l1kgdNdL* z5L`~CXZjBs#+Iz2Rkrh>pJ?HGfsJkjDf9Cr0MB6b65Elby9bnoHo0^$MB~|U}(3L{2~!U`@htGyrUo_ z^FAg|#qoSCt=EP6IRhd|Y6!5u#pnt=$2B189L8Q9ssFeLOfz4`;jDHme7z`gS zKI;l&F#xP)*~FFBOeDu%es7aDZ|vHkpuE0`UCYU59^{Tn(`h@MJ_9c>R|y09@}hnaL^|O4-xc^Ze|Szhr6r66=%ssH z_Q&)mTGt-&J)EXMzAdQ*#84c8q|QpEn3&iuU{2$in&bTnu2+HXq${6w-ttnSdh+VU zC4ydDt1j#%lJkr$x}e94v{Tfa-C+egVPbWE{NUD{w$T^3_myt`s?Ulv=`+F_Ct%J^ zqOg1KJ>QA(?lTbwi9m_ZLhS#EBSCdSmWb{~3L=gqfyS(p@CcC!vWMtB>1du`R4ahX zv3Srm8=CfgnU0)UV)3qFLF>}f{SuXZ<+D~9M-86+AExba^v8!kJcashTc6_$fINaf z#{nL=8^8U-eKFoW>1$xKDcGJ9)~F(!d&s`6_sW!%lClEv9+qJ~D%w#{OnHV0UDjkKN`B)6yMxwg#$nWZw&4g6wYe zlgpZ6{&unSeSwbc#Epw5+gEJ-I{H`f|4ZG-4>CR>1{{sfh|K^eO{8o?6w1pX0keUT z?=ycb3*qX}Jb(tk&*j}Ey0a}hD1-`m$>_sRf{Vg=F970Hj$yTch*zoU=mut*Kg$KZ z{Xaa}kheE)1nbig9(Wn?7af3$p*p*E>HH{guipT7#p|6u{eL$u;wSn-BtJjD%#4gp zEJr|kxCF$Hh=&NF+e|}feO6v=Cg67e#*-#SS0fL^Z}im=H{1XIoxn$sY3QbfQ^Jp2 zcyzJlkE9c;O7DZuR{$*p1CW!))~)#CRx{|;#ozpo<^vY7S)^{|tklSY=>1SvoO zyUDFP_b)vU?V8>4AMb-H^vS~hN0Q1Uw}Fyi0#(?;J^@L=gq``vPghJ z$@P6=;zPi(Pv0l1Xfcg(&1z5Z5z3x^?}Z$pZI~$Jb5(y+#it7jyQrY}_#mkfF5{ncsQO4_5om zIrti=KpdwX0>V^9mI-I8YN?1r_>#hR4>Yu!cfhy*eoYvTU~oo*dOZ@#0-E8EX$~>s z#yDB6!AVHYCj;?Mcil;A-GY?c(dX{&?wu|^ET9c_1rNkGG9J6j1Iv?NJz;%rfH$%m z2pP%Bzt#PB-1OF9Mfu7SFG!{vATKiFb;0A-6#!PiRjd53kpn(A^%(Ujwp9H`+G+=@ zpY}YL3|ZEjL=Ca&ddpWZQ$Fx~ajfjst<)F)c&fX<_E~*t@k?Vk-S?_C{Jp6<{ob0D z&Tn~oD;@P!1$xf&E-_a3v>Pqh=HBe)uM^(OI%8uWF^5*0ERt7MHA*p0V>NDXXR)V5 z>+4&7RiteYP29pUFg5K@m54ea)v*30aP-0L;KMDVn1p|qMi?T0Yp%%4M$jc&f&H|o zW@Lq^h6MA16X^Dvhhl%sUsf_+>^`S`hI5_em@f1?(H}{ zs?%o55OpmS|H|Il4OyGBG&IW7c*zRJ?IgnGK`AqtfdGwx!ML?P6ai&eXTW;70Hy0j zC0m`^UApzI1~iw-Lpma-9SWP%^5*C}5w=oZ!^9H>T{~`6nLS>jRWM22QKz(y)-V2C zM|XSnYs{mnEo1iEVhHZne!B~dBoGIQu4X4V)^UmIOy!)N@HYtmd+`oH<~YX<=&0Y@fr*p$iR5S3vMvBq7du?*CYTVku^|T zOp}ZbH0xaNJN;0@-e=N+gc4A2zlMLB`}oX2e9dbw#cF3bWnaQQy$SthsDeQozudwC zhNg_-xD&eygIrp+vhxy+Xx(9t?7Y*`*!Ek;I5+jhx}&5*9-?a`L#H|Pzsf6#TNt!i zu{a*%+~q|_{n&kv>1)dJ@zt>2ZGFZMbvN1`P^aqzMA){<`c>4AuAcH_=?j@`lv4ES z%st(68_3LR*3OOSbS>`7PI`gO(y6qfC<5@B+?E6&00`SJdr5;FS-!L9uKU zdZ5vZZ{aaM4)Lun*6bs_S^0(kP6WF%TZQY|K5DEbW#?# z(EF4I2b;B3(r>jHtF*EPpg3@%E*h}^%K~?1T=>|}2Dsk4=m0N9=(tkQbdrRFgJaMfOs>!Ra15HT2AupC z5hf-k?3Obh9UUEC_~3KcX46)0yqKMtxjA_w!~CM*^jdN*@Fu-uPKpSBE(~?_P>D3c zi*FnvoI8WV=wAFi z(|sqko13<0o=Zr8{Fp*dW1>cWj7~GDKv!1EkMgx^lN~aWa0irtdBkP)#TSkiVj6Cc*qi~*p-yGJRGW(KGBd@!-fY)Y{FS`jNb|a0hoU~pN|hko zXO1Gba05>+CpuTSz9gHl%L=g_kP1C+&-%8{dKB!hRwqq;oWa0-lw5g_l{NX2>?77) zI|}!m>p-Yjx&!1gluk!C6>Ykq2@rfQNqW{?xeB zdfx}`orE4DKT~k@@>*WAwzE6-6ozC9vUvsy0)W;U&;~d>t7)li{IjLpd6%GDZA^_` z3jDkV0W^}ZzStKFcx)^z3toy;7Cin#dyAjyzP>xqyBVOcWa>!TYbCyiVmYx{X6dF| zs{^y0Jz#qcl*bPpo{cAeW|r{C(BRU{&1)YxprMr!Y?$#p<)E>Kl^$*lyIkzasL}!; z6o>0u$W)vAqBerDFNuxdtZJ9tOijzYO6n*N_Gln>{pYdP3cBz#q3uF-fd}_k6D(WX z?zF^F95=OqXFLld_uBi^ba$KrZJ?RyF>}ivC?uD>`hNB7E%$y!=VGfS%yR3oe<;{| zAJI;l+)h3~M5M<}$Dmx7TeVhc+|Z~;0(?735LC~E5EFZQdmnYn$4pOuXr6dwE8Vzo z{NRuP_o)zer*?!Q1gk2roQ^M#5e3Y^WXO=LwUzOpC<{6a==nX?cvh8!cK2tE?a6yd z+qs^vjkL_b8egPH{KQlbi2Dq?BHT|DFkm7(P(z3V<4EvfvYi%Gmkz$K_JY;jIu?z@ zZ=EeYiJb9tb@gT-F~ySVWsyQ8e#dTf(i+o7=7faXG)&ggCQ5DcX5$aRAt!5doJCHR zi6k4JDLWiCN7Mf9i)77qrDP2erkG2$+Pq1X+_Wbn&Sf&yw@M(mVekM9jVh2@hMNMU zPTQLy_xX+2V<5dEC<@UP440Y3xv|n@8+hTdoyP@8Nb2h9MkXfL6?}@+v(Y~*3Xo+n z+(x|^uRHG+2=zr_TzXyzdoqFSNi}>EVNVQ@J(VUv-vEy@@Dq7Ezn1k?SkzRyZfA>p zLr!8}TqHP~b%@O0F!ie-(f>&5Cs^bG8p`k5WNg+ z!Qv3?m)&tE9q7}QSkAVPsS>vWiwnXrFgBJ0J2Bhn{^Hz(Tx)CVDG!I>);(W{g`KR@ zePFo{9FAEX9UUK2boBL&w`Q7C!!0-!82!T^0;#KuT$><>q`I$>0GXcixuXKLU#cJX zFMUA(70iLtx~}BAhcGR`5sR@?N-Q%k<(iCL2lMLfA$}>Y63LKT^&A^-BB|hhWbh>n zBGF4!)xOi6N=pr=swvB7%0pNhc{})*$+C6<5xTE4$GF_EI!~z6kz3|=?)8Ez!gNs} z4@+B9w;{WGGVevOZy8hkDpeBJ5`8vDrK=l7E&dd+$UIu)k8kI1?v}bw7^~lK_j2;J z8{_9LhX}Aa6G{lMFCPGg+q}BNs5%jUuL5)c7%_&_V1_HK(%eoUuml7`?HsPbhm^{N zbv&k<0+u(Xz84~i>;>t;>Rcry{h@Quk9cI_?66q%KN5g}AHf#8Z7~qe2%n@)axh7? zd!7IY0_`^hNwQBk$%RGIKSWAn_s`m%a81MxF#DB(Jw-?6cih(3qPB%a;MCkCIKw@! z7dV9yn9Q@hfa{hcHxOkcfYlL{6?Z9EGSW8g>8f=?>j$zVyl(f3!f$mbJ< znQW{7_@Eic2XQ(ziy}4<jeZG=)@=fk_Veoe5!;+n*`(o~5!*q+4enqh6XmNpBk z86s`wd61wW3A~1c#kt`Qj;U0r5?i@H?u7lSr43LQ|K^^RU8)gLH&LHtwZMok<_%bUW9VRzdi-M#0^(rf(Y>yH+-YVm13 zjXRZJ>?y%k@Nn;37Cr7|?r!pCqM+lesbdcP1l#61yWQiqE2Qf-=uHTC>k7G#Ct+f< zPTcW;S+ALC^JJ2dz2ZB;8I2`Nw!+*XZ{5pRNiPSGa=HhZs!E%!WWU z2}bU`>7&_eGeEwz&iAPryNwuxfCuenu1NrS)yU(Jg%qrS_UXJ(s-<;#xzh~*IIL%W z-mbPh;UgYoo@#XU@>ovle+W1#o&Y;OgpZY z44h=;XNJwp&XON^hjW80pRos2#B>D8RwrdoXlUdtEH*1*k@g{g0ccMsD^n5*xVMW6 z1%&uKuGMD~&Dl$q;Hm^S4*>XpGD1)H{gmOC_?uF|r~Ec#3CDmt4K#qkp}PF+6V;ze z9Usm{4J?u!Rv8I8 zQGqGG7JHOvJHP{bJoCwT=w7dd)bHk04upw&bzPs`fye%yHdD}zC_ca)m%H$VaDPx# z)bd#cgYBC{)aB#HpE1Bg*DGT$EG`1tuk1|W4iZ_=;>+`K#}z68ryvT<$&#Z-U&J5Y||hPS5y1jeS+9AMS#hZKtr!tsls zN56ee#{L28zM_*;RW=W4VE|C)cDrdfTF!r>%DJMj@h3->3`w!{4x1^$6ZrK*0M%#x zO#TA&NCpxvAq`Qj-G~_>AxCx$s7)(f=D{xL-?TF8d*@|m+?I?%noHRkMeDNRxt@L; z^SJp4BeDiH)b>{5do(=fbs~K68a{xx4+c^7;NooH z1JR%AH~aP%`Bx=%ou_;v^X(%V=gwv$FoiVX>nkW8pOqyB$zPwD4V1k=QhezSxe-=u z59r*1;ELF1H-RYIFQ`UJ@%=nUDeM5W4+h3v_SK1bT-7QnDk_20t?W4Rr3;fGkpb6A z@)pUbQt;pWRsxr*leO!Na9j_fdR2(;ue`(x5{3-OBWsVy04S#%z2vjr_Vflgp z@|fQMQCbY&8^p8mGC)iDUR~$k_;WzA16ELS7b=cK@&NqJb``^D24Qa;p$Ru4*9kPP&W@BbWBf0ArDu)K ze=-kQ>H0)I(X2ww7!bIrl?6To6GK6irtX0%)e^#)Pb zV1f27NV7K2TK-tqi2!*yvSfD!!yHi9jfshY9fpO4(a_P+Js9`~{MMf*KmQ3I%6?3F z|GL0tCW3Ja_-tIfyb4KIl3?Bcj84p2_kHt$OqD-7EeaJDTilSob@87=<;+PmgZu=dsJxB)aG!mK}%%8z`L!16> z+b7}|Df2&nBLTwEW#X(Nei0AVtxlWG;fP^iwcu^<=5GT3yWCeGnec_xvWYJvtNp_z z#+_PZmC)>XLV~c%P}w&7tbft6Vy7g}MPeR=WRt{(e|*iw${|4)yUBL#CHQnUK;T@~ zp=+t||HzvN8G1;}>)4CgzF2g!_w^j>)k!wX{fCsti&AbXK-5O;wRp7h|Ev%6i9k3I zt|%O_(JY|tCCT#M5T2^JnFk%!Hu~ZJQ|0+YwJ~bd38^CPOTj>sb17^o4@Uj;&bRD; z&F9>PK5B!>v#4ehhl88}_ye3CB!oYh;Q#yM9q{&MA$Hqe7C5*Kc+A{9fbG}i0a7ug z`8c;)*!s|bBd5(KIZVR0XUhfRUVP_{!0TN!~HpNC+d6}@vB z|2`pESv`>E?CbA`#37|=6tjibeiX9*kC{U2A_zgxAN{sMz<7f}4NW%q4O#2?n(K^@*J%Iln92$5RwG5w=Z+B|r2~Eo%RNQG>y4X7xp6~qs;|qYrP5Ri0fFlXt z1}{YlgB~LwiB{lBxu_;$b^qTd3|5_Nri|+a3EkhCGljaXA#rhWqZoQN|Bm;3MSLOP zk4(cnA`B;kwhP&i@E5q9)K>s7f^}|k#f9r8`^>u^w`bgjQbAxfHKU_~XN>6G@I_Y4 zQAq(8WaE|)Du{Nx!{9MHd;!3cTuUy6WU4?J^7HVu>mWBmN&$kONzpUvtx`F;*Y$l=&A>RNpo{QoC^tdBCQ&44%r>V|<9C2*k1 zRE3sb{)6~=h71J`4xIN8*;oB(Jb>OuSP@w@82h`47kJPEgLpO_x}NPm(YkmWit8GN z>GUpu8A+sFBewi8JcmJ&Cr5W=rOMoH8{VC^`N~LV46=<$j8uraB`*=$3CJ@H#Bw>E zFoL4{zMBk_e`=Hb-$lPITtB~^QBk4XANNXGNKo)S5J_ON_}zf4f+F`vilqa(JreEt{mTZ3N58K%px6BTtKWbv`?EtD}9ymDp z--HyeWdPZ1pdItza@Or&gJvfcHg+vXe;JjC9v^ag$(EQKjc~cM@OEwm{%!^Kr?4x{0i#q zBE5)@b(q>+)7hEXEQ4VtiKs}rhj)G%R7;^;o`9{Yz=Zh>GJarCD?H<(*@?vlUl`Bh zVT!H734j5^BO|(}6<=Hs**43FGt=*XhX8F&`B9sGgctq^?G9xrK|~z82*L^SQ@o-J zFciq2G>6${X-_-uR`1T62Rzjd*&MK9n#!#k7{ro5)0_t~f)@+!!HvYt%zbK>=}?f- zNqOrIgIKX`J7Er4niN}1EN)w43-=t@sj3t00Jd*J};OJumL4qM48s;aY_;hOtj zNuNBC0BtCh8{aUPZC73%Gvp!mxjU~YbSi%`(F=nACRza)kA7&_S46nN4VijbS~eW{ z0?>1mlgHmRY~I)lkGY-j@i$qd3_RLib7I~L4iHeAzmC|jkU6=asFaixiaL!PyWV#B z!Q~R$Py6UoEob(=O`n*;$ZH#mtT*i{xcWD;oLZP0Y077Fs!EED!p{7R9V7amJmOC4 z{Pq(y@J6QNqGQR+Ec15h*~&wT;3lv{86=atj;GI){cn6u$GoTArumT3u$ydlZmxxW z{?A^LeGbkA>Jy49yGf#HBGy?6pl7@N+W=ZlY|b zorqU<2$Hz6?z~l8(ri2&&g4}{+ZM#pzn@k`pdh1>#Jl>PA|wmvO=V*`_h=rM<`FqWVLB#`>^fEvh7IC#q1do zU6x(gfbXJ=7U5`1>s`m>AIs|lCh2X%$7o(`ewJI7zD8#sI#)EK+|I-a!&KFolFMh?Z&CHiGL#Hl< zqG9Aboytu58ihNaFYZ$t2y|3;8VIms(VQ8k`^l*}4?;@MO39g?EH|i2CkoiL%em4Y zFv;iDlicew$&y)({Zc>D#Zx42+2h<^l}c~@qvHK&=acRh1snCT zWk!aJ?tP6W7t4}-qm;_Hq2$PkXQ+hbOzTcGvreUg9GWHt90JvPE?We%CsQfBH_4V>OS_ zea6^CcGaq?#7u4t)@;Y5fb)k9y=TKK!PT#|#m$Sw#I<8ddHf}Yx zH5k9*5|M(guuseEj1DzP>Se z$zc5PF?S!AO2RNplIIKmwFt&yqju-naPtmy?2fzfDc*PFGqdA=ASYfmWtsLbiF8vw zOYr$J@kVi%QNmw@KZd>Eej}w_<*fITiNruK2Q~A0A@8;3yW<5fsBYm#Cn5=(%BAR= z~4C;Ey$8}(Iz+^^m?RMJnW1``hY z6{xGaSFc@V6Y}P^Gu-3CGU^Y}3ZL(yed1zoXPxwMKR3gw*mSdPl3D*!vzm9TFWU4d zdyaCz`rXOqr=V>lwcVe~V@bHb-h+AYRmHQu$C^2ffl?FhvgQo1uu8vH1t*f;!NZ`l z(bz;rck7LzU6r%9(*rVb8nROo6oIx9wbjREMn6cMw2SlUyyCLPu;}`7`Sx~S=FzrO zX{JResz6!o+bZ=(+qpnQNfW53#~yZaPo1qXx}Mn2Mo_;2mr#utc^9xP;GYXXT=7G8 z3aTeS-WpQ~xD)eYgNu-Hj_#Bv1%Oe`?l%&#;~ab*1pGVrf4Ue+43Q;mY=0gX3C)h} zg@Yg8_lMfLmQsvX2(RU}v1oP|)gnP>58}80I82m>o=+*nUw^|adXKNSQTysGP5_%H zjxu$vQ6%Rvq4;dVh8ns!c|3XCw!vv;JNqEzz*H~?qx!hNX{d9nJTLps$9|rk+e^{R zDbj~S+{7nxKAf;xFY1Z>pPIxJy+0kgf2DheGNTL0WMiq%X-2qgp{%QK<&;FSv?GVV zE@8$E9;Kj|^$SnSu$F&CO&pOa7Bm%OF5ST6m3}FhaR+QK*CrlS@sjrx7N7MsUi&b#|Azi~!UFT46-!P=G=ekDObnX}Hil~D0;`sa7l zt%F9_U{}g*RsSGAJouq&{UeSaoQP|^Ch!rPRV;1rkj(=wE;{lp8F98~ks-I0k=E1= zQmku7^^U+?4H}sCkt^yHDfZubft3E;%8tL47SLIHXTwlw_jGvdb()EIm!2QmaJXCh zPGn#MI(9JqFJd9-Z^cD4JKFr;+=(00> zoW@K)#30+_@4S^g@oCv5UbS6l0mlrZy|rQfsDJp*%D3M59GQpG2+x{369 z5;^UeZZ!(znva=Fe3BPRm6h6hT1G@qXmXvb{W4Z?{GT53$!q3k4!mKZz5aDGX^zMD zQn0aTM$38xO_re4G~GZ}sN6ns>OBVn4Nx&r|+hUDzM7x#70qXkLn8JR{5c64nrf9^?@H%gWiQ|`FQ|_{iCU!2zTgaR;DCw+mSsCMFZH?HOjRCf?awwP`l3IkS#aQU|t!Ljkplo+9$ z30jMQA8J>mzlAte_`ut*SFno^jmF?q39nl$T^>s7-JCYst;J5?SXhsV&E3?DO+&+2 z?+?ew&ZDyYPrzxOilkwZrN_iaibJwY z=ROn5E@XVC6LXBm{5El5L_!PmHXrPXRG!kVk1aBl*fuq_!fR^&A)#dtLFP5HUortg zGS2Qt3wQ%Rfkrxu%HMt0hF>>DjtYCjvxe3q9H zuG7D2IAoUTF_b_)zkR#JblW(&TVAN}t%ZxfzasORvAo!_jDx>&Xn1AcmY!Vx!*IjZ zgWbL~x;p^cGlk+jN0-iaelz5&Q>FnoCV&x(R)SB7Xv+Kf73Lb$k_zXV$dxuhgPY3@ zIl-!72IHZ>7ICU%F1ilpWIXh2&Mk&<3+HP~V70lRwm&jpddyz7ygrjY6hR zPQ+M}PAv#lSW6?wBOSlhTCI?e67N8;q^vk9WoQ%1b8EJh`; z0`?@9jawSi_4{ak4DT)lJF3dpWUD-}O?z9RQpQ_ZXQ}__U8vLQ!<_1ZF4J4PMth_0 zLJ3y#WqTu0N7K?+47YvSWu^t{uAdzdsGuZ2TOEFxGlqKgSFxLf;DkR=pAVErV21iY z!Q;hp;ag!)T#95e3~ux%Cj5^>{eNvxz?6$W4sh$*{Gin8Xmf8m_b8fT)FrQ*=UKZ$ z1XNQ6Ybq;^$aJ8SSzly2Z2XA?x5r6~_`!{QiIAYrvF%c7RY+)P+S?2PYU-bNBM9k5 zUtlnC%Opf9eZTxc8b4(qt=~>XVy-ErCP2ZkOg*~i1_vgkJCk8*{}_)@f8Ov{BxcvP zam7{GkmBmFzS7_)vRIeStL>4u9iD8M23`{3zk}`UVD7aPfJ}D-kN4;u8i@-dyE~J4 z;LjT;FYWNn$7H4=%~|yWUG>z~2}dNPvmcay^w3<{kN`y04s{#XuAq7*UJ-`P&rPG2 zxDKV7&(fS}f!0f$pHAt@LOF3$UrC)i(~k!`!^XN?{TM8Vhb6KHKF$w;`>T}bb*Gj2 zpE(s-{h7rR_gy?Wc}|gkIH$htvlK$1IdfK1raln%zd>w(<`t*2L>QWrA#ah|_j<#o zjMF$%yhf{Yj3-lV=#cv7v(PUp{#R*Vo~?8~9(~IQqV0#5^D^@H)~ePsi;5VFscD>b zyaFvPEt&Lu)m!Lxo`QQK{8r673hw6KBRQ{LBW`}a;O6&kh?)XXsCEV!3hw|HEj+{r z%pH-q%ubbw&x$1#XG!OSG^`^S&T7?iTl1_n=FUqZB$ts-o(CxHE!De~wH%4Yx4z?2 zo7bf>6ZJ@ypi!I($wq!Da3b;a$YRoqhWjC89IjB+K!R#ES8cq`QX_+kd*T9zv^;Rw3t!VVcvkl{ws{w#wblPr5#fnHY|h^o@DST++G@ zUt`rTGd&wkfyh!*Cbgstw{?K9;=1Ju-wr|aK1PxtijLY3m6BX>TU*NC5!bF7qfb10 z^+-s8Y`<5TT9b3c*~tG(jEai-eFAQ{WyqxKZZzO4gnSr?uu+I`xaysxDk9@FX{Kco zfa_|BJauG?sb)A>_>Gfie6s0qZhoe4qqF&0QC-&eW8=Jp=646rI9@n=4Ls>hq`Mm1 ziMMT>HViAl#^oK>sCaRG^{c9Yk}AXHl#iY5c|Vnbtd^6}35MFRu0gGY+~Y);StZ;J^9k2E}W2LTwkC z_aJMIqcrkW2k`-|k4QB+>*?A(S8ceCLwb#C@8WYd;;Ch~3A8e;4KJ^VRi77`$h>(6 zp7(KAD%+s0(A6}>mT$K%6sTf?hHl=ZNR@UMhHVA*8m)~R-%Y3Fwrj7JL`pwb)F|-w z)U>^~M^8HV%0TnW{jY@V@7W3WxsRRj)p+IIzZE>L5N8gvNsYunMOrdhQ!UZ!#(Z)z zQjyvnK5}xLskF;X(eo?X7CB97iBxjg-IQzu- zj^U7>SHgbB=b!NbRFh2SG2twh{2}{|r4&sb8karWGcM4Esv{AX6^bD$;o6c_DU>CD ztaqSa>C(9nhjvJDt*Gt(J43nE?+TgQVMZ8jKMxssEH%D^g7I7mMGn$Q7$HLx^EbywXW9zwjDs~p19)+=%eP0sk}{l&}%?MtguPJ7rOW!a?CUf0IS zM{z2*Z{W3A?8zm25X*O+L?E|EcW?K3QJ!My&z>~}#ZsI5%Nr5%mKD<#m}(_3W$R_? zC)M4Tv0;y_@$O~^TjqtQg_bGv(6loa=++qRdnNN3FW8wmsTLo47%>k$r~!0OIw-j> zo@A&bBW<@ijc|!}+8U-Y#)q)+84t8!aW;KelHWAXiICa2b{bhs2SG?O$hyYI*RUQQ zIxBWFnEMj4blm4>2hEGC2or(4)&FCo7?2~Ebm_|Na2rdm>zBM}!b5e~XlHcv#H7bx)XOyonb&W54 zx9ghh-#<3z1FJeosz?np9D9q8OCh+V!AGPInc#1&?{A)M?Q^376HMB_F6TE>@V?7i z{j)dIauJG=OOXiMfLp74cYa;|eWPz8-st?O7FTu5-w`Lexx73b{fxfzi!+Qf2i0an znB<|1^M?!-Tjh95u^p?$>M(_|H;qvWnM2qMVY-=3vzn~WV^Ak##mpaPxxj41idTG4 zI&!D1nmY401j0sXwWk}N0D5nr|U;5tnFAnN-;Baz2v&@E9xov2vOG5;NW%M zoG)hC7+5ATx_ZqfNb(81!W%sx4}E=3SHaUKw_baUyDVPXVUfF})=F52BipM=p;Rx& zn^B@(*G(rSF;K{S+#@VqOS(Z{#tdE0_^vVBj2a;RTmt6 zv7N35C9wB&g=QrN%r4}xL?y+3`Z=kblo2gIogR%->s6wZ)qApm-Z!G)pZ%&mFX!8e zDKjxEzNt~nt})(_FX*iftVZgO3oR)rS%eBYYil>2^sd?h%z{)z#M}p-Kal8~AE1)8 zxm-Bt?S`xjjRKT+zCQCs@Qu+AL**SO)bb1L3Z~HHOzW9g?D{zFQt%g>T8dHEu*S~g z%I?MpGe-E;fh?Xksx((Er$$EeW9i#_Ddcj@GELU$vrm|7hh7^o;1~?OC!eF~dg@P` zH!S|+%9-7Cd)UObC6Z=ufaxNmCV?hQm|psIt+`r-o<`kevU6%5J^rx6-!6^(y1FVh z53lk>YwFm!I=JS?axb^;&)JU6Va&6oT6%8ha~pHqJ29$_kdn0ZSF_c{*5^B}PLjxJ zIweR&mx~=mwKZC7+z1<9rE?i+=gXVp&iQ&=&QRFqB1x)c6}f-IA*#7tZEjp~7Mzym zd(t)BazoSM7S~^vGguxCHkJz`z?erHCaUO|u;U&e&#!rLZZ69$5+}lo3HjYWFG5b|EeVUI#5c@K3 zHBS(Q@U`D<+&zQAQo)h^cUj*WmR_UyWXJoS8JP8Yw?w9E2~LtO57=5vJ7V*0H4sqP zKT*QSQp_}PcXKfv)q6{P#dj(?pb2@Gn&^4WOP1IIt-0)9*5A53y8H*>V z*%U`|1rbe2D_o;&tb&ly%xPO0(F7o9MS$+;A)YNeE;kP(-r5EZ7BLA61e zwT?ri^v#&*YvM;y9`e-gPR?x(k%=UW5pQYMC%=0ZCa@%C6P9%QJn~KR6Gr4+R5=Hp-s_^WqIe%bY2kr zsO0fd0Z-IaJ=H-;@Q1IX1pORW*dgK1q&nGEnXyuWzBR*7DzCqYnnvb14B))aErX|r zZ*Genw>(xnjAWPv<)XZm-hTaMi61`F1NEQKo`HxZb#CmL*xtYbsY*CoKaX-!OkOF! zTImFI-nF2l!01}?o@$2dH(f6jD=B@QYOAD_mHPv>Fm^AfW;ViYTEd5+W@PqaYw5J#AMiD&5k3_snNFzjMC# z{&UY-p0$R7nf=-O?ft&b)5dO%Sb>F3@Ne)OJ0Gb=DjbcQtxgR0*=tGS_{tS{TT32Bqe1w~nI{MUGx@nfThByIfqW=V9Q)v``FRv?~z`$tj-2uHFAGN9ZK*r(s z^0fKZY?aSS#?6}(qsK`*Y>$^_ICN)kel_ZeA~Oa_p1s{MMb?K)Dl0r`(E(=g6OSKSS8-w^QzM_4!EKopfuH>^;qvY0FK=PWPt<#Ib`B$>TO;vY2=4s+H0;eD9qjJN@~Xsr<9)%tm7 z7U5@1e^n|rJ(mgZ_&6ZPT|wua*48_cjCbx;?h03V-5(UC+a(^H z(6mh`GD{FW2{&~$#HZXDlqf2Ac4jv_@;W z5zqB1ercyQRwJsOhPPdd+~cCY-zDAYICojz(zqJ`CI|fu7;C81y}LMHGf{pX%=~+p9AiHaWS}#gw*3Ub2y*uWBPz?N#nQG`ZsHkZwo5#0%f|k+5zXHcp|2 zT|MoHvJE3(W6)I!+br2qx>WYELJ0GL^mZ}r3=$F}Gl97SqKchHKV8_aIx}-Wc=+rn7o`M*LEibSR zj`DPnXHz2P!+mt$EBbFKX)He##O>!`;We3VGn=7{o*SXk^q0-%7&BYE?ZV*C9YJV8 zb{iIaQ1-n`fhKrFH@B~IAI?!>Ax7^+l}R5p>~vFS)zs$q>*|u2Ze0xzlIBVIfOI^? zt2ubHtb*M`DuUf!j>>~a9WGfd_a1dCtlcSeb_npAHB)@X-d%3nxH7-bx9fx(O$w&- zsa}AJ8LqOlbUwcxkC$~;CB}^5WA!4@0 zVUf$!t0oR%>th)%&q+2{-N%O{cpLazI(}NS^s+ajL2M}De|^`*5Gmb zArTkX5psx|nvrfZ$IbeBW-$8MW#$b zPwA|Ya3;fC4y~@@U)Q8+)OG9mM5TT<_&ERcmM&8YF`iYDGHS#Y1~(8xz0;yO?^IMv z^z1e?K0js5g=t!~xC3!Gub-e9Ji*prbm zy;j$a@|{Oqz;xQ9RBQ7J=dn|fsz~{?gvyOq>}D$Q5iWL3RPy9Te(8jNa(Os@Qj8{w zcjj`moqW=n_0CXqRM4%5``Nj-V7m@Q)JpH?%vA4&o1fV+x+T@Qn$U}KU*MOu2J^Hg z%~v?nZ4mxERc0ho?Hr2LaAAdm2+J+j&a*ewj$UdC1?4<^lm+qyQi>@m2AN0Lf$ASr zmA{L^okG5s>v@7qCjp|$)Ppv_|0i#vj2!c*iTFF55HXI>tWqM%H%_8~@gmf)ZN zUw08X$9lN$$s~;oE7%I<^`G-x7h{}7_$2tMw$5ht3^W6UknfvC)Sb!v=+igW4}eG zfY-8OEC;A;Hb##VSYN7D!LuffzhZgwYi=+y0id8)I0>#IT+fT=ZODWwRkpjyE5>|r zGTGhAUvmBX{s>uQZHxvRUpjV;2BHFj)g#-VN*Q(Qp#$(mw#)bK%&$Q&(Xl%WaVgA4 z2-r9yB6_assEN`}h1wC_7TS7oySUUECUI{yukICr_ewBUmxOmbGqN4wnXs*IjB>5a z^i&_#S4rvU-M`unK38-t`q4{evE-NtU#85wgj;x6h^LO{DVelNv(TrFt8w?+DO{E$ zaYb9a|2hfH0tq48j`gK8{R-)+r?Mf5UJm9aQ7OQ{MXka{en49HXLF6@d`AbBIDvBi zJ1p65_?feTv^$t@=5of;+;SfG*;uy}+7GwhonzhqQGKDP+)Y=5Ic-S}{_OkQB%e}~ z{C&52LV;`l+wI>K0yfV2VW$gZ2*os!4JH*OS?J5-g8QJsBrA^<4Pkh`yS0JX3Dr@i z8KjKtiq1Y+8T=X)MAC-V6!3B&AR8b7BVE?Pxtr*5>CU)*iNf^&2F|WRKky zY^=*}x<1X7{_(?c3UmI(&KvF{?r%+WkP|q6cvKGs>>9G`@ST(%K3V{gEu*rN7-rBQ zGb{lqpM~VQ)&Kn~EBpad^_z&^4T3b*E&|_+V*NCaq=d_ehB%iQ4Y&Z4>PlnXR7I#c|&9L%A8-OGOKxJ;Y@g zYt$2$rbgF-Fq1y|+;`b!Mg&NiCZP)A8CtK3LL?LomBx8)VLrGX<`ZR}KBU-jg+FnU zvp9k(y5B8(PHT?F{f1LM*MB1?NcI+_L0IL+s(wD&^FN~?wV>DO6Yl?kWun3^Z3P@j zlBi@^MGH_+O?<)Gt>FLi+Ce35(N6phVg;{*^~TVrjK|bam6z*LC7G7PB1wizKOxx&NnrlK zo9Dcd&*RDI_Iw;w9o0UCM=IH>4Q1NJk}u%zZIb^?pS{!Yn#R6Pa-u-b|E}-fyNCf- z>1y#5?p&|XH__2uRgp2uiJiHl)>fXrPEc_eXAg7-Ct>dO=wq2M3=k9C?(7-m*pj;3 z`hz?}zNw)|gxVqM3r4VX1%+nLg!f9McoQe1><9E=3f+;Ul>J3-0=?+DfuUh>@}GI~KV?I7Z*fDV7+#DKcc2^eQKFa=1a7 z)x=xq9P#*#}uUy&9_q^~wJ)G64FI1NFca(WqI*U5>logP&ZW%mF^6B#!H6 znxYy*X5ny--Sq+!J&e`@^Gy2U6~Ep-g0io?SYj*@%Au&@QBF<5D?!GAQd5w%crLAY zMuyi)%tko2Eg2~^wULdm&>E)EkC&nfdd3#lJ~Rl+O*GGD@H0C+GNdd*U5(_MT>UUV z&2{IoyIut0(WL2)YPFN09$t%O`-ZU|hVe9imcnUur|OBGe5tdWAvG~glsSeW{aZcB3@>@H@ht3({^Jft_9$XPPPGNCO?l-qjJ zPGGm4Vb)GFxMmGMY8{MnzG+-AnYrTvQSR&IY(dd@~C;3aZCvRegYuGI#y`UdIy z5o!v%AMPKC3+6V|*pJ;Q%5cqkqO*={J^jB5jrYA_&jz?xhO!DLr4iAk&eO(2K9i-# zjOA4_yh>)lT2IEXz36pty;g8Uyj|{RXD?~&ne5#&@t-5rtGv1O*4$-?Z%URV%Sw<% zzQoPN|1Wv>AZ*X?G;14FvFsz#)AYz?2aE2{-!ZBeyecN{k*1w$?Uy6n(O+)Rp8n$TtJ&RA=YspqCF~z{|!lHiPq#AM}qa+)JvYVxGjR)7bp! zvS6544K~3GVc;`0T_XIIWGJZsPodVn?l&~6s3saGE}K>?3j&MWpJ0-tk<7fo5!{`|M@c;wm(1| zpcy+l8h~>rHPX@WirZvEb%}ILv99Z~vUBfx2kwXvw}9aFGuL&u2+IA@PV3$~KO?hN z`rU+RSWnNf0G_3dULiXImT8h3etNUi!PAAQ*`Wa(q3{PWhbo|x- zm#?Jy-*UwNR4clW?OP*W%;f(`Y|g6`#H2B`9;>S5EO%k_)w3ZKp*&&nM*|G2StX#lMb4}<6`cggDKj@y+WCFO z%9UnIWkjPSlgs>v?NUAGzXx5A{Zdr3$il!eH3nKr4Z+~jqA&h!=$SnRfJ6MMden*d zj|W6IS`mwpdH9QDE2~_;y=3|1Nr7;PiCN#83`XnQ1!)>?GX0w-gP{2TBeh(V0o+>) z3;*ct;ptRVX%Z*F6%y;hkW*-QUCB6*f93O6L0<~g(DuJvGprVTb))t9{uzQ9(nU#} zMRfS-gA6~vUok9$R0N$5oom{I&oIypnLC;VFQAAmzzS~Hw-OOZmJUyDd&s7^3{byw zCpd&ckX4yCicPxC)B?8p<;jG(w_7dsLNoGpa`HQpK~W|&pVj5B4c8&ptBLR@j=c9p z27Gyk5U2Wcn*V~KfWH4SXd&!l&NDj4IbcB+)bwWVqXX;hM}Wvu@OBR5KG>l*V|SbgVtU2fG9?ACwr3TYM2uolEOqhW8e#4+@k% zw-tRZWYpZ*&VE*q@)Cx z#M_sqbD;caK8#-61C(B=vn@b_U`s%?`Ha*oC=qLGPkBVER(}_;`Hb$N@YnK03R4wt*vMcuz1mpCpPfegM(f@_s=mT|k z@zHGk*Qn*be9+VX*U*o#^iNdw4}!b4$NfZqjslc~_U<0_O4!sf#aya_dIZo~a~500 zi|Lz5zb$y$9>Xrxmm(z=xwa@`R3}H5JRnYoSARV{;YRGfOl+;tv#X8yC$d8rK5xuE zzH|2>ppV1Tx%Av4|0@<_yu|ZRcGHMr<1bH59<`p0zcjEioO{LklIBZz>)z>^{@S?n znu@@D*PrlkEkZ0j;m_`F#6LKDe&2$TSyq}=y$)wxiOzM?;#A)YT;d7Pf4h=dzT@V< zMn7YJy0DcqtTaE{Rx|Ot#xC<)=UN)~^!SIl@u2AJr!~2ep@I*(55=laRuSL$AtTF& zH8k%CzTnq=G`jf=LAA$j+&hhbE0L&=du~6%Afs92*xN$V`9@?$o zA)&iUuNb0j5)~2_Af8(m0#L=o<^d{h$p9^jcmk{F)=3FruA@9P#bH6Pq1zi6&scG) z>Xn*GQK@b|x}svIF)=l(axaZT{)c{%)BMfg!2b0}#HLsiUM@GDW3+yCJoX-5@VgLA zedoK3{rHAsvW(pk4X$%L_6)dM9FG}hXtAiU|N*}7LrT&S z4>-&Rg*Yb?_m(TV7*BV}c}(O8Fq58K+eIC~{l`6E_8dTJl${lgp2qd$y>3vDk%{y5 z^~GH51+Y}(3@j?oQa@dPL@0Jg;e7e=U`Ux@!Y3I0u+zY|wT^rMY~v`eLJS71<%mfM zA*;;%1B)@YCZSxSX0HG9%8_g0z;%Hc>ntYe(a6Yw{R;1a{U5mlt_yJ`K1m6SZR`vb z$xrxNoqhisgiwv*98$u9mxlOr?znDwb<4=Jc7~QL-?9DaYO}2~VY#ZZHW*ZHQc_`^ z<*tXTk0Jz%6U2h=|4_Y>o{}OmWat{h$jducbzcsF$e5R@p}a#zHl9i8NS_Z^C*yZ2 zoTm#wubvqX9Mwh4Xc|b@KC%Jvw0s(W89h1Wft+~msUJZ5%XOU5NHW{$%I>rxoyhJc zI_m(XE+)z??xD;_bC>X?+#J=f5S=QR9_>0XN%86UdTAV!C&xM#e+^sU?>ZON%Z!-I zOli3q6+`?Bbn+=b4qK3l-1rWhC}jaw-*3T65%aqXcVEtJmha5zTdCgqbs{*C<*hk2 z+!-ipJhC&qxIgZHf1+q?00*Knxe80c zht%1L&SIf?7U6XGvL!R$tU}Z{HD<=)$-=X(Vd==T9U_k7<41^z?-Oc|@9~N@JSkOP zZ0}Nfj)>Uf;yvGQb`&=pVrOY^txn5!P?3zyprglAdjJ5|S!12Pj{RM;E5sNXJ;CkY z;Iu0`y!4^1%hp!}RLZY(VLn)Xmz^t+-f~(dezfFrTZFtw`Bs$Mx8{-4RC)W=!&o(k zne4;kpy=F;yKtIF42gABFUaR9&>XBtAK10m&h*L$qXk*#E)!NGnFC7Zm?hXrw-`J#X58*cKE{pG@dY(|y) zj@z)3qGEb{eBGE5!&~;zV>32R&f&-JCMy(sN&sAyd}xVWf$rd~PlB2~3R+CdTBXI? z9g-?%^+mlkr!RkbYnfv^oa~A)&7&g9sF~#$dsHdM=CxCy zn@ysI(z2?phCpo+!kS%!WF~&jpT>KZ!-sncXJ5u__NzEFW;g!g|cpdsy>67Du zM#~8FrGOoTY2);0eL3En!ix9p99wKJ0IT5pBZcv=;uX~pPFBe0jLghIy(1T0E35p! zTU#7JWmGb$cM8b?cw*r@p8SW%n2N^_{|hSGoRO8HrF@l#dKC>f<>yUn2eh1^j4HR%XV&r!_t5P+u7kz_%BdK;{T$X25Ep+M6 zSyi4^=YY#{CPJkuqlG7!pvB~mJOg=qkr|!RiHm*4to~H2Tlytwxw(>124kzB-?n$p zhcx0DHx?2DmH@7eCAGT!ge@5dVxZ^Z>ut1lA`N#UQlN&oRWkLj^3uGt?tp5kmo6Bx% z)zTkCDMpqNWUeRjz#6Ru=C&X2?uA&Mo;kIbtWG97S-Wf%xP2KNt!(-7*^pt&$0P*! z*>x7Xe{!Y@_nn80fJZ_#J3AXmCR7cGX-B0CAP;~}jV!6OU1*f$c{B?tANrimt;s8k6%AtIE$(Hh;I+D?RvQjcd22V5%5G7{052XllCS=Z9bh zdZiy>NyPU5sb9RxTDd7y&qlu%vP(s?iHdr%1d`DTgazKa_xE&_TJegRnP0t?_iS{g&w%gv90HP1Q?*(GYu2> zk`e`NBi28e#gJ=mc_Ek%tvDZINk9DKE3?LS7DQN*HdA*rxZk+m={l^%EEU15(ok*t zq3;!86{SbE#re#!*Sp#=^P23blQsLbcqFLsaQ%l%-t~F6ss9zB3CYEE9&aX8IqxB+ zs$J&RNo`NQS(hGEr4-$l16}aL`u!Rkd6@`;tQG~G(o>AT5 zUxz%VK-?RIGqCc5zOo%A^*blau@7ALcSPKW&CLt3+6g&ogysyz6IEX3R&)~F^NSRy zs<Y@J&LeC-W{M*|U+yueOib^}8&sOdl&c3ssd^ z{7U1yQn4YI9DDMDj;_@?>Yw^2Q z2T$ADWS%5mhIYK_TAphc`BT9?)eg$PCT7N)_|s1G5mwv15f*=fVp3VQ#4zX#QhfZO z(kF2dh=UUB5DTQxU-!}xOJgRrLQ$=Gy7O0vnzS1)q#8^f8F=D#sNmcOla;Kh1+WFn5YdEwOcY3nT7 z*Z}TD0EAl(fbGVq>7G0p;;i2E<02&y`j?T$@Q}e#?YQ+lJ1&TXE8iS{F5qzF{mWhJ ziB+{3Y6j1Nez*!erPE}Hw*Q#Iwh*6>V_;z027exke=w-2Rb6V;Fpxik7#^2 z0{HIpp*HJs672IqZM_>u5`ICz(B9dmdXGV5tg{Gi?vYu~*1c$E!DZI!4gI|gQG9Od z=jEQy(GTqIE;?N_tl1B!uMPM5x4L+x0rKfq6v(h3Nko|-uRxf!bpc>=v03PecMWjb zUg+id^^yABkbH%G?xaRp-yI8aR)(?wW`^%H5mUDLP-sJp@6nt>Lyj=oFm6)~S}z6I zV#X}n-lup}2a@-s(Y7s)2%Pbud-FqRJ(u-89Pp@$BJ}ff1z*P?GVPsH`yM+bU9K0^ zB!bN0ed_5O&w2Ld)QT1uZeiQRWg|j&-4@vpxmzdg379xST+Dx^&lLK;rr-OTgml5XO>425@@4%6ge5;?EkwsQxFVPwAHlW?1w z^BrZ85m9V&heUp%X872MhJZ}MR_(BtLY%l#_)&1aDtN`mBB!xn&kG(!N({td$j`it zBo7igy@`RY98BUj3SlgcRIK+2t{nz-*LsI<1d%+~K^uTRtWMKao;Przzi+4s2yFV7 z&FOmA8@9^VHM72jDmnPnt<0kvJxy!t2RA5P8a3`jmDBqX@Y^iH;kXKNL;r5;Ka^fl&@s+!iP zZtLi!14?#1v26 z?-peAauo!7x*SapDiyU9*9R9Z1r;4$MO(}F_j(LY)22@^;Og-^6Pns-(6%Z z$46rOarP$H*L+MSDtI;qnLMl1(m#KrJ<8GYtkR@E|NMQDQUDHCXuAvtlD7y@9p&F0 zyr=lzC!(jPS5Q*A1$PIPI>etJL2EqO-4C@K|Gq3jmKjKm>qT2#Gh3Xc^P$umhK-#C zXfHwn`P7^YQs&Nv-5g^?b^w)US_}mb1c(KbL=NzMGEVpcNu_WAEDJX<=A<=XhiI$2 z;9(?_L~_AlkoS8O4u{9HYepPy53;{!n~)^ErWRS~+LM^5;#E#mcR&&$*baWZ-Qo9~{6CACHVjoJ%cc>fJ-` zC6q4PpprZ)f1D-2p`0x!L%h~`D1?hFUkJXt<(-0Kj}iecpDR-rB^;ForUc2K(q zdv|S$Y59mP#&q(wp^-BD+r06Bz%`i9XnMRZ^lXjD*~PGMdY*GLir46bu~;%yreJ zVuJ)J`{?FjI##^c6EAduwMVibxDkMc7wY~N38^~DDe)^DP|zo$T>C%KdYD&J41Y34 z;L`LcD()En`o&D%dnFPWbb9mzkXv+zAqj4tX>*<5C3Rtar+}5O*MuT-8fV4?UCecvp=^7GRe3`~2VVAb`}dvE0>FA8=DVWXSlwE8 zKpfz^TG!)@JjaVTr<(z83sYyOB^C3ZZ!-4vB}?d9+_1TgbP`mW*Z`nVK~|O}uA#NP zUA{^M(^$>8MSK&k+tAk^!u{r_d$9!Czk1%6F??Ry$;ddF(C^O=|InY}7%RF=2oR;C zZy!Q^X)q*GvQKnb0vg>;_yba4&G=0}(F-2;m~cO4k)?T$Hp8`Xm)MJoGK;EouOvxs zH7vH#>3&sK;+;Mbk8w>BIX+#)H21!Ub~IFWI9W_LCvAr{)-WHdm&$;noY%Ey-bT)> zethl0yH+YxVYg}(FpZ=WXLmW=CZ)KdfeL{>>SHdqw*!8+3j&^cTTN>kkxw1RZkyGM z-lp+9Ux@alfymhRSBhgleDUWI|6Vz0sHBWe4N8T}60DdHrtbYkcP~A=nui-$9rh(9u`ifZaXd8s)?i>lF~%Mnmu|Ag4l4tc8it^(uNGp0XU|Hh>uhh=)HmA7;{T zHaQy+I6_$T8@pYUh`@=C@K_QHco*r$K9YN=nU%e8-dh_?lTEalupMrC)v}1}TmrLM z(gFub0AX>KVkA{yhtB1&Ki0oN7OHW07~P}=;NsTS){W&q3*Bl6?JfcU%@@AWUZ~G~ ze&a_ZpUMHtc8e1p)d#NcH_j9BIN-V7^;uvcR|md73WVO==@#gHKfR=knRJ|ltq~R3 zd)9U+BQ&S4ME9FvYM~)S{TvKy^RWy-!arKCnc{E!A}yd2d(dI{%dylKY1lqGd3^hJ zGvw=6qnZQO;v$!M-l^WkPuCNxr)*F|vH)!x5PPSBSn4Cu+_XPAf|Y^kD$IwA{|ZXE zx2eE&5-!y%wEu>UgR;9%1)V+&5}M0MqBxJ8WK^rKM?8ZzPT&*LpcH(%mlpy8qq012 z6tu~S`&^UDZc*6Vx*Bkzh~L19TkWC>g$o!SVORG7-AV#rG-#TbYGg9M2@*)6HDCCUXDyHkflCBJObd{NvoR6f{Q-FTd)qYpycNj5c$N{WnYR2+@N z4qObJR8#6~#XQPs0KhBixXS1!VMpOwY6e1TdU?xqb$pSJ>07D%I;<+n@%sb~tQiwc~ z;@k0p(2)q2xzE4HTF#ABgu~07dG*`A(yrSo?U|`s1!p`p|4Tvo?JI5dyVY53RP|Py z7zcQ4Ra44xx2>QSg}2wVsO~h<#T!H0ds#pRIRM{ zzD3Vl{EDO`VL&Ln_JmQZT3z(6rEsB5fGZRb^(4B>E4OZOC(Wya3~W?MG1=1-JN%56 znqPwsToovD;-J;-E07_vm;L9pU*XT-wNW_!A-ekGi|63F31U4F8ecP%O#Y8stqr{D zaJ<=qbP%O2M}YH53kL^B$YA9%67ljQj>ll8(lU}Au_Cjnsp$~VAYQ#Rk%BI5dp^!< z&Dt#~s2o=lU|{<~5?DzF`C{Ss$VG(V9G)A?Ab=8mmIOTBR9$; z4f$qB|MOJniAejfl(bxmKsX4%Lxqtlcf0TAWn^*YBiv)^c#c!G#4NhPS!=)kRw3Zm zGg_rTfj%~F7?=Vlsb(l*|8G4N!{Zq+YwQd1FkJM$A&ES`b7*a&#DdBWxuP!hUU~G< zy{mOzuSx~S_mh91pbHEL-D093Xh2m#U>*1?BL$*>yBh(BXh2cn8ooBSuSE=0p z1WiK;tj7k3>j`on6+ugMhfU#3IxRDv0E}pRB?~}OyU=)Ddc|2^9d%1aeAR<|PGUq5 zL^~6v;PTMu;P0$Bn6VKXV~4)176u-Q%;@zJDlk|;j%-!8S(2l($VOz(k8(8kA+$lY zRA6Cb4;&&;_s}1mf%sSoHeE(zC4^md*r>zZsl%l#J#y}s;xyjhm|^L+N5!rn3kKlT zn%(?+QISAkaSj1yGV@n%QeNa20a#tIv5BpEw8$4x$QK3p`drX1Q}rZ*HT)~+K%(W^ zUD<^^GhQrsb3;WAT^7gN)|duSY5D2l@&w6U+X30bsk4)*YNclAR`=?y(&d~A5ZKL_ z4LcE)Otz1p6~Pv%2m9OwW=iO7UuR81uD(!$XC|yP(Xea&l=Y8^cM-GMJ1=GWBYirx?k3Aij_<2?IZv1R} zsA$S-RY)zgF_3nd>WL!CE^kW!ovGO`J!jm|K{Co+enqAM>6bP$@|RNtOi{o5-`i?; z5OJPMUr1P38j`a;Jw5$kC?vcu#}#<-6z3Rws(t3GYcs9B{X!M4idCbQkpUv zDGtAoFVv-(-lO9TOkA){gtT`dQ-k3#jSM@+EKorahQ}!~7!05oJ`d@+un`D^4UmJf z%m_9^|LVUXxt@utOHhE6=f1-`9vplP6xu4(PKQdWlR*xTxA$%iI*|VyE6f2r>WASk zqtJUWszdPl#o66n^aNM}aso_23O5?c^{J6k$sT?gNiF~^D=Eo24t|BRTOH1=g)m+J zPrX4srl@z3Osm5+r$u*k048!dfare)oduNv_>QKPfnKEBr&>e4IJv=g4$!*+_?-XX z)}sw2@q#8;z@9)9)SVHBM2tVAZ#hTlCVt|9B&q}?2S`I{R_BpjeYnC2$!3yI!gPPEx_kq?D~dgREJR^FZEx{SN1SK zl9&JotN5n?`_U3?1B-x%4eHt!Y{RXzjEp{j$iGI>wAn(LtKa^uY5(H_;I;uOr>mPN zE-nu8^!et}Up@@%X5(Di&}M(+Mt%&Zz70|gNs++NUL6W-=UB@Nz3SwjreqcK7`Q6?Hsttgjxm-3MV?F$K=EJR}AJhSN-Qj81kjF&ThXpXcdvyc& z=gjO5w-*m<&W>ut>^-3CbmD~)@L8_c0W~0UwP83Bs;UD614&>^GEL?4GsOhd-sm7$ex$SA>NZ{)r(NktF{)%EE%a10y;Us z5xs}LdceD!QcG?>9K+h&l*jgMS>-u+#2{;qsT*W4$o7I5fcg zMQ{#%wTE($X+mVqnTs`y)IJUC{+bxD%jY*L3epK)4jXiY9tQ0D4L11~qUdac z5bSv`3(+%7oD+Fd;alwQBq)*cCL;=8O_$9@jU=|_>Ico1U~ z-sOq|n#IA%$v{WtcU6W4&J+W8y+)%`BQz?Nae$^AoR>Ns2SsaA% z1TnD9ed$F<4-!~_oVftxAoH46egmFzzvP^LFp2|oUlZ?mNvTH8*f{;j2MGKkAb?qK z8oU9Wwl<3UJcgeUC9hn-4O-dFoR8Kt)_@i4$Lfke;tNv-y7nRDOB?J$xy}%6FUKEf)%8Rq9GGZbqKcY#Z*)eAZ-E9w@(ZNhjQx&EwrqU z93GEi-^yjev#sopd>C?i0Asd}Dc&|}Kp^fG7NAleVC&}lUHz-{w|_s96G>BD3YjPY zTmPT_1egVoz&cZ|^9S5SqBK;`hxVewBBTp`{V4f4^rz$C0ZOkzOMq!JW1mi)9l7y1 zY{UV$wM-iOfip$~FGnmc>VuP6^k z>Hxmvg1ID;kP4}k#L?hu609{B&{Pq!3}8)&R4;$jni@;Nd6CA#%F!((mkHj2Si}kZ zo8TAx?aO8Z9A5J@?E)*xf|%m~j`b8^mB(CE%*=9-!RyzrUnk*0j?hBx(}2&`8DK{h zCWsTjM-#`>LM8)%_JCE=t)+a$gf@d2+0A}&D0CF1yk5xBw|L$?Ux-V^qwd;1$R~13 zCY{y`*sQ7R;?JUwC-1<|`*IT`Efc%-TnF=j zI0U=fSujYv8<#Xs<1f57C^#MtBnj1A3KghN`~Q4hwSb7$p$-u{c;IrPfTA_r7ZK>0ze*{9vw{{;2RmB=(_ z@3z5P=){BrYVVj=#unen`ykZQdR-BA9*8uD=%YI7KR>iAbr!)`=> z&E@o56fRxw=Z_?m-~C+%>eqR?%st}f={C@S@^0JiLp zd;L9VXNXC!A+^Fxil1n0M$k8(+zLIg-*^Xo4xIt)8(2%z1CpT1r80!CfR%B;RNPeg?tGo^UQpa zbJ&m}NV*s&{jIr+SA^P{_1$*+U9;TbF1MPLlR60t8hY&fuPTA*f2dquN{Y*=nIuMJ zA_i_Gb;tIfeV}v4bHCPgnzwogbMx`cK1GwGLBd<2usH2vAH!Rv4cGaLh9=c>532@8;i^OO zy`_S&f=67la{)5Jybgc(#^JvQOYKALLd_QKl^e@&{0aSG9?hMR2`ej-50%rYQ?omh zy_T{^^UXf@j}ZIohd=d~eU%qz=o}egXD)wGUR`q&pXvkil^f?f10gE+x5YuSP0EFI z8-@~{DB%@S1A^TX<0gW33rX0TjSFO$@ecsKPof=FoAe#80kN;0|}f7sYduC_uj~kRJfe!lvm}{o?wwMhG%b^ zy}LEU(HXL=04K{)gkd??1u^6M0%CJrp-WVbYHv)2;#VUJO|2iyaVq_u?_MdLlKh!i ztrKl$7O?H3H5n8ctuCd?E*fskUy1Q1v~Y!faBe50nEHJwFQVMXV2&Xkz;-9@W@{xz z)cv_D>{HDit->z)`%!>huE>qHo$Ml#(TzDT8K2QMJ>y^Xj$|>tTl{kwBPo6+Pe*dD zxP)TJEx^B`1RfbTfJps@)>f+#de4yV{=zHC3(%ecqtr(n?cfFF{4h_PwVE^65WtQG zF0e`_>>`g?>NOyj?Lm9fVIkgo8+3tHI6I(g55B)9`#6w_2{w(i10z_UR~s`b%O=DG|dJmjf}yh1DGz z=AF@LoRqgm;h70v=IJNCQQXaoOE7QP+|269x(Q#YR1I@Jy?#yq)D6IJiOe!(%gRsR z_NJbr;2j*-JvAI18&IFh8ko6FP8|5!}{FB(j)D0ns500%YplK^f+OAaTrjX7CerOXLN334+x$ zf@Y<-cPhGHja}mTMHP2Pwh!;nEGW*s6JFm81#P1 z0S`%ks@)V?T^O1?p1>l$dRO&bdWD&_is4lLS1rC5Dk|v+{>KlK!>p4{3LnL3(Akf^ z(VsVkwm!IS@9W@`RNH2JkV_YKBkk1;T7mR^V zW7#t)m-{}1GQGSle$#y^d%1;yjX~(;+G*Idk8x5_nWj_`N5-h!ZHo_kwMfr6*)E|yqKk7Rjz+DjEOZpTWimH^rEO}z0A7dT?AfNJ6u z6KQTxqIJmA$3m^e7O$L53dr`9ObqQ$x*~FJw@54G^IN{IH#-u@_!i#bpA+W14s(0~ zD^1i|y-1kE3GEU4M%duv2&bcr*nO76WyWTJ}SN+uE{&F?kIkPMvVtlU`)0*-C zSF`W)T!xW&PO99V?om8qHQkJTFJWh3JN>{tqhxGsk)C2`SQhTlEXlezkjMT|b<5;% zEgvpBR`%@3G`Ds8T`QS}_FC-r4g0gKJ@>its!@5>o6OZYF^8++#RSa}A)iNip6d*$ zOzmB(`Ag4yI-+sfoKj>Lx*IlZKUY|0K$7&N;oB(vDx=o?>ef%k&+M!%<~#STyVuw; zJ5NqKx%j%Ho=y$K&e}KYPqu5VI52TCyd#Musas*fU*<3As=UbXuJYmu{g!|!8wKc2 zY`e`SLVf8{NKE+R={zHGGB2gec2I@khm`Iig2hjBF6{T}lpcszyec^luEdc!TB~r+ z6-ZNbUVtDktK-HA6mD<;Tl@W~E|ipEc>KJME=k6}Zt!wcdhSyt+oOy8Lk}^$<6pOG zdYb+2Oh}#Vxt)~=fFiDFg32fq!_|sv!!C$bh$|LPo|*w+M z-LdYRiH+;GsnDOR6Xx>rgQE0WhQZfkOb7I&BFx^gHJntduXCF_zhOBUd$X(+J8C_w z6qlab0gqU%nBBRf)dTMw>M+KMj>ng7Flb7>^;YR^+hZP)-!!w%1@tk&yP`@OA$FDD zpHNwFnAKgI)w6p68|@Sq3^}q^;4&7L;mpi2O?h%Q?VVaxIkFX^)DvAnXZ&1yM{NjU zQpO(^oVmGWNXMIb$C&|pS2o?w;XQK<+}UA;#{G|*{^SZy8`F<%!IjcBY0XV_KQ0|l zIsRwy?wLG9VHkrbCt7eKz%i4r>%TNZv07O@;mLbCL9(`>RZ+(?;yB)#QI}=0jp$~$ zE5);@ps#wH>}Zaa;ddozDeEhzUP~HUJuEf>wMYa!waGx^rXArQjV!-94+vxZM5$dt{0?7I4IMj=1*-t zzOk_1w-`9bhMN=%UfdbB3EBL5WtPSKuimV5G?{pB8m$lp9es+&D|C9-4-B@B)8PUi zGN{ME3PN6KJ6fxvq>z*hY0aDY*u)_jy(pm#x+Ka$v)n{ytVKQkA&@*SWsP`Lp5j4D zoR*a}5a7B^Z7*F=P*C*tz)D|l-{lkY#pY+XAd{{}m5=$LRghVCkwYmwu8-%4^G^Z0 zYaO5d*gK91x)Ms(8J}U^dJ-tCtljw3ztcJCt}r#WoK^yRAz{E!#ekbMBxDd0Kcj;A z8tcHNRj}I=`|p;}a<1xfk;$w{zUbBFm^@)>?=dnvvb2Wpz$Z3m4XjJ%(8O?wHgx)M z#lMjnlVaA@?oaJ{Dm9)4D(`rqv|$ZzVEprG{BYQU?#>@#KFHbdjSms=O=b#nOiNB46W z1C4XX4c*G|d8M{p+z1?Tl4|#OO>7en^Eo@|Rp(rCt=pqs!(T>BSBT3we|*LzO}+~% zivL`Sz_e+RbxneT4pJzl!9!l5Fzd*nOD9O)qNkVbp!Z-@D_Ed$L=Z_$+pe+RbW1Di zHk*NG)fK4G>mw89^1Rp1R>SCAcWkb8H$P|f#UD`In4k#`Jt;lHvQ6*d5#y)6jUas6 zpYXm`Ub~>bq95`Ie56XstfJe*GY29u3wDBGx87*LcFkd_cs zN{~>xOQpM8q&tW1R;0VThLY|o`|To#b9uNB<+QxO7q zEO~!hn5=^|^xlfHZA6|wsh=u8S+K4sQP?6QBk%ljW86Bp%SiQP8~%GH*T$ukFrKre zv3Ta)UM&84k!zP(TjMd#KyplkBRRs7p?WGXpl@4FP`gA^z}y(REhz|nkip%$z*Dn( zkW!11#MjRtefeN}=K?sLjBxt@XoCL_zR7ymW_gsB^yqJV2`&OY{DSuo>z_QH9~wG< zT$!Ek%>e^}fi?|G!DzKo{~D*2%yz&&ZcCu=uxH?T4cDJ6)vW&PCGoe%B}^6k@Fkvb zPmijeNcP8XED9DeLJ^wRt3kx$wX9+2uhT6=rM!Q^;Fc=2_OYbwqT(`{;HA!MKtpFN zg0N(qKU>2P;i$zJy01HN!G}gW9gPtlVL@|J0&Lps1kffrU-3lK6f&p{7-a=zcRfl^JXG@79c z_8jC1jT3UIM9;a1ECh1u^20}oe>0!2`mW`zisQF>`2un;QtCeL-kPP7YRATN@@pr* za*P-3v&ijbt9OlXc!w7sC(tp=2r0p+^Re`@kgOteM0V8}?CaI$ubDM;UPg#wpC^r1%5j!BT(rs~N;yqZdIe0fyH+n- z+c;9j3)RYMK@3yn=#fmx#VY!rYj@thsCL;sH~S86-qW>3$A1Ge@c%%uACfLkp;vvw z_`|IN#~=hr2~Fbvl-GBqqWWm}eLv{`MrFm6@5<{mmyh?QBK5;Mn>nz@UL$mFj85o6 zG97^MqRkRG|61;{a8^r;4zPWz&4b#_%}op9*@%aK4S7oPc*O|qZ-^Rx%^HQAr|@ys z0?-luq$YG|6;aaTD#n+gH1ab@nte8qu&J(`D&SHhK@-&kvcSNK{lfR4X5%2pQywaF zkTdXxO{LmCY+DwWk?!=n#d$KJYns~T;X`iz$rR{pWO9~BhV9Ug=j0ef!WL5yIc-k& z&$;xXG`qqjJIknMf4{ovehpeDL(-@UT7I8^r;; zOC5=FFXMtern+dE31CEkVH|=`wN(?dM`Jr75$1Js7LLkwOVoUA*$U0i_1FrEmHlR1 zai#);PTSiozjVyZpN38p@K!sh1X-^Yojf~MWp8)kNLJ`jy|$hVCTmFLsNPTwEgg~> zSF_f%&7@O~52)p%KZ!txfCet6E~MGt_3nLs$DLl{$P*k3OHplhD|4Pw%T!X!#=uZg zt(~sFep|4o)7F&AIGIE^Q2HZ!fitV(RrTkD>l+-rnpn5FJvYO6FrO`D;iQ$V+KoL1 zacwGLrj)+!B#$e86K{7Zcfzve5~b3S0Nl+4wQfCmyvK86fV7ht|G6XXnU0x3Vf={uOysVg^?zoKaP9E^r~NE{2N>Rg z&&qHA;rLXG^alW*YPQxBzvoDo?i3?!Ur25lK6$RVL@v)}O{9nxQF*;%>a(hY&^6#ReQzicx9f&Wf zUEmZPEH^ypKB+(iSX0_He0ZO_dPFu%S~Ir5@(-&v z8Esmv<%iJh#~~Lr=L>?&gExPI3VVFsrqJT|F`PfD=BMF%J}OQ~|B6ivK=x^|<||PY zx}FA+uH!o$>+?wUyw=OI4}vU}Wgtv!8!L8UaJyNSw6bU7MD)|o5f1cI?%$=z)I2(v zC{R%zc$;9fo~IvU@SYm8n3crSI3zL3S5Gh~gPIv?3>^>RozwQrJvIlF6HmFbDsp`Z zgVOd-ed7bty7mv|4JktgA%?D{ns<58xSB{ILJSL+r*%P7mD5T$mnl^&jFP90!}74XnOFB9x@qZv@XSHz&>)l7W zjsN|wuzxa75>mq3t(tp8JXzFjPLov^0IU35YcY391U8eb9BDqbVk==$RX6KxiXd#* znX`uw_qUYQ4{AdRqrE72=HS@p2SuOz`Rj%rqFJYQdPMG8{n%^3m!%qdKEXzl!XWEf zUejZ*%6@x+Su{jW;&}CW&BWy)d}l-ZEYAG68B(G!J@|N!kDEJu&>X~|96=a`@`2|+ zY7rVY0CzbkE!__wq1~y55yS=K55H7sk?Dna1S{!KP-4>16_!+5M#!8sgyoNfJru;hnTx&T>Y`xA@WI+XFSB|rKK{*DdjVz^$?HPo)xQ~ zK*z;O^w~WtPx0k<^HJJsAk%a?UmsJJAAe?=_LN2-m+bGkS5hlzX@T{xx7GKUBPoUS z?k3y}i!Vh}Fl#NDg00KrDh!UB-`zbu-DvOhZX4}1TDxXPFZDID>sAuYRO)P- ze%DsWkS$aOY4mMNvxQ-LigC+bJ5s2ehLzMpod+&5oUy4soPh{dL6Cu#0WUop>^1Uv z4=XY3DOoP$?=RbsG(5y=rS!ezV~I1t27){}i~EHcU!i?$OS$BkVY7ug95Xfc{j~Cx zxRmKoV%#?luKm2Nk3*)4G>~7byzcqWF}DDP!PmrOqk9pR_du6OX#v;}s+*N8KB%f@ zlcFTU5ki(Su2QKvGp|)#TQYIp`nmjskQ**I=P;;rag{Pst0^y|U0Uxc{m4hnGiP0< z--TPhtX95l+iv!D=uJP5Wy-C{af{aYx_iiADR~d}M7AbkJ9W_9B%}3OtJQN|5g}fD ztx>uIopJS?1HQR{=eXdtp$3%BcK$qmV}Bf$l*Y1f2oh+PT3GN*cGzXvNl^#w&pPH_ zosMz%E1Ut1_?-9mbS2!)vL65g(6sdoX1Ui3P{O4MiHOeXFnazunnj}Tw=z7tHKR@d z_4L(!SIQSd#cH<5D?Z-B#w@C&QM2>iySS9kUOf}d{jURu0nb0-TA^kbDfIExwfa0- z6<*g}?!n*5HH&K@uhqBBnJQ2+6ZmKC{9oq5=u3brvq>sB4<%v{V#$RI+n$kvPuff1 zv+9AQ6Lu+-9=_1-cDhph8lPBMBy)=~)Tn!OOuu5+G3FmyYnq%=zPww;Tw5zC`T7h; zmUrNs#g_o$-0VIR)>?X);&kFQ(jwV5dfXEgxt-S7Vu-GM&a%k`9K0%l%*M^8{V?S@sIqbz?}_ zD8gL=ySuxkWMouC{RjJk-pGmk!w_&uQtm*7^%gdZfB%306fBDCovNMIS!^ck>AZBF z`L*#KtF418Cicb|PZ7H6+hq+HPh3Mt8qH`eTy-fx_t*p?YAM9>ZVK8;Ltg|9aR0&^_a|TlDofm|&2Yu#os~ zc)aGv0FKbu4Fw-!_M7sLe78hXnvBB~6NpUab|Rxi*<8UBTSPfzi9pb7x?0x5l6208 zq+&cfL~;*TvPuV}U9*7ikM`D=AmfZ2<*@)DZPEfGI|Ez5^)y%PNegS!WMUM}tV>m8 zRl~JAif?a3@Jg}_Jx+n{NLeaXEV@&?8;UOW9z{qR)Y}YBJI>L|MQ>2jNg5KPV=<-l zl%3mvk(sBjP(XN1Sxqy=X~feQv9SpwtA#I=tR|%LtBcE?;?Q@`im8GbbPHhvL782B z`Z=?27RV+S>glvIAwiY8neHAJMpyncJp(6XiMO77NhNdatC`tm^CdWXd+YeiiYput zvldOS*KCU;5jHac21-TeHK4NGYv0xgAl>+pl?_Tvxx*k3GXJU$VJ~;!om;I!vHciHY67;vb`R>b0d!5`*DSed{vg?uav}KXD z_&1PZ;MXCbUDFDCFuX6R&~jX8J5b-OI@mKR+PpMe0RJxh$|1CbQ(qA6>s|l94Ec?8s)altps*MMiu3MV6Y(Wtn5$0nPqTwL$OB00kVd%iCAtqW^NJd~ z9!gJ~*zG4*X7clWqRB4k;b36dMUy;L5HTyHSg*d0LT784f10%o&Qpjb8c-?dWY8+( zpgpK7cy*A#X6e@z-o4Y*2jkPpJxZK#RbQ^e4(DQYFuoioR6^Q{YBZ$~9KtkhR=Z8B z>&uz$88lbz=gVJLe!js}?dpdf@+YEgTFBRk`Bd{+?`uz^9(L7oHb>pH*{H!`o$yrk z(lqkh_%=~As$|X{a@c<^kSFB)IVEW!og5mZ);(LMO~6=B7(^CrSDkkvz#~oDiXAx0YIkfn)IRT zZ}-Yq_#r^82nuaI5J$A<_O9MC_GFYhW4WC;DN+yWAB5VpEg9K{G2>e`aKikb$cY58 zK8iBhcJC-HI7v`%cPX^HSa6#DKzS~(=|zNzO#S*3DjnPUMt_pJJ9N0xu_K^jYllWx zBq^m$uXv{IvO&MN-G_AB^-)`)zx$qV(!XnxBe&?%c9}l!eQE8|R^GTyy0*6VL6>q1_wG*<-a+RF81-7! z1uK|snb$j}pb<&BU-ML(Z~wk4gN_9(i|ior$w7C-I@Se!SP#44Q*SA_PS@I^Em9%C zReAR!M3<~Das7e%#qT$1;FiPOaiv2)XEhikXdhRj5(YDXRfW9k7|jq{%nCt@sWP!x zzd8hV_nk~@^<(gp%^%>5C%w$Y;IJ8SrQuRDvosek>TP1xLC%Y=CN{J3v!(h4UQksw zKzTq`s(YTG$*t>#j~SmlUqnIs##ocLZ0vKniuKkE%pfy%fcImHuZ)cz_^d*2w31YD z8l74s!8Pdwm%76zHx?XcF#8I z{lh?RfFRTBKBH}z#Wwi7MqPtm-8ercAZ}N>IbHQJzf*XbEOWJ%<29s}x51^};vCc> z=RaTExuUvo-R^8YE-)dxFOOq-YT<~vUyIYa0}Gf2h~~Q2rlmp07P!N5XZlXIY6C?1j73NlAgJvD843` z#RqkJ_>BFu+NdgDUys0-G?%Koy(vw6@oWS3_-u39hh-979K;m90{hcYE^yuBXI)cY zi}N0-+n9GLy~M=&k3sZv_o+t14Sx0V6%(gt7YSd(kz&R-coKE;zqqZ_+LTXd)0~6h zD=!B_<;INRxAd2kR3$f^`G`XfpHqTRFf;>-uR8YxmRtd;5@Un%q;c*uhOZ0CN@9BI zZ!xoOC@2$9rsT&avZ&TQTUipYJc-Qwu+cg}DU2d+tQ%taHe_#Y*(t)o@mx*xD~IVbDM>C6z{r7Q z<9j$pT*@79X$)O_8g;Ig8vH2l-Vv947xnv}(9h0mRIDd-u6JfFNl~s}FOC%I2rEF} zeluV;w^eSgwTQwC*WPet>zFE+uV~8PE^}TJv^<4whv^>_t@M04RxB!({u;lYNOn@Q z-oTqY*dwq$|2DIN!scD(99>F-?q~1_+qKZjsWsnuQV?nE{sEmtzrf}hdqq&7GrrE1 zi@4oehl*SU{SE9j1c7i&Q-{%W`vqulq?%85B5LfNy>Fp*TA+RkbFo!wv|So7RlAm6(=_1!(66QLATB-~ zM7){C;-h56SgJX^~>wqF`g`(&DmDzhwH$|t`UhJNTL zbU&xVY3^kBK&+ELjY>=ZDgAAsrP^%Xq&kRzZ|{iGd_b}~uSqLEvQP08ziVwtolnmB zrv9K5(`i-`Gfv3fzsjm@M?govEZz26Sw4<`M|u8|dKQ^F^FUFYF}z}-gwk^h+cs90 zk9hMk)i&XFpTBwFoLtyp=1tn#^J4=xD6pZ~VcsNO;8MEG)7?v5RnTK84L;D+cm4Uv zoPZlZkrF!Aoh~{!5=x18l{?uAvRtXwI`j!@0uTy+k-i2fd_T~H3fOu8Skz5BwbFyN-tg8V;V`#_mpW|Aba!r#qpD~ zOSnA_`pehyOI5umUzFukZW=t#;iun${iidkz#5u>462-Zr?Pnvmb)8f60q-)FpKh{ z6vZgL%=<>~`pH0uR!tDNs{#x*SgWlTUd|o~8LL|TS4CUqW z=QcNG_gE>Q83goY0-uR-5#X z05dA5g5#YZ{`usl|Ru)pk>|e z@;t-_5+Kd&O!Gn+HLxMoPq&&3U*8R@KPVD0hMPY|qi%CEjpr*dQWmVAY0uP_07Yon;TZS2mes)POZ@Vz@>)W#HuN_zVqzk#$10QEL=~YJz zoI5f8S(#|tz6|ra?Qw5_Zb4j}GQB^kb#0*ux;0hn4$A9g#S270YXt#yzK3mG{4_?QYQ&JE zxUV~>DRvtAYTOJ>SIFru!_R14$5fkd=iAS3!9t#ABfgg{Tot?5fZn)63_8+uJyiu| z_rpJRNl2NkrPCFje{|-Wos$>HlkwND*X@tJt0yX2L{D6-y;R#YJGK- zyhGKM?q_S}(cd%puIkyFRchM*KATQyE?n>39cR=V%^0vTl}pY268n*_9K6C_swqKWH%c3DBJHuf8 zlflLxP+Tv5;xkSlOSQBWEJ~3rQ8m{7^IJLnl;jPTDa+{mY!PR*gYpK7%AT?uWG={w zcN

%RN(jK3=^r3g*dh$bg;&x5Cc0wZ=WL{|G&*#;MB*AGI88liNS$q+sw+p5g5b zp$MsF-^#n_kvSy7m_77i5?{T`BKgw*s>T+FjbF`#)o?I}F*2uI#i_5T2)64yewu%{ z1I5q;@xoD~{E?uEe~6pFV;tST*kS=yS+Fxu=bFjI{Y`)Fc%;B60fCTXx5Pc@9}1L0 z&yx}bvb1gZI*E)n>GdyqM?tn<#1yh;woSIKe1!5f>u}{=e(93kf-U9iLj?Q-vztot zj&)8cna3k5w$RS{8c#nk@!4vjsVBdr#X9`c7a{zEXD}Y#@Jx5>(PTzHSTeQX%IX%p z(;kK|>kE(cLuS5}wi>LzKGL}^{aJ@0)$l-~uCB>>*K3b=Tgc=bPgIaniAUe%h(&MA zyfDbjN>m2!qmd_0xo0tn@r38m!p;Z(otcfM+)!m*-D_+T!zlN%&TVRMntIj=&Gf>e7@`nJR2^J+DW%Mi`ieI8U}6d_G0%p6Os_;!8! z?wXC#Dk+oj*7S-=Uukme6N~5DTyWcJA~;A};O1PozQVeCd1gmC;i8p}H*(xum8nrC z$|?mthNnN;C8C*E-Sy&C(`r@hH4M(XU%f#bxLpw@Kj-Bsic}nFr}N(&3Ni{H;L)_6 zVmS*n9Y`w8m_%@@jIYVt1^CXcGuE_mbKC>$=PIWa39Rf`(9JG`UnfG|ZeSExIw)obKRLcQ81Sxz0rYgi@7k*SafrZYo){NG z&s0m3p+tzjLZ4`7N0u528@1zXt2F6G3S^aegxD4200*^EP zc5K}x2T}7vg)%Gb70Ez{bec=_JQloV$0@=*U#IQ5cP)GMF$_)R9QKeWmyH=+3T!bg z&6^8Z?G=8FtjXn<5bag-T+X2(-E;Iw9i%^xH(%kyKKJdi9P#g7e=gx@w_7?kJIEAb zQDb|oH9nhFnz7|cSv#?>`peQUyV%c*!uTI`+kgvXe#^&VQ8-A_K_={= z_mtt!rSv-cu^xZ-dFxa!tTV!ernF<4ftmTBF;N@b+jr@>=oy*zRy-Hekz#=brky&j zQ2Rk!dUqc#;Pmph`Tr%YV}Ca+U{jdj_pc?u$_i9>O)lwUz6u+j+41C`UHE`z7ARZ zMdLHJyTCWBCXd#fp%7ee=DOaL%UjsggxGCEgx6!r@tqMyy%dn!_Zq%-ot<-TGEM7R zvFj5uTJv1sDlu7cJ~sUU?B}stvrt=!0J&eJnpzqci@ll`GkF8=$FRiq$=Y9h%vie& zuGl=aso)|!HF&N=$zN`H3zxEGNrP?*SkG)V*eXHP%QeqbQ)Rvcl{|yymJHq5odPy`0^PC&EHEEDTNilFTSTE_Xdl(&hOj}VAyAFHT+9P zkRM(>RI9kT-I>GGPz6DRZt8U&ic-0J**t@tiH7pHzP1GR%0TcPC@A^}kNb3}4^MQMNFgr&VT)W~U+ifR2wUI@Q#EYg*=|ZK?k$ zK<8|&HB)jz58ez(oOBV1Jy@@P4Co}N4pzMk!o*%s@6=?lq+Zf%UzxYW8eW~a&hu3^rjh!$PPxj`*FGB2 zzJ5pw^w}qgIDxRDfW{`Gr%KC`#k5WrpWlMDKKGVecH6U_>{(&LC5EKeH8oXqgp0h_X!vIPWN$2m@n`){u7%WZ5-o2hcwNww#`R9E zZLhYDf9ui+56q5H=r+%WvQD2f8b5iA$cgIR$S}=G7DI{d^pz zC;Bvac^-A}RilqTgv}9hC4BRqTw%~1LvJcIRk6hoQ>p|nn%|rL^Q^Z>ESufuK3723 zx`yv1o{|B3<2QB1z>bQwdX(-8v#`*wqNl@8{;aS#j*IYXPKi(Yy!o|0A5b}NKf+_1 zU)585eL;WYPARqKL0kZR7ww#CN#Dz(UW=LOj}g(%|GWV;is8t9)yRc4bcM#n%RDufm#_A{)<;;kQ!?td%?P$nReBPAnqSESFTO5oZuukJAsY3;63F zzUpc&A8owt*&)4b^y*PA8p=oDQXxYO@fJtDnBJ}Y{v12rHTryO#}9pf2iowoT%XdY zL-nY!VS{_9m=8}@V3iE3$5A_CH}=t@7{~W z*(qebKz-QJ7(jg($_h8?5<#n zI`5Bd!5?f3J5Qm*7}azyH~KxHpFW~LHEL3!ts;-_p~p{;S0oQ#`w<&-+Vt=_a7l^2NY~86;XzP#J2+Y*EBIT9D_WN>+M+m-zgwJa_b*rS{Jw~-_O@}bG4Bq z{dH3(;|xU$>Xa0DoTYsY0!Tk06-b;WJWbhVN~k#gPJC_<$XWAn_vZIztoqVgymEH0 z&hM>}Q^x~@V_naiBk!Ke5M4xcYy^go!?*QWOL?!xKJDb#UiWy-ILIZv-fg}&OZ+ht zM9vrs-E*xiHAFY8S~2MzhIz09`BXN)kYFivlr8r}^Ni*wSf&yc(W0^uW1*r=N&cn3 z$B`;inx%Im#ztt+GqBM`PTgHuzaW2x_4=*Xk3toO!xo#0pN6)Zrz`Y*{0&3`E3KRg zEOEQ~k!}3^n7?2q85vyl7Fs_-7j`@14q-jI2-B*=%$bbN4+EcwK6yGnc*hoEVanK^ z?YWvK7l>Gq&N!1G7M6mtM6?9I_oBs4VAHNd4A41Y7kmqnfV+{xi8i#LmbApztEp!Z z$aw{ghs=LHFmT)_C;;-VQDYYEegx7Oayrdpx0q#DV?xpmravdhz0Ulinyc=G7g*3K7c-F? z#%Y}{r4Oers+t^*Jo^!H>gsNi7uKQP{KB-hj^*5IV%NL}?+t=gjB>qX>dTHuBrghU zL4GQ=Zyb1cjLEK`dGk>A(Yte|z~QXhG>|1jd^0CjO6Xi2=JRtAQT|kYjeu>NqoB2S zMtdAZd#*yyf0GwtD#cst`KsT%Qs3%6ujP z?V|E8UarCW*)QCyJ39l}3sZVOj2g${08>%#C~WiY9fI(y?_QVArTP_DE*PxVFm|lx z{2Gq&A^&-#r+)ykgTp&HyP+gdGkftG2wD<0e8jTI#D3H#jUH4v zu*S3rNUcssx3!02z5vt^qb5qYKc2CuxL71l<+&{-MfgCgdv-5Vea()ul z2|MZ2r%(O9e`l?+R9~P|I}Y#9xDSg2XxMj1YskWX5 zp%hNY{M=sK8?P!|x%~c|Ww0-JwKC!$!q&@;M7lVNda%QUEVz(LZp$6LO2G6QS+O~p zg_5pxHt_9vVUIr6}pnSu)Vi;Z!#!_D*o3| zOnjs9=e@UH4LC#UB$lkctHp@8$jtNpwVVf*%VdwDQqZ6?|2@zl^ir)_j2;}k_a8rg zo~|-R!@-%<-5|Svy3*(^faVdmGZcJe(@ozK{mAMNpGj`00D9=%2 zK9LIH4jVjY>FMTGA)oUf{V-pC%#WI)zUTnr0IyPBmV{iL4>Bo*&o1a=QC(bO+k4+f zq+8-oNZq&Rp=|di_$MCl8>hC=Ko@Mo+0ci&uX>01`!c!3UCOh$M%(BEi(@0V$Px$A z49IZ|&{zmV`B5>_t~DOp;jFO>`mUCetiCh9oWVkW-Li?N^j*JXBiE)}uY=kRk5*BX z1ky4)X;GF}b$(;5)cR4oTipL+t9Zwv(%gq)wfw+=S>03QJjs1Y)ueXBM!rLsN|C#i zezsKd@heJ+BuRByN%VPshW6BShgzOR(I z5dK~KzI^VZVY&AO-vWx;yxZb?L_Cad;G{((E5X~`(O)HPncBY}ialMS2{l?sM7Q0C zHVl7>;3IP!Qa~zhAGH3~(HCGjJgW7(kF!R_D)y_HZ7Sztd|!6cmWG@+^%7)j!12jW z^x<^v?>=C7J)UpS_QTn(JU3XLhi{aUBQXpzS!qjD0)Mm9z^`|%ylPa34s{zp z3PS=AlRY93ZZc_)q{CiTG0^m)I3Pt$Q3cGwWX;I*{#-F0Mgc#CT%(!B@b^Da_(A!Q zL>TNt%&#_56vIGSJb^)dy0F`VSltMc*qf+9Qkw4KaW$^PLow%mV?a5a$oj>Dz7QRm zngy8<&j0ZRn-9~s$}`O^AuVu^{W#W75}gO+%uB6Z$FTDH4h7Vjqh&e{s7!I@GIvr@#es5+u6$)P#a^s zqj#fTIp>DnUmC2nosQ8x$`~uVSV6i;B}_;H32aa@-H+>qSypX_-p-@8n$k8mlh> z17AN#DN7LffqOVoXSJTTC#soe!hElW>bN`io05*>FnyT(g z?0>g1^6pmd2W|b5P0Cohd7u|m;Vd?=jo1fDpOXhS#@Z8~;1Q#tvB3kP^8?MFZ4@F_ z6fZ!WXW(%aPc-Q%n{)PzR*SD*56=}M*rq<&=3Z|~XTr{s8i^|S_m$e;ld1sdHRV8R z&;!(o3FSPzA+$s(;ig7T0+pdB+ZW-NTD$Gp_x|-*Q2ImTcRKC>oP?KFdwh7>`Cpp- zS}Ebzdn>MN@vVLOYj^`AXTbiva(vfBB*7cy?MA+g$r?I~rUU(T*Ts7D~0kC1Ye z%;}IhvUm&aj10?tkBU_N9#Qmz{vp&q3aVdwLf`*1NAYB(?0i(LQK#+UDCV)chTz>Y zt*E2Kf%dO0XTv{cenu)DHfr(v?_i1jCiQ(N0Gv(4%IZzR8_i{hc-zaM`f5>~(j4t8v&_m?8@5={b#izp5=+~)r!e<>n& z*HPGr`2K<`;C(_W&Ym0iF>5IODb&uKPDr6m(;HV0J^#;<`+HH&P90Isgig1lTVcZv z48dRknui2R^4~<=C?=1-A9Os*moNJOBb$j5RHaN4o|ZGhaU=MZ5+tz9nhZmgJzuLJ*o z$#f|Spwj8OTRS<%^-4VA=b-l)+sNO8Kmt^V?$iPQbW+}AMa-3`v-D*MG|R5Hi~K!F zDp$bkVPv$fO5I)8hhe}qYOjjbdHl~O`l5sr0_Rfs+3LO?M6(ADlGGcCw$+8i@DW`D zS54f?Z+*RAl*Ak0$#1W7hfdEFS;L?EXLJU?ge*&!LPkmJ-kMm8u45%;`Od1gcq3P3 zR?k;Bf4=ODyFux??d6WUHpZS#i?V1t)?!~@gsC)EY8sE-J!i{~>L+oMtI&?7p&pS<7c z*Jjz*_DPpxqMR62X!td(+3D4&F2V?1pMT(qP01U42c@yJpQ$-1z0n zD)GTV7~3cI%?E;KNO(#K5~w*?UZ^jh6>5RMF}d3%T!!Y3Mhmh%IHYroKJMiQBv-v{ z_-x`HtW*PwPq|b)de$@q#n+Rm^go_dJUXYRXyZO2#XFUL-1lLs*(}dfw+2L~??NXK z|HVLAJo}9cQ5A9T@w5yx0-bA@;(>&$`phw>l4v=VaCud~P2OSZn1ybSKdI}`N6ATYXm#?r<; z;I&P}_NG=!Uhy4{>5r~L!Cia>!2z8x^$eHdN7RK(rV&e*O2t%$o0x_yIPJ{lqhMs} zdxmd9SRNZ5B@y6G~fJ9#8ZYy?HGzQ~%z@v9V(93|7CI3zd5l2Y66pX@W_ag)6Ue>HpF;Ek?23L#!T4{5Lz9FPge!`CtsDKvtC`|8OQfZ(9H-931t?52c6PQT=HV_=>n*N{v|A)p@ z*pBGU`ks74fIVFs#@9l&!ts80pWyyJv@2YRJA1sH{m?i z$>U9|T$;!>FDp4MsiF+v(&yO>ly1fN_CM^a{wB1Z?t-8}CwTd&$V%^q-{9ak9L^Mm z4+A{xb}AsORAnfyzonkGM@r#jR~@F!U=L=J#~b`d@-HMP4;OS2N9k( z-wyC-<8asD5xDs)L_bDhpuxlu=WNgLl;Izf^ro3j6=V(G#HNF?LO?v+a&C$J_dIa7 zQRw8-gq@x1dh8~3U6ugJS1b@-dFJ1DuY~U;Qd|M${jp?fIy%FyZ_m708Rh1`RAK6(#Zu<{t-f9ZF=T2Z ziP{3$!=a+9}y^H>BzF-cXVS%lx!~()YBhurKIc!qgcG z_Gcc(if}!>CR0CG(n!3`Rk6^ z(bRA!rxGNL1knpfay14o=TMJQSA3Xr6N z!htO=b1Mi>NoB(>hO5h~;?Tn-N6Fh$B^!twvHYmb4i6BU7`x8?^M+HXoWRloA0YMI zo|&#T%~h|^JCnYW_@B!%JF7#^@2ZgADTX5u$hY58ExXa#{x2$if5|o*Mt6Ng+$I zBbg*C5UHo)?YbbJAfOaAXa}9m1QyqvF%3)3J*6amV7vxOut;K*%e@k{9Lxbh!&WL^dk#awhzo{Y}-zCW@Sz+Ku6+3q)qqqxQMNEG^77V+Kh;d_WH zWr18u9(Ee?i(QgEk!M-lE(w9=6PBsP(AfF;;ON{-cPuqc2I_-@DGe*;sP7{)mv3#H za`wD+6(n4KU6}kNBRlvZAL6c2QfhxI{-?LIdPpCUk+r40P!S?UYoq!pZN-iEU26mS zD4P;TqRt$@%5f!uf#X7S69ea7dH$53IjbN6<@uyaUR(MXzs{d(&BkZJ>kb6AH~3Yi z72h+5J*@^>&&Jew&g95^K@q0Cf+s5c7h8DL0CB&%n0fYV{k}I!(AoMnbV9SxQ9^%W zWhpt@F3c90x-i^+J@f6c+roo@0iK>{=%EwSsw_hnlNYkN3$|ajwm)=GWDQ$d_4U{K zRfAfn>96UHyn4=0Q@UdCTbR>2$Yuy;!bK$=$xO`(%$;=CM%-BG z%R>X!40!LG>@c8blrGK7y3v5 zb(^e`P!a-^I4BTaYdtY(QKH~%M=Yg96-rPR=))YBT~HPn^XR4Vr3_lq2aqFbg{M5Y%lrYH*Ix{9Xa)PZ_D;MA=}>xFy$fw~wl{JlFWSE)_6T<(2dJg5!LtL;@pG#RpE5EB7O%rlaDKY31#Ox zvy3ggxae$DJOUY!^SYowgj6H`L<|>kyI_5AgL@&gMu?aJKi=us$#b7np~x=+8nvzf zXf^g_0gzp6*oR!A*gj2r`!~mWSo;ImnR_trv0rRS&TfVaECKX)Z`C3H`zCuP;34`= zpM4VmQM`Rl!56rsm5vIktrC?+9oU_sYa6ttZUTL&Wxmj&c*9{r^-`%r^r;!$mpoV% zK6h0GIopht2-~S5%HZ)H;WG%W&Ubc7FmhY{OiS__s3lcWF?5kb+ z0DAbtwU(1aWMLG4Q3fQ`6o&7>E*Ekhhxe`5fDkkkfFx32g!}(`3AlK`cL#Ddqf6I5 zj_Uh$Y1fj~Hz7vuKF2{`8I4DDQTcFTS1_+Q3qM58E4+w_$N!zF^Y8QM6>4909TW*r zJpF8y_@wQAWv9K^dkW^gY5!{3t(kMp8jD`DQPw1zM#Zk0tgFpLZsk=S9VaDQr*F;v zj?fe#syNq64JWBkUvFD&&{k7>&LvJru{b%<03(DLAblCUhrEGqD+A#Di}v^@W$!~* z&fT*MT(JhqY&*u@9=kl4R@Mk3lt3YrJ8`62V@ruRmCAOK|A?wLq zo!2U`@%8=$`d_g6+<59hJf8t`$524xWRhIB3gEp13WwZZS3y z^rEyJBVIK+l&eO(KiM;l-vmbjIe2VIqJ`%pEMe(nFCgMA?j(-a$hV{3y}kw~S7Iq< ziBI5LrqDJXWJM)=38wZ}x%y=coj4xYi#h41w2ih|PaB-@pz`85gGjGny|Y7m49s4& z6s9a1WX`1ASX`j5;z>$9;(8zsLVCU`;`}=JL^VPPpL{$IY`-2$K|iTq*BT~P&-W-> z4C`RUAg9^hW_?#741xk+^hw2{Ibza4>VM>RKLxIKr4 zmWYTVx2I~Jm&-xhPhIBhkVgOfd27q-3=!8>DnNR2F6E9)g zTVl3i*XZ%Qs&LFiW@)oXk@@D?vl!mWzM5SgFCdZXlD2xI6IH6QY_Ea#aSLk$N0&SE zVv1UoUCx35Kki(ilkP1HlV-v0X)Mdj=)+O4`Ag@;f`7tLdr_QL4lU}CDb(XxOqP^j zOwI&meKSdSdH^*#|8biRZ92!{yAML^((Cj#D7g_yG}0sAsa0WnGph>)>JWc~jiu_{ z1xpTbw~vT7-8$`c8{_SA4C}ukqZtoJ<8dw?sLgyLoKU;1JGBrz{S`jRh>T6+`&`M( z$x#&v75#eGg=aq*!Y6DxTy=g5t8YwDmyAzj>HiAasj4`5o8aXF)Y-* zt|7oZ3BNOb^w&OEYcWzFZ8z3My0qC2to(!}r&k{vr(h{4?uzQ{yu8~&Hp`>FsauJS z5o&lDPxE4uBWZbV^2WAYl?5*kO)~aKnh!zktuvg=IXMy?VZf!htejkY<{SJ$Ik7GK zw6vn4c<*|J@jO#dU2cF|Q_EKru@JgIMs%@KgA}nCp8eN21SGp?OUVi-w*R8}ZvQx4 zx;lo(wWzLJ;Oz?1M8pkSsZ&-KGQFCfc3^5&27OI^7QH&L8EBj)s`0sOib$PMZqX~K zJ$FE|O`4DXhq9~eMBEsXm#RChyiUUv64`>c7=}<|8h&1Up%#mGOP*S{U6WKMZ|PQ3 zyJ=yarNH{T|3N70bsPRK_biaLhzj6w1;cbP@gfGiK38AIk6-`Mf&Stex6 zc`E$x@{z$q^iut&RvjcM!iB|bt+m2IbWHI9`?MclHIS4QNQ2ZZUvRF@W?;T&(drAe zb2o&wT&c^Mw7JbCiDh?He;Zhl6J%SW{pltrpjgSGO%shukUt)Y1khPtBB95cCsi&O zx<48XGXXVe^R>yRuw)zdTqJGklT36^1bTYgqn)aB*AH%-T{ zAoj)(jH z%kEJ$9$-C*L)S6(8kt*eYO5$9V$5TAnc5?e=Eexempg(o*qq+iv``?|AasWj_D4F7 zt$q4)G?EC85bjvlSOWR4lropy3qc4rO8b(_gel04+l*qYWU3~2cl;3F-_$J6A^1o&$v{CKusWoD!W9-)QozUmEL86|zu4q|4mJ)Zkmk(&z0jI;fRxJNs5 z-dg>(mp{GE{~UA6Nf^P?BQSWsT5+iyAQrPHnoW7Rk;|>l*(v8#ux#nXGHDhHONY*f zNe)Eqf=sTEY&l@4gOP!hnlb6%Iy59G(E>1b2Y(G{KM*TJ&He3(PRkG_Wn42D99~w! zPosCmkVH)2fX*VXw7N_9wtOdvl}HHXJSZz>2dJCH7+FIyI(R@rHt#q_6;{qo_;gtE z7qH7tQZJzY$3u^V8206oKW5qcegdaDAR*B@22z86zn#YT04o{Nh>Zmd5Vc|$3#CJkr30SU8lFwk(61(*|^a3218ut8EHm>-9-1x!Z zeJp=loM~Wo{_@mI1sneQ~YkVt^S1#MZ%BKHkq6#n9$L^%6 z0zz9ii_B4H!Ll#}HcvO1^)>P(XcPq5FY*O|r2ER}XGVg)_r=@Km)Cr}$wwEFdtcz` z@e9QWMlNBMk^^8}t?wZef_>qrX#oq?W6ehh=_9~1rvMw(?wCfXBd-Twv_1Z^$D4bl~gm=!l;?hgLm_v4$mX4?fzO5{Hw`LlkfxCgv}9epf1+ z3&JqPM6N~D+O?VY7dlrStST@@Ueb>Cj8et!{j7>i-Z|4Q%XpC~U`?ecoVyas&|pF& zKqe<=mvR{IY?o5rFsNYhDI@jVVAABZVM&ruL)2c(XO0?2arIojVD+B*_ zjWR^DP>-C0d9PsuxHn08zmfAI8gQ<^Q-ivHUx&(X$p5o-#gwFZb0KMpn@OlA?N^{C?ik+pA*s zj+cZRs9*7?V+TFx1gsAbXY^9Kboy+VV&+V-F7WdH1n}Qz+bIWc2-G&sUXNu}>T8Nx zw6!0#6Li<^As2uZnZxvI6coXX&94?wN#WzuvC*OXuK#h>Bewc0s>k>U=yF=&YRD)4^R6V z0>V2Ho`>s^WcA__>vLkng+ z!3jI)z|L6@EcY|H_&()9@v!Tps~ zTkhKtamQq*&+YLhGO;TzkYdr%DO9OP>#m_@Qr2S3n5?Rj-5NzCUr%C@>h3DIzkJyV z?jTZ9xq5{Kv=BW-MFp-Z-5-(iF+|#wftcD%u~~rRW6tMfP}dtP8UQ22wQ9T32O49+ zfM2#S*o8t!4l9ZcP9WHxZmBr&1Id3Gb@lj+f$%>-+6AmhmmIe1efUiG6!oqn*Efe5 zUgtW%o!{*j7*mks@@E6U2xTs=?C550`i8*jBRR>;cOIyyuA!B{NS~4qqJj`|a=hTQqB%k>9GHM@Kc% z=Z?#uTO?4ZVs1cR!w0!hyCNa}E<75D}VJpybDwww9lt!(Xa)5X1Azvg}D&=D%TnKCGNNK^6jy z4Ta=50i;NvAJo=1}QMHjm5xKhBUlzRAm(C8R#2whlAhSkhkGc-Uw~8*nQ}yh@TCLje?}5stx05 z5<*e(RAX?Ssv1Pz<#H-S5Ic>km@3APzx|Umkyl+uGC3pDs%o#@6s5cJlo3Q88quj2 z=DbIXU9zISy36=-tnmn7qdpoH$njs!72sW4iYS~*J!+s4xFv#6*G+!#TV*7sZTT_$_B%E{|Mi^>Xv&Hg=N2ZI%r zr$8Sn-sjXo`p482)gf>VCp;;?werEgNuOJ-f57!H780Bdfso*QNfZvaHtar(v-zLu zx8b$kIBw@%?fb7}W{|3&4LEs}dVF)cln&4E=r=7GQO8;Zgx~`(&&?%_!4wbd(0TU((}@p0!n2&K?3rZujB;!blIQU^~zE# zkT0uF{|#4n9{u7M?_E?1BnwA?0S$pBYKxbKNGDfn0(awf9+LCj+JCT48#;CW%DRXn z>uMw}2MHtbinF~%wUftYbb7sM+ITg1=DzdhsEKJ|lWbxBU0oq%c2#S+0}fe#4ZrMl zuPo7vzZZ(cjpxUh0*xoWb|HD*q9#LEEVqZN1Ia>T1xKgz4y zH8X%BMz_cajX2%vmO9>nc|D4n#iw0~>oJP+yaGyv$)VPrq4mFmN3XZQQc>qIlmL|q z658l*-y(1sl`?_0XzeCX!I>&G@7y3x$g#ziP}DUw3+|QbJ+CV2f{TjS9QWr0Z3#QR zLgKxjU?XE4FFqYZ>zfNQU(+)uGmcvsF(_E6L`^c1AE%Eu%y%?!ei^JH z8XFIXnVmZ02x%$Mjn?wJ`;PB24MlmH1w3-pKdv{{-eE)t;-~W9G=`U7vuH&8^|kTW z1>3L%1CB_DKK%a`)dsNRG5iuV+Vj zG*qv)j0KHXUiOPlI0Tj|NeymyA-x?<{3v;C;|w+SGH^kcvCiG?jiP6o@?x@`EL3l}4pI56du-jCu}A0O zR4hMH$?7|a_p5$+@|hV-Z)r_X95lzIoQViVPFQtkIS6t-{Q&6BXEzr~FPn3iSYi*x zuVp1I8!;7FVIpaML~d{E>|hNtzU>6a%XQ~woVCPsvZkn)cVDhBn10A@v**=fVQ05J zMR9W;6k+eMB(!LHW7brWar1QKCtp}|D%ek4_Z&R!>E#4`^czO{IirnondsA3Bu?rr7n_u)k=`Y@zKARcQX1px>Vm1o%xQ^ zWMW2F3}+lD<5$3I5u{euB{ZtP1#9Lk=VZzA zzkh5C(k7O4Udl;go3KaI;YT)gA2qKpMTI%oc;;}uf6s8e$cOhNFEm!CfaP0KfxOCx zv>7+FkkziYL+#=6AS?tBmb%xe1D;C*dapj6)H4q@k-6o$kkbnn>e{z(ESUe0OGT^N z>{?HEb%oA3K`ou04IURcIHoC~%%Add?w8a-0(dqyW}wy!kK^duF@?dKiV5SCITy>b zoGFU?&!^n-=tixcPj|j3w!&7eGvzkdAiyJ?+Rka&UD6hLxN0`~fllA#rNqlkczysj zrU>@imEKX|e$5#2h5b=}H5gJC9Xrag*IAX!tD`e1qBDuhWx8%OAeqxKQ2RYJ_K(2X zGRYeLcbb!FMe%zg&Rn3nr$w%8B^+Md|^OVR^Jpn(H zn<%Tn!)UlxaJSCkKD!olXfe~*3{Wtk`a`zI=*<9-p<}rAKjKtN^uBcX(FC%HxdJ-x z{%+vA-ilW2=v&nxD2&o+ul&`xb)p~;7C<7pB{A@RNnTwP_8mmLIRx{qu6FSg^FN$L z7gCfQJr;dY)KXF4*1nAI61cg>s_E*A6;A?SJQB$JN6u8=SrCjaOa-2T23w*(7^y$Z zDZKUZ<=%_187<*t)A%6OVYy)}_{GVq+nAFl`fD>nm$nUN^=g4GW7 zJy#8!-xU&=Ym!XZ#jd}%SD5iI)+7-lZG4SR`k|K`YDlIND1{zmhXT5GWIwcd9}D?bkwZx%+eI z5V+f^FpNmZmFxTKRDvngmEDKb>rx#+@H!5W1w}tioM6e(7LnjuYLs`sLbpe81w68g z61q0H@XS2`F3osj3cx=Ki!M0TH*vOM4*d>aaJGJ_CK~mS1++_G6d~*dRxq%QIRi(6gOt8&-+R zoo&HA`+8M47OiZzpVMQ|fSde}WP^j~vzpJ_s7BJCD%#O34*lhWue)>^O_*D$Cq+;l z4BHaf*SQ?w&o_#@b1zIO{OQX~m^=NR8^-pD?!=s1V^9n<7#7E?*S<$g6k4axe{Yz| zVU{=p)C!w(?C2NhX$s!fE-zpwASvFjw|%4_zG5hV@T zzJHFmGj^!_B_JqK)>XE4Q+&+}oha{{vCY6tzyNV*>A?F{64RkX6xBGx7S6;EG%Cp2 z-xfn_YecD$&Nn47@`{aPe+m$GolYKdhK;@}LyTezd|OU^g1pNFduV5&sM<7+pRahcfKjqZFvS^j&8 z3^En7I31*>LdA=yL@6AK+S$ov*u=J|%C-6>nW!=<9PKq)38YE>pDOS@&2t1wp-^!5 zBLMPHAVj-qK8P+;5rGq6UHa=OMc^cRfcgi8@ze<2B*%e&EALNtbD2MxaCC!)j?1n^ zi!}UUKm#X%>KlLQYEyDlk3M8KtwacSm|~JLH_rNt%s6c4=g|9}R`dDBGiGJNSA&Q$ z=6weSvMkT2JhaVcswHyIW8HLeW8&3W;tq#c<$*LiQeU~pD2%l&^5s-rJSOVVaR{;H zRIf;)mx1N*h6b*^=F7M6Kws@HB9UnkFfR7DxG6T9ho{P67I+e!$P-koqp}F)3 zh5Ozq74VHGFFD|WoeHPIi;{r|U>S_Z@OYr0a^l2cM0`7+NuAxuEg0QTm z8DQD*!R_*HiA)OxmmOx-GwPr<6Vwg58CKfq1=a+=vB@na0MlB&9rfXphP!~ztvY&Q z>-rzU>I2NuWZd+Hep3s0I<)K}>SyVj77y)?y^@iyuJ)~HbQfB1EWUhH4rpI-#CYVB z>3s~IHxwi1=SN64GiXGNVyhg{+dp}&QV7on%xa;n$Cvovl_qg?us)-6q!Z7ol{}Xd zU{))x{#jpxJf$9teVfoHGOao-gM5%nbL%fkYvZfFeRLW-9BqY2_a-4NEg?b3$@rz& zgH4oY>aUsnML20ZV3LEC^CU*T+-cOvIuRyDcxv@2@B4?oXC)TK1e1Dor|V$?o|M!2 z*F=IM8d@r#+O}w= zeI&{$oX^ed;^Eb1Mj<04^Y;#Qc_;M<+QUtij{r7;OykkbBHf_5k0xox21ujnNmk~zwvKz4wGg4=aqNHBG$A?f;uqqnDxG;fJ^A)%{5s0^pDTrB zU2)sM`)hDLqjuH+wpTT@k+W2cJ9|&gFkq!Yal!zR%M zQoyiKJLo8gdvIB}Kktu9@?jU+Khu9qpf*!>&{`4Oy3MC=8ih*hO#zKRGEup8kn+VX zKBgmlZ8(E=gl)5u7q)BXJ^Cd*EzO=FFP(K9)ia1pXHY> zk5(^#fAY>L0Re@ep1wYmf-^hr2jN|+2R^6iZ8`k|Wh+a7SH(NOM;<@-e|r~VV#q*Z zKzX*amtP(%>TjQaC#-qrE^YW0#v+jxM!nV~P@TMQVxE7Q9C;`_I#h16#(014P5xxV z6RmAzs#XKU9=%9QI@xh5Gb6Jpd1T_(iC60J@JGnOWq9u-7lBYSC(lPWm{%>{q$INU z^%nB;H!z7UPI1`WUrMsD>qv?V7AC~~J|dyV&z+D*WZra-H`t>t-OhQbGHrv0 zlc20|F16P1^}uP^ZiP#n%xEyFKnZB;wsMv28c5>Kdt-&qnx`6HlXS7!UY~T~0jT;0 z*~})f7|P!5<~FqGy@1ekwL!YJww9j+bYTZ0<1)Bf^_@WVJrmdU{`TSsxK|pqL-PD1 znKzIfeKJo{OZ6bk=3prQ5;67p!T8Pq>Ac)(yUl#avw$#zf!)SzC}HTvXw8u%y7)h1Uj2u~>We4(?EJi3AyY~FZowG-!E zUj>t|8Uj4m*w@*=(YBa5{<1ww^bNyPjQm7+9_w8$xoVHm}fYWy2R?b3z zg{lR!1`B(uUcfwOc*mEBiIctziy;p2$uPIe;cDki{GE~DJi#rUa?}jYp@eO@!O8d< zmv4T=NWS18EUM)08i}`WMMkE%)EtpHo3CCDPtj_F(@x1 zLy&sdt!MTJ`;*)&5OUwI4|p4cGJ`8Z-LFipQHdrLB=q_d92U}}t{WN_+W~j`>;G-mAqo^c5GR^O&TpVt68+n-r~8;H9x3h ztl#p-r5=8UY@Tjh>|kutx;e$hn=})zoI7T#Yl(@mUiZE#Rh8@NXQLT&S{jj|C&&Iw z-74B~%z>R zJi5M`IDqS$kST};wH?}E+mZNf?Zq5oL%=IKG~SC(&(iRNc%SPIu9+Ns+Axw#|IUNM z!{s%5!gW>=7-_v%81=F91~)?)Uh14fk@I8QX}CJ_{FFb>IcrPsbN*#N5GWm9B9bd6 zAY((0NILFpMDuh6>Ah;qoj1nlN40`m0mHnv$I2g4Lf1H60k!GxMd-;QCQNKW^k}EP z{AgM8ZO2Z z>iT5+=>MqNxwC0lAl#-)d#w7k^= zXAS(6!J+3^2F%0Mitjx*L)h%m{uRnlrePkiXBE!s@vy`NXgMnepi#WON-w!u_ct%{ znK>K-i&7sG@`0m_RHb^CIJ$gukK{XXVe3)vDI2dmVdaxo92jGh6IRbhWxlAh7ch5E zH=KQJB`d8NccFgI@@1X_KWOcFev-UBsH~}4MDJOolFZ~sRbqJ-Dsea+Bxmz>?S+?I z5$PzBHB94wRGra0p1WhPF$;%#c!)BDC)z4MOm>d>OR|hIC}{T^&8F%d?8qw&iB(qk z7Jz90H{fT2Ka8ue!KCF23>$zEUl>p-FrQF*PrzM7ZQEXm*~Yh!+W28tc8aW8>6 z^Kf`1jQr=EdcUxq_-9#=KW-=y^F>wH(s~%D%FkEU;xQW}BZ}yPF~t7?8HQ{EQ?C$j z7E&s=0yoahtdK=Lig`8QgMx}(d{?1X9}c{a0A`j;sRkSF+>)=49Vee#W_f!=kImgy zoc7x2&jz7(Fe2jzDy0`3kVH(=Rgife#h=`jaoT$tzY1MqF15b%3;!nx_EF|DV%^2w-Q#uihn15Td!6uK~@q#C67) zVS$^0iFFc#tU~yqA-zeZ>j$_|D_*|;Bxc9L(dfGR`A)ct6ez+PXFk9>r&T+Wj#9?K z#BOLbY^a!WUKrfU{fQ5v26u`+wdP5B9MK7AS!nb)0v5*Ry9gck&=T=ks?+X&fR<*& zY*EY$;g!C$^O2FQxkAFa zIr}imfucKRPoO`+53&mzg~N{9P~0~pt<3~fsgnUj8vb!f*_XL4?MQCv`1|A z7@murfCra^B6p#+ODr(n1P*9AWIdd9xDO0JJOV+`62oP=?>SN-rO!V*G*bS7D6@VL z6sU9!sTdiN7ClcQUf8<3zW};WA}2BsWFB(-ewu&;fX4F$A#wU86OjIkQnr3fqKXY8 z0}iv}+djm{&(Z|+GV$#F!NU=->cA)3UvOB5QcufoyijX(s@w!<*J!VJ-#^^s+{^b=gtq^_!($LyauSW{T-WKqP1J^~wPv{0B z<)v#tp6a|M&!NpXPniMYNm$m`YClMa`77Y1E_N!=js3vcf7+QvMQDH!#lC2-tmG9s z>cI6DdaD!xVf(N%+}mnvAbaJRsU!v>$X7>SgRIv0tOoqPr8%tKFO_d*($Y_! zpp&d!IBXz5CBufk%yOIstXJEI|1Lhf-Gl1}f9&uvn6C}oG7oJjeq8_u#soO4AL;P_ z&Y%NgR7?ZNO(B;9)&m`$ii62w`={3Q;bo>TlHdK^!F?ArJylG@B|K0l4@6u#dGgU#)31@?~^-I)&_?O}$4-&>R9w zu9txZYnARMpeyreSQ4|US4e;fLe(^Ys_bU{=@0MDv;a=U_dCJ`%4jC6OYkx8DC`lT z->nIr!q=!3Bzkx{0KB<`x^i0YadS!7W9sB=8@dL^-JR4dLC=+w9RlKp%75_PRrBoNNc>eUIbUq?n;Tidtf%by1t_!;2XhAq7Mul9YT@D9avMlN8DFn1alF6$5Ukb5+ zyOtWxCf;t70h?<>H#2;i{u{u|bVF#mJyu9#XLEEU0MESTGj)4VKpp%Ud4O-#v73)Z z4W+UQID}0jJQ-vbmkG-VOuwlwc6ARl_}_@=M}Ym`axpJpVs|Agh{7_)Zi*SoS#89- z^P1-6lOa*9B$pxKizT2MfH{$8|1f4R0S+Idr!6_+!Rpik2n?R>+b-lsbsAO#lLxqh z1WK>}v6}`=qsnUFq{ZxmM^J*@(I5KUU-(SbITgMDnWeiuVTkK(X6TuQ{?dOBJ=;KF z^vibn^NT;wAy#h@Dbg5pn~_98)zor_6pP$Pf7)zHm0+NcQcvgEf>LFaXHo z`KER;Kb%%f4BY5~HQONvZFJF9H0q6|-^Nq~fc1AWuuwWIqCFDI=2kY_!zJ;xjg10@ zhV{g^TviiBMg|;{U{s~r0(({0>>F_4_aYPlzYwMy5TW}mVDyY!l4v{r8(TMKLBzo zP0D74|6Xcbxv)YO5@IETnZD6vHvwk41bDxQ^3?m1|5gjW0sif#x-1Bx1q@;X*nLa* zLJ0lbNqX=pkB&Y-X(fi*065M7?g!O<0X8LI08>(!TW7Y%5Uuj7#=8V+@#R2;PhCR; z^e6eH3rB2h?2NrU8D#P%1u#3RxXZ3!08IPmXd=K4`8wZb z@M%h4w!1xGfLJCNrng@9TtVe3h2yZ$@D?Q@Az^E#iZvaWaQp7k@$yjxl@a4nWaa7U z)}4S}K;!GvzuVmR5U6bdt8dwP*iSGFW61*~2ps+BPz?DIU}yjw9Lqxp7yu^f3E07j z4n~I_^EL1^NY1(;1i~7uLpVf4#LJBPD@rGF0OKk58!r}o4RHyf_UjPIPxS66>hgN0 zbQ}bA*y2SM!yF)Ikp|oTr)>P_SblVlq;upWDlxyDi!rLS3RF(oE#mPK5PSZ?o1}cX z^T%4GhN~LS6VSJP!NpKK$itNpwMEe@yN!pcU7&1#_|$T&sw1mE(>pj6rX=m2z$}af z-kj1il?6zg36|qO%3Uki`#9WV)`E6+nX7>9X?)T*i1a@NlejH)C=c>d7L{q7IGzQ) zj};hy_y$bBSzvOcLw@(L9vmTuUJGFS!8>ox_ht+4)As<$aBv#(exOt)K7s_jFP@EJ z;ypn)?6$8n&`Vb+`D@Cz^cepB+o$RUf2;A!g(Jy(^tqmX8^-AP&ui+mZL@!W0NuRc zVVTK{wfY^LEq;3;9OHK1etLn1lVPW`am(jOhbJ-7FL37TWMTg5;tzV)^`D=yB75yq z1T(oUl*Z#NO=YcU=2#c|0c>Nph=lZz<7N6b13I<#>1Es1e>ehIGSNjUC~UxiT^`^0 z7tp3breRaq_2%-;->FDHr^5{z-QNLSC9n6xoe4b!u{4;2Dx$zpq31ORnDi*H`~+c}*mN#(eBJ{r?Q9G8hxa zob4(e)fR|;E)oi%>5ehRL*Q1NN1hxns4_$9qrXD7P#C2w|aI1?q2FJ>Zb>^_p8Qw`V}VjHt1rnXM=}qp=s!D@YZBh zEEi2N)>5uIX+P+0kMU}x8NnUWL(jjb`<@%+9Pe`S^s_bpZ$h!{$fgUr9O2xGA;$AQ z5q3ZPsUewsU;*m(_LyZMvVL7=&Pedql=fLhqN_y8&mmx5SZQyN$ssa0SWH`+Xr{{H zA8DRokTG;YD}coy0CvRd%Jm2!I+2+N4%nD=&J`ZQy(|tuLGndm^+X^39gyvdBcduH z+qa|De2xFYd6y^@P45}>$ybP>VPQuzR;3^lV62fA&=GF#>SK8J7d$T?il=@%_)n){ zaJH4b&BK6Q6d{sb*>Tiuz5T#+Tbp6j3NUUwm;a^9GW0z2&^)hske6v=+!|P$FUghJ zEY5PHzOt4mj3a58rR6J1nMER2A&UOZ{X*I}Lj;I#?!ocRB8?I>3|qPYczSh9VD}k{ zV)p<_^){dT|Aa7M3GmaHUC(1#p%(a(#)vsdokfk`e?!7@qHAd)%z*`)YKyS-{%Rq zWNZH^43Z=aGyhu{cJv`YL3Gb__dcVs#xJtq#l^4tXfd2NES~6OED0Q3Ds*v#+~=P0 zeL>@)HEqQ%G1Z&~p0j~2Zb`0I#VeEJ94aI7HszCVK?gP;_blx+!a55RScVHd@ex9i zEvQ21===m)#G0PHi)|PZ0bdX+_u-4DVvoZFkHbb9c(FG6hORvq1h5A~c^+j{?)6TYsL-1WoL%`vz6xgQuz2Brk^j>CRU&;J$QAxsePn9-X>=;LOl#pkYk z)A{qyc2oFA@o1Dz$28H1pJtWS;$1%(-Zi1V_YwH<=cgcb>)OVi*eoGme(~wzB=sKl zGx8I*0Jn1A4!GD_yUTE7E?9;Zgk%_8A`Ap=F%pcz0S90`wJ-S3!$H7bwk4CvLZNT+ zmGVWTLw=IM^Z3XU1Pt;Q0pN>+`xNi5GVX~{3D}69{M=yv?;S#LqPKtFsOak#(;dzq zcoSY-UHK=WMGb(z(Fjf)o5&e1wgz#J+%A}$b~l0`ZN7>jM&rZdE&$3TPuWhTZ$Y~Q zM~+pf?1ZyeqJFsqhOibA9QK>Jal9rzjvjOQcbCD;`MCp93j=t0FWzh>a7ZC9w{0B$ zVo|o!i%pUs4>DHb(Ef}Nq~6&3fy@q6eknON7WR|Zlzp(1=$+mbdB@U;Nq%fVrrdoQ zLI_P3R_ZWQoqd8AYYei0znr8W^x<1zFxIQ0={z@nY@^ z3lzdJ6)Ij_L+p>n62Sc4gJ1FR-J*jZQg2YeLg9E6usaKB2;RV%V$k=eJ^%K6vB?0epd=FR{|eqD!1&Z(R!GEKQAQ#Gu+ zpEMEMh3XtNl}E7Mpe<$$+xwC7h!4VUBRIvS8TuBu(W?4tp=XbOyOr=o%=K>V6aNZk z4`emKNf;XKR;4?-t$dK(T;DBC|Hp4GIKWH11lD|&geRq? zg`<=58+AOr#y$6hh?!Ui4#0674J!L_M;aaMaNmg`%oWGHDw=_{K)Up&;vsZ#KR1oJ znBz=Q3}KOVlHC# zNTgIp)cAXBP7aDWe|Orv;V$lVW_e)7Q^Cw~Yq4{cTO@*ouhZf)cCX651G+g|DHV5@ zU-&D;ObNcekv$7>D-%*I+^9>tI@guJRQnf<_I)W~#DYpy<@x5TA6pUzV~Fr6W%Q*q zC@=?oF1htAQExK%+3G+q-XP(dlW=FFzV=y+x(=gW3v@J9RQSFv$4wN6RVr`5{@VN# zAuI?^H6b6EWfF?G&AsZ67SJ^LK9Gw_&ff8%L)NjTR@p9|j^ry6vQ_$*{$Hc8;YzvN zYAHG6)7t(O&50m3>kW!A5!*E*cX0y8y~3MNZAMJ#aDutj{E9@<1Rud{busxmJM7rF z%Br1gzPd4~ubotbf5qa@efnpHh8Kh^G-)c{<_9#uDJuPWMUyi7icoh}MwKkK{IpSu z+zXLT@oNO}AI4Gi)oO{x!Xk--rd*vqP}HREhi0kNT-7c7?)oYZ3~?0V)>fvZnzH;>oGIQ8jXJv!ebad_5R@zs)9A=y z3^G-7aUhq+%=amhBrtE-CrJ~^URQAIRHM&Vd_A^poi9C|r55}(@R{P8N!Q$$MfRmX&*Qvg5Tf7={vHkP7d-WiE6?&wVgq82A}x21v=~YUugMWR7GeaO@fW)EEuN^nW;T&;tgl{$m@?{+ zq}N0nC5Ju;D!sZ zHx}jeP-5=+-iXagp^_m?*%vICWur=fDl7T!n}E!tiBZfvcv|K==E5e3^N0ZN!~wRy zD<2S&gS?3d0YCtBI7f=uM+l(8z63W!s;jG0v9O>FiQWrIN2uQI9#Xw;Z-0~wA_MTW z!co8Bo$6OdATLkhdDyy)=xf&h;N3_dd@1xDi{?jlbL$N?8d+wMyt1Q?8cw=O8cn)N zU=gSLOUCr&HtFs+qVtE%cXI)D!I2ap*t7xEa8jLJ)S*i6M`Q@z2yKSSaj6zIC&#!G zAb%$B#-jOWicv7(;NYhkBQw}<{wBor?W}A6`+JEl+KZm zV4o@*8QN{Wmq8_MzkABekN5HL8qI>Ovq!iV`@yG0+FRb6}<2a-` zJ845%e2|$7vRnx*y=t-+^!k40tz@#i`Fl}0*?W>i{jNyE{8_3%Ol(r50Fp53DHqew z6Jg36W0S2pmj2W8a1ghDl-b6T1H1 zPrUuqc+U9y*@MH1CJxuGDfSsxM`6P_oT+=%?Q20FnY03dMo}*|zLgXou0~zi{irCG zs1yI_4@Gq;+C2Z?m^$iQowy*9$h@dVS*&dV=_b7T`eNk$XME;6WD!SdJlxJj$wh2F z!^tVOd2?HTWu7C;QX<2KL?Zu*7Jf!vo}<1je=xC^b)-Zt+ZSU_i5#=({CGY@%3|_8tSxK2Dt}qZ@^eqhcOAB4JXhfq6+@Fu+3uIu z&$!R6VL}+kT<>x$w8Fg@Md4S{uXeM3ze;G2oc$te_zP{dE82EkJ!sTFPoDIfc&4y1 zscb#bcEhgV*V=wX4-%LB1ru0{{1URw-%vXHA_ph{`e@s0C;F(f?ePE#w>vJo%|LiH zd;pFNTsnlHULu!YFjbky#m9!5{d#$hi#Z3(kH40`1owo+JsN7Rw5P5@6xK79$Vr@r zn%~(As6s4dADu!|?0i8t9KxOLB@3ItB$4{=j!u>FM61%zMe|7)xMmz}x$y$J(Hy+0 zd_xMOiH}c=;WWii z=p3hl5te}>`zWC&@#;#keRPi+*?f9`{>^0>&vWJ3@J_=nZTadOd0Z0xh!)wb+9@)9 zn_tSh?_z9Meke+g=9-3Qu{@VoAOKh6QNQc%5oG1mJvBJ+Lz8yG=#Cl&?Ji}%5b&a1;! zxq@NQLVo9tHAuhBI5VFOyJzFFkLLk(1@naD*)!f6Y-$wQkSkO!W zdJK37%h#NHZ?bBHX%NfOzB~hP+WDrFXN>RU3t(?QFuTz_>R_^1G+1wO@SdAM^|l2; z9YEtFrAJN6S996=mkZ}#l~wtBjwf?ITcS-4c72v&3(Nf3`*`cEP`T;uq( z7jmK76oNSV3mBE(O2utzne~I8)QS1QjGM(%HQvdJySMU&JLH@6;Tv1mpR+6nd`-fh z)DlDCOdn*(j5j7OO%6y*FJg@d7-7VN?TuG7z3PlCE;kGmaC9WJ@BIb8}EyX*LCeRoE-~G(VVn= zJYoDh$54jsHllK6b8^{N*P7ni^swRee6#~u4vv9=RLVd0MAt($`9V3I+ zuyHnBX2+U~i^FP`8jxSfIedAPl#+LF5@I+su>$)ICrWdG=iMiG4@LGK8ZI_OI$bz5 zgyG3{7Nse#zl*<~K;kBMt)mqFumW0o*4V&T@?2}cZFF&EUE&1a7h?TK;^jTY#nls$ znCx=a(}Y}*W7E_p0x>W{81VVCLnbOGlLk^hi3{NmK|N@{*^9o0*L+vpZU%7(>#Yn6fVDb8J+T z%c`%%$XmXHn~YJgH>ji8%-Vxf+7d@{oA}i&!FuPOxFu{XaHb8T=`YybAyJ=V)AMlBdMY`pC!J zoKVvT7N4NVOy*`a)Y(eSZ%Dxh5f{UWPc<}~KHpnqhbvh4*_!sLBp-x0RcSN48|8w4 zI@A02hfN`U&wy%gCNwvnfj8QSt_qy{_BYtkuV#7lOTa-=(9-gxi=-=;{$JKe8x#c; z8X9_`&IEC3c{!eT)ncIvL{wBXf#osapOEq)v&$MeN?(V+jL#_*D&H*<6N=y`{C(oH zflBu#M|j94ve<*)k&4Kum{8m`wq0nP_8@F`D5*|3p`ydVbzN3{G6kMb$8ClF!h0)m zM4I$6-cWk}5>SUw)%RlR=Q8nmc3zb7Cj%U5)_Hk4>b3+eG-*Biu~$Kv+_3wivc{a> zz?=-Qm+IMq<#!2~r;9_rNivghDI13UcABDunk1}ii=z)~dj2lTc4jZyTNA+@8{J3p z9qOi-A*~b}p-zbGh$)aqiX z5*jg-Pdfj8jMi0U{m5WNBO^dJSy_7rs$Lk;?Ge4oh9#1gBzBk_Ho1Z-<5?8QoU>Dt zHovGGS<;C@d5E3L0v%fjh{?smc*iBgfua_ba@R1EhDiyFf7B#i@O%V4DQRQkI@`fx z%VSmZo7ilE->*$xsua4(q9P=BPGb00+z)Ptx2E#)zrYkDP1)rGN@7d&w?Y26@!Vv$% zdrhx-!tB}KdFxV~h|15}xMxxWS3!6663v;iar0~&j25q)*6qE?7Ef~nkKZV)P?C3D z5I_ZgM~Vt-fTxi6uVVgglA_UsgD!>Y*vY|bX7~qM=mV7D2l!tF)|43@&yL%bLw6xA zCZUkgJhUjtw5Zv(Sp^gP++gv1TM@O8(YNURpmJL$Xk%|AC$AQ~^SVE(jFwiUpC9-u z74LzZu=z%SYJ-0Js{Qi(bo+GbRVgqyK0e^o`LI$N)UWZ+^~>VTekbvn{<5Pc$tNy( zhSdKa97WnTVN~IquH=UwBMfv#`y)#4wSsZ(a%g~jX3lk(st~D9Nvp(MN$Xzow4b&V z++J1*?Y6>|V1l5)vSrE_p`c+L*Kyv9>C3K@O)mgIs#_qG z;ug z%^K~F^8B!hoDxcmoo&E~JwQ4$i1+=21^`kxNEtvIf=5%wW>08F_FuFYqy!azVIp?S5M# z@x@0*Xgjm@L~RyC>*ZKh2Grf_bh&;r)Oq!v$ZfZnH7^k|Jage};#988O3w}oJcJ_5 zxQhj!^<@MrhT0Xj7+TbyL}BmLJ^p0l_`J`{?)aX@^OlISMY392FkVUS_!(5dZcDLL z3A)e3dhG45uMh>T?0g2Aoov}77FP$eWMpJbB)dbge=p8wMbP;cZ+!)h19QIP6JJwi z42$ZR9V)(a@cU6PkLtCkusXK@@2zQCodfecspKHC(oi25Cuka`$N{1frRf!kcpVty zm_XTNvMT48c6$=tAq*29euNoO#qW?7Y4$NX1<{`giD&oy=a)rmgZXBPjMpKHKaLiL zuKuLXHQ|c2Yv;5@v)?!Np9GN$IpW{zH5ZGv{#%8QfKnJ_>8e) z8Y61Rfa$=L%Uz_sg}M&N{*tnA#{2&zU``<2emCl)e#^gfAVAGJ#cS^CUPgQO zE=R2SW{ZW>bD6)v7r6AHg6(KAmk7xOe6Y1pFLO(*XFc*z`&xbCRm?XHKE=I=Q>uPE z^@AtPIMQjJXfV#!&Vnqf6yZnafx?cZBAo8T8@<2+3?@!T-0^>Tj{sU9>@SYm;~89# z-VdjpxWGP~r$7GPjBmIQP6xc>b^{8iszir?&BQQ(OG{Ui%_aerxK!Y4CZ%(SP(K8f z^C~y*iL;ME&Y4_X1Q}1}kbk$o3F3=BP!z?dn4nrBl~vNVN*fy297_N7jxZb;iMypg zm?aPqYF&t9V%reP$@PuPFQRd#nM`wE3hJ*$l!HU%fh06Ok^K{+Z-)+zYJq_fU3o&Z z(DofWzjCv9lOrmL8J>`1X#!i2IGRJ%x_Gs$0NLEKy^(PMzcfrg3quz-E0OU1Sa9yd z%A7xZu0Puh+8jq;VV<5u8U>?6gNFCA)8;MosUIWd*W#WTQO;iu&u){IP){zx`H@jO zj|={Y<6eH+l|fJRB$jn4UTR>YjXZO*`ixZt)2_E?1n0;LkQU=AZlZqXX@eX05^F0#&YfrTw;7U{mhG~9$HSGZmZS8g!P=D?wE*~kx3*fq5_JbiH9RV>bSu(HbhGrS9Db zDa2k45Ne|2>08ru10B#`4V~M}z^&l3r5KIOQ8zK^TI_s;UA>i!W;R?^EpyQ&-cS`K zx?e?1>Rd-2$?e2L-VMw5tV^iCh`R$!y#TNJdyNokPn%5(-m7lHVbx%-bjKGLs8x!k zz`JpazN38zwugsVXCusNFAu^3Bhs#AQL`n@?~NzQg`(n6Hiz>5gGsTGy*9ca`+Meo zhZ7ICvwCVXH}~Tib9`2MZL0IjlI&})59(|&h$1Nn7ZhKIUdn!)VCVdx_;q#PiAVTF zW#vc4hbEYtw%uJG!L$yBZz><*zb^8ePCmOVdQnqTajS*)2%B^yZyK)!Gn;qi=eC}$ zf{kZMflt92Xmm&&ZuC^2u2d={i9PhYx*vDAMT@KVt#h=%o+xP}aZ%1LpY%%^(5SvU z(7FW=kBu!W>vX2Ul*Z}yVK${IkH6oj{WxV@xvJ6>S3>IohmGr7>vfC2y1*WP9n6e7 z$zImH?;FZ>@3ADjuNyhVIJx(Anmy6kZ}x|!<~`RPb0r+*X*;%jK_MZH&T*@imZadNy@l7ORNTuoFxwU|hcVz) zj^45P+ffD5&Sw~(S1q4qBPIZ3V2KDfvp$tf}uK1n0?|uPU zK8OC7cYV#h&{U_-Xcp&>1|n$U$YB*tHXF3U=7B-uYTgoOs2KZB*g(hDg0y4sFRS|6 zs)0TS{qyH7-6eH-BWi<~bVXlyzdZ_I0ix&!-6x4=QZx_i}c11=B&SB6A4Diuh=>$^Uj#>Rugqk6n8I<9LpKl{V5-j z-Yj0FCOxH$8rhI&%w1^ma&Y6JRobu_OLcSm{G%+znnyI6s-Mf0;! zfc9Zh|8cnd6p;(JUR2pGx8lO{9OOjR0DFL;=vrt^+VQ5h{-}_0JY;O zY?AAjyMJE8No-;i*W%9IALn{S9o*c z?fjE?o4`GR(YxH58o7N>K)I&}2)xz8a2hJ<(ElR+ClP=a@ixBw@Lvws z76w|_Uno30BxBsZ7kTT4t=fKwcZ2%JwwD%=?n4+&jdc>mx@@@e$*wqHwiUJrztEe+z3FegPw|!soVnubv%dK1h{F zVJV)^^yLjpwrN}{5kgj=wkG5I$c6`+YT?u^0)|_r@!BKwTZ}JPmN_np6KyfOD^kcE z?g9w9s_lvTKDag)3MehB=YrS4wVh*zXEG_$x(G(^AE5~&=mdZPCJ9I4(p$U z8sq98N8azI`47^~#oWsJS6H}afH9zw6)cm*-fO_KlU?NAA zw)daO0-e$xIn{W4dxjG~v>$p=(pv>I7;(}loctjEmI8qV+-JrdXuUH980cPl}?V?KZij~SL$aQYT;LrEY6VI2W%m=2OisBV!Sp12W z+wz84G7!vvwoAfG=vNo)^TM-usv)Wt$zKOJRh_J+#+_KyDB#1Irlqonh&yz;eNw}j z9{to(>I>Z{ix{SK^BLs8An1ue$w7U`*_KVxKIVH0 zA*0!%OIiNdY~;#|&>ci?asJ9F79`x}+B(J>5uNI)3yDv2L^NsKIR11bct_-xEEhj+ zv0aYtYg?LM!mAZs0;<9x@;z+cAM9qN`%6GubrewnD|4`DY_K1i;fOT2DIUE+0C~Yg zybI4Ff4pfyNNR$Zq=|xyVS5oLznd=QF~pTsGsS2e?s>-d-Ot?5tDtR0Te=OY?bkZJ zN^QpR5=3A7+;ToeuB}sw@0;oLpYg=bFY_apoD@JRx-wH0Q$+7Ah&;F^G*tFI2-3sBJxLdJl6hTmOE#tEC z1%#1hZRWCXk5w?2;B1m-T(}l;@+KK*W)TXCwpKFrRjvET%d=dQ=Bn}m;cfW{c? zKR^QDf$?l19s!Gup3Ij=BjRpgmTRM;^(5q%KKoy-Q5PLP@KV)RPXtfzZnO+Zg_;wM zmpzCb01x@R|m)uh+<$2q-#kUIFowrX8xP3U4 z(C`M6bF*nPBR+6XDa$(`yof&_yuQ0*A~gvvf#D)9 z=^Cu@;~eA0FsdJCgR1V>lFW-%*4H5p#tWS11m7Lkq^E#ycI5OKg+;o9rqvoGku^$( zbJ(-SU5NlD89Xy>w&$*QKOC-7h@m%wO15Tn^`9cEfQhM7c)61{bcor8%nT@@{9G29 zHJzhih@kq0h7eqsmRCny5=Rnlxbs=z*_k1nc;)lPWd?tJofl|jU-ieL?~z4x$bX{d zmfK+?M>H%ToI6nlnKXj(7`Vo%Dfb)IX|?ILO84pZe*$R~KJkA&a`nSS_sW2hl2|P_+(Dgvb>QpR=)#xF#WNHS z$0P>Wq0sva1Z^$yX7DRLu8gb$RTxvs)8(pjVu(ta*gprxmF8!Qlw?Gr=PrsdaRZPP zqn_q7bF<-NCzg7CUJqARQBI7Cp&P#c#Tm3IaDWqeUGb`s`~y%lN2pmg8;Fbt#=aG| zbXcFvSKH9*>+6RV=F8OrX+8GEf9Nc6{uL4cIx=k#a@|CdvR;G&eS8|44}|qK!Ydgp zbuCi4+2m!`zy4N#bY$R5UpOXKS0iL?Y|yYVLk}4kqg|h_d}GAPd_7+ocsto_E)I?q z$jR{DAb3JPh;EDjMO+;zAUf)FfH>d3K^Z}cs)Xe%d-dM2`hs~)w zJniEl3UzRX=G$ewwMPRKap&6i^yRWC@hmb|O+J`KKHH$mEQ8D3VXkqx zZ5)Fl6jb%y5nzQvj9I6iId3ZIxCStZ9;(sE+i>nLql$Pb*$ZCeO%-J}QWRHxpe6*-i^Kt_f-wlNnY4@-kT%_&|*?oSG&S*T| zYvxAAfwCzVvyanRaJnVcHAEIYwMuz2iv=-R?IrO({QQ-h2 zMU6wsk&C}w(a&6<cMI4V*gn zR3_7RM9SEYf5eRbq7fwI1EZ2aH(;{#K)_OQmKbOcgQ67Rtb_fdJrq3e^lqT4VQQ7y z72(jU0mUSxEwAw`gSPuQym7r9`yRRVTWmc*S|QZo)R^U|GnNd1o9nDv$9i9W7vw#c z$tNhMh*=C9ld!B&*C`9GCZ$Txss$h7P!pM}jxDHReZ(-EqfrzfG_$%$e^MPhG;OHQ zNOoB+kB{y&kWPlbd>lO+buRM2jdV9iUsUE$t1U#hOQ!#mkyYQquR+PBgy%CdC}?PM z^d+`VrJfifmq)hNi(CgQ?|TBTuLQU^Soegsu_ATWiCNC1is#t}{0+ix(A+LZ zf@&aw7PQ+7%eOI9a<^ZJ4W;M%FPXAV{#YvDPzOGwL5*!6ii#!itsA)fz#8b7g8gjL z-!mM=_-OR4d7H)DgT0ADD97n~iM0BtqLMORHI711Oa$R84jw#FmLE|yj7+oWIMs0n zYxQ#=)PDI#hc9abi%KHMQI|@WVv0F5*v^vWUHT)^hQ#&lVj$!b8GP%M&ewN}LY@}! z#AILUU@Mj|n3|`nZ};7vC?JM%!l=3j7gKh*DjTZ{soi>yEI1JnF8E?X4WXJvS`4q@ zjG(HYZ&O7uh2vB6+Rb}oDu$lI)b+cGCtAQ;Xh1P+WwYw@xNd>gY$Ho1hPZ`mNHHl! zmLyQ7CW(7he{5V8Q=C15iHU*XY0C;U;yY$KC3Ul&r{D@GqPiw8fyMG^eg2$+U^Os& zB_97|K#DhXNLZ-dh$61F7OB>^8{EciTlj7~D1QjAO$WpTF6$!d?NZ(+cU9{R`~kKK zsG=LpmMAK>Iot3=4zB+%4|EVrEG`DWst9}hp~OV+5umb@{slT{&aVUrryWl8KQE6f zfR*LO-EV3Bt3R!eflRf(q@k99{_is%PXPPKb!uNhn2MqK)r&c^~u`7`31hx!a@&@Qo18HLwIM&2h1<3}VlqjG`#nV(8EpZE|acJ*}(f>3!^ zPL=e2KKMIdLu;hj=-h;`FQ`yJft2iAMilMj;kwYSFWx|cL5YA6F|^jxRZMEKw~?roW8_EX zwxt~61hwYhy=w;!*qir)?(Kj-MDfbX!bjlpRD|7LPD>NwXBV1QfhD2AzTHi42ynry zrY5u(z>v4RZgga%7nu5lQ{b`e{pa#}yvm_EwudGI5J5i58Aa^B2owQG2SjM@!~aGE zfT*KWzr@Iji3nwTRWiSau9aT)K|Q-{Y*cnF%NAq4Qz#DL=qiiTyM`cw zSg?)S{u~3_5WS^c+_teANNxA+Be8^XOdjiD#0;yVw6xa+`r2~;1F}_S*9JSq z;laG+kJ4071&v`h1nD*>TC5x+afpWPwE#V#xOb%Ik2qO(cZ#|-yM`j@Ao^Hp338A^ zUaicm6nDykIPz7|^NNtB?DgYnR=N>ZxIbyUR8nfx0*`i!S|%C=9PpQhzTouid~6`! zb*zrVT!JM`gnOSCBV2nHz~P57M6qLmPhTm8BO;xXSU?qDTKa z4yUg0wC>~gF0n#4^JX_-9u0%d&@%u|0253Zc#|u^VfaiJN(U+)XSKIuM{?g6F(_MF z(!9=^PJGyzdwZt_IV>Bz{d%_9- zfDMhJ^3xl5ehfT(FA$Q#DcZVLHS6o#rzRvhtM+QdWGEv-&(BW@%UA3a5t(yVe-4>n z;i{RtkBjU7{z_ipeR?nOgZ0161o8!VvM9TMXjb3{zXx@r+iTqs2g?~;D7_ZSMr|-( zLC|*5P8ugBVB!Kr|Ags3*y(`Z=H>FC^0|nVeP~1$VpjS4iu7tgddR0*$Q$=d1lxED zQX)#M1-_ce&>0B?YSrQD2dzYH(%`RyYW_W_th(2q73cQtKtvJjA$hV6D5w3mh|URu zHE8pRg0M3ukeodx^mjy>r7O7@4+4ZVJr7FeK+cL zg8l2PJT>kLd`#PD4)kcQ=W3V$7>ZtVen0*!QQkFsy~q;dgfk^1tCb*EU!@jrszYR@ zc>LqbO9MurpnpYMz4Fac>|jRO$s!4Y3CjV2WTl(;{YPWOxnEUqH`p$S=%9H6{w=xN z#->-=3Srlr%|B(Ymk>=?59-XlQjnuK$?ncpoHrF6GS}{F6RU19@0L{6+r9Pg+Oi!g zm!Cg3a4P3*FxV^4BIORQ1jmzBD>#Uiv6oJ=XOZi^Ser| zA^D4ekIu zBo~8>jQl#kPQYBLT2GPJ@hj<{4EYZDK_k@OLIa(VJ(x5euCA%!-xBj)3c`>vF`K{J z-7ykrRINzI)cr)sH&`p8@#ff~k{qyezWid-+HD{SLDHv94PJ=8JWRLCFCs(#U7yAa zI+gYbCF)#+7r{%mw)A~{>ve8-pr*CebaX8hDt&KLYduc>Lx}I;9Dk|V@nUFtRDjK< za87>B2WqYnx1tqYK>b>dw)ZFP5yn~xp>N`AD(wt|1GJ9K)o zBHkI5@qceKba;lRiikE+Ve5cthozE7zx*Le>hn5(1hK0q0#bXarv!+czGS`r%t=Cn{g7HjV*yRKjt%SY$C-+( z683wX8f(UTljGjDpw86-R{HmAc*O@sEeeA4MV+X{_4Re9>|6UKo?FW2R;tGH*6HNMf zmzS5`D~*;bA+LsS!%wfC2sSqNncqJx_*?ytje-~Z4R{^h9$R^p>Z6Jo@SmOT^Ksq- z69GAw?dt)U)_|Rcz#bJl^(#xoy9EF<6S_GPY@oykQt zWfkClYJ}Gi7P?|E1i8(x)+ToBp@ijnp_q|9(`}S!zpusvNv#5WFQC`n^t4^z6*jL~ ze-mnC`b~2?G5=RPmE(AJUVSow8=Z~>b{cG_W_DN9$XO|Yy8FM`AcH3b=Ng;=|(M2Q3K>?b0dC^;s zXEk8!w1U{BA49x&tHzI_S=m+IArF>?z^O&^Sfq8)_Cp}6WnUgC3jwtzisGe>i23cx z)52qtmm-OVsp&&m5XDbpkft57>{agW6-yp&t3y6qK#}f+L=c6f*24({+h#cw zym9AEf6w*Zflh^^+c1G?G*iP)B#*!ew7mOV=G=3a4-k!d)AAE1;-{B3;z7mPVtY)T1z>KNinez zP4v9-$Cg;DlfGR77Q1V_7Ly_tKa0!B2f^8Yc{3r}rNqU0@6m6@sndE~lSdpM>u7T6 zGq+lEA?Rrs^HIeo9hi*WB{{9h3ACF14kwl^9cbYCys%mBpX9Zs-dm6GLbo}t*@_a} zn_oWr{u}&pD$YP?TB`22baI-+Aw=CQpaB3`b-S3W2-FMvz3N@N+@O|E@E)<8NAcJO zP7QAPKt$V2K`!UG>4v$jY6rdZ{pji+lDU;unP$zu6?${yuE4UdF1B$cE*{F0%LvIy z@Z};=gt*t@cRd}?v10i9Q&K3Ar-1}Q#(%Z5O{$=_%(=YP42+D>=*2+IvfNnuf@=g! z$|NxwNo85Or~gM{pb>!ovSjs(qbebYz+i?1v_am}G4#?T%TUlmY@9GE0{wA`Df6b2;GPeg~3*io`XcSju&;b{)rFTRBBEcAuK&6FfPv7 zo+T~c&B0nK6M9$RUfi|UsCSTtDGuAOl>g_fcr#9+df__yI~V*ZHeXx~jCVytDFP;L(z9vWqP&^|_N6)S{HSl*E``qw zxBU2~g7T7TK7Rcdx>?Q~Cn%@e(yrh{TVGv$ZsB+I#0KcVCq6n;I z*@*v%SU~#J=6%Iwcu6jBZ;$3-fY4EqUcZ(67ZZHPr+XlElW8)gV0O?kn!`G3I0RJ8 zeXH`6p^8>2;or0(B#I|!u0&b@oiI`PhuxlkQE->oZ=fU5zuGQzCwC~B>SSYM1E<9u z5fP!(QAE{2H1P31_)B*Q;eI|u`nqv7OF}^5t9q+rxq+N;1Xee&rva}7{{sgL;Qc|= z!HG-e)iL(^^=KlXh1^d_MwFvGK085?!scPpLk~U@WI@#=9xn6fX-EjXhL9kB0x6Ul zK?riuqw6+`SsQG@95bFD>S65%^+dhfbP_UR0s5S3my9%4v!cbSf5 zNN0cd_+8G>muenWnTK^x-b`sHs4>qv+{C>8Y-{-DEre6v=G@T!<$7FbIrk?fd@p!< zWG~zCXJfKwxcaLC@0sH(UGS$b{wtF!5^fjX4Xjrm$X$}S387oY^R<*TTbHcQtlVTP zLoE|hr!e_>4@_QMOeA^gE6CFZRT)tAj;4vpgZf`6!z-=8^d#Ld0>RNdGR??8AVlfb z?rz*4rZ25t_juUDj=fJSy*nBj53Jr5E!scA617e4?PJS-a*w&LjS4V^>E?Fh@))yM z=L|p3-7yM`7M32@ENZKNbQ{i16{wE5J*#?Q%6s>!jjlwC1vbMV47^u0mh~x1qKGmn zi0P(gkOIau!W@26y=5>uAHaJObgGMS7yb>=el`Ix9`ye9=GNaQ_=tm^2ViAtsg;+d z%g^9Ifu+M0^u8(9pB+5Er zw;lS!(8Ub$Fs<5Tixc3)v@HKu$Oz#0%xk(q8yFaf>8=5$a<4Wt>K*PXcQ23w5!i>! z>!g|PQw_FqmDtT7uWz(A8UmUrsNDIlJ`uU+a&;;tRL=0k^{a^9qT6aQ|Ez>@*qjnM zG(`3!PjQsX%3n^6%-Q?4P(uA|aHk5TCqbWZ5W*=wPYxo9a_;WA26uYLwVMNqWZIff zA~Ej>bVG7;hr%0nUPlt}>gD#11L*;?3$H62i*UcVa> z^K1@jvaD-#g=geOueNF7%-=5gNjlgwqh^bjmMvAnG`sq*jl>KE*yPo931Jb{JO%4r zIKO=E0X_D&nPIALs!P0~J_`aLbNreGB#zqf>F&d3?6vBIxeBs^y{^O0vQ3>Dby8E3beek@WSHvI7?d9 zh($F?B$E!)&lI!Qi7F;TSRDf%701|mTEmBI1}x_V(lc9xutUPQH+Zu$Cp&(7uHgFSF_i(^Wno>!V^-8Y2RJ0V=UGE^h-MOu{3b%heHs z)KZWTOZs;(_|x|;S5CQSy0j(Q&+XLE#$^uOn3A0%A}ufxHutooJ`I1RLfnV~5|+CE zNWt90|HVkW`fz{OEdkzk2PHA+?6!FX1i_{d8$8@^zeVShE&BUtvG(>X(yNS!C!qtY z&3<1Fm~2ACiM(YoI32jZI*g8wpLOH6{FgyNlLL*>!ze)2FOrj`oz;oirNNgKjdgRU zsL}3Wl4z&rFApQ?#VYa4Rhq9(#2T^CU4^B>bD5NB`B)#*8YdF(P7*vUU_Tfyqze?D zZjg!}4hV+y6D##Mcy~5HdqcHg?PIfSBv6>r2HRX{HiUzN3dr>1@meP296%MfxfLa; zYC`^wsg}(Uxr`AINy_v=o!&vFJz^@FPeu5Vvj>?Ld#hE*@tehnn-M^c+p|WO*~-Lc zyBowugw86^@5JGVxj7KHEvgId#TdY}N7Lh~sDCX|pNgubg6-koxBnJptUo4<9;lzX z5K3ge=lj7~l3Mvgp|2l|ON5Y`omP?dr@m~1X{6|4voo}E|D}G)GvP} z)zaxH)%+-9)~|@iWm7TqMJ^k)1<-W`^;0PvMl`&pAuTWN^GM|TA~-9c^XQUT@;Mwu z401lI=%OfD?gJ3EnGfZ97>WbK@{|F=Plu_M?DTgd`dBi&=Pc_-8NLZ(DzUq?{5{I} z_zvq+lNxNL$153`?N~9#HAssv3X7!)6mICV)m!6u8m2jW{aEpFV4Q9nnq+O;)vkAK zzJ#zY?@dCGoc8fXr{O+|lI4oYFDjUbeKMv&2p^+lPvO}hWt*^-J~<$uTOZBz#&;TK1O;%4aJ!21M`{Lw!1)&?ziAi z15ZQ@0JZRK_LcvaEQoxTmQ(@tL-Tk*Sa>+Ie%`D2!2=}`?0++KVEn&xBe{EkMc2ad za6-I~sV-y#D^6?SfJhQS!?n~t*+h6lTIY?3N+W_2`1|?=1nFIF$rPDb*N75S z)nUV2*D=0Wjg62Z>Cm%eU$zLB*lb0oM86SXtGVUSs6(9>Xjjw0V<|K zWRr#${NrUWxv^S^0hj#ccT;cdR-RHhs3;cnc79R)Aj`ALqDHa+fjB`hRRTK(^r#DO zj9l0<-lQpcu#!HYZpr7)fa)D+?8#NZF7_3uvK0kq+%z>ph#eDJ^76V`^qRWut|*S( zDpJ4~9{QfaDW&|-22mgQOJvrMx!MU+%t=L`tFsv$RYJ329In0=v3{cv1&?w1tCLBY zE`~T_t@%v?OM*@0YGa&d5QQiSlH_NW6=3?U?i;-87B@&H7j?%J1N7d=AM??9HW(XpaQ@B33~{bu$aRRlREe)@HAi zm9d%G54}jPIa-JaOxCfe=$hKv2ZPTr|7$8;Qu_c+h)v*CfTr9_cUlcjwd!Cm>-bj> z^Xt=RVi0m}D-ech0b*3xF?nLxZ(V&12L9gX0cZXmAwtMz6mcQUhCCN3oIx&YNTPck z#7gviF~j3zE2Eya>il$3?=d}^AQUF`BZ`dqgbM>p-Z>WU4nNUoD{u-?B1e(ExDIzV zNg+f%5`%M@pEiTGnCnGL*S#A%;tWfrgPEKp6h%OZH}Ec6U=x@$(I(;0q7FLK9T)7h z_(}SiW=thCDy2-x0in9JvMy_AyXS5yBunsfdayx+YYyG6j@O#L=_>2PC-#C|{}N7$ z(DDz*{bapivXsur9$_Hmr0pj>cJ8gr;h7B3!1xCmHX|fqQB9qQ-qs$PLAc<9Y6)C{ z!n*?pEIX-&hqhLz^cBMRc7mN>&gXEZW8i8og^3H?-=jeW7ii~ z)c2WkCPh(mHa6h%%i__egHXv}$pxy~Mmb#{+H!yQFD0=j^QSPR+J?_{PdkHzJ7oS) zM5Xd#T;k0HW@9IMT{z!@f&`u%aH2@$l-(|GtR+yV10!$*P<~T24?!Ma2;3OJLtpAgZ^k$5p9T z-*7zTkCSoizft8!aF+~1lpIt-*FGLOe*(b~%FkN{ZpBCI`0U3f=_|Z^X%0ovVZ{=n z`g!Es8U+!4ATY4|xvUZRqirV7290oUj2szJ8ZL|oSE%ZSZ@*Odg6G+)@WxQo-qxQm ze=9A^`AK>vZ$#)ZEFg(<7FweIDLi-&IM+x^5p0A^>!u**AyyP^-57pH_d%L{z6XB{ zh3C;m-KY$oJB%{#t#;=W3Tee8?0Uw|XQKYC7;PcJXjdN9LmOQ!o8ON8DWJzj#gN&up)#_#d?y1qLa?WE_YxlcPJZI zb#hDj&Fuhp^{m#v$FV>PAopv?Keuo2JE=<>9=O9*gBMM2j{?YmYaX!IWAm>;2HY8N zs3Et+8{q%MbnPU>g*F#PQ!}%N_#VK@wd-8AlVIof+duCidENQA`2&{cb8F|S&0Lv^ zGEhjqsdOc>K7#U#@`|O@ei}k7ijl0;8f4ujP`Rzvw%#IW#M6N1<=nk25d= zT+0YiG1uhsdkE|zj)sM0T6tM*d!c$=naTJjrdbFR!k_av2Sj=S#iF^;#K|QIv!yu@ zVMm(&h-|;Dc9Sr@c$bl0eHg=8NElV3{dLq3xVu4;mL#=b6~bX*AC^}b0~?ucOfTw)Vd}@ux15mpPzZn{m;5h#V$t-gy^Am*cv4|T=8GBR6jXsu8lg}# za+u#PuAX)UfX3+3T#(+*NCNNtHSoHa^`O6Ds32pYBp19qx3=YiOM0168cECWZYbed z4<86|H6Cg_`jl6`5`?Uk3dxkP+3w{5%JoRi*m|O){{K2`{MOwn?@JNxj9_ERy1jE? zo4COBhoUhQsE3Rd7Z~-4g#cVL^LX`dIo|X?gLrry&TYU(BTvzthGoki<&CRLwAJin z|FKGNI3Shtr9r}(?&qS;AfT5RJf55xt_Q{MobDL@`OqMEK#Vui7YCdg#sSKGCl-ZR zCL6JgkZt#W=C(lJi^8J9o_uYWj*k~5QYM1z=5ufEj3WZuPGl%Rn%d_ET2#mhN!8O( zQCxl5>QIsm`x+T+t!6wlL%Hk)I<8+o(5Qv@Tdj9!s($(V;oW`!{0%JpG12NK!(_X~j2H!lP;He5jXhNe|)r#Cr6dQ**HH>I|FDkqEt@9CT`3I^kXHfc8cr9D_n|0rc+LRYO4pXUTPZ zfx^Soyu#{7YGmA;gE}lOZ281(-MVw^4@~9aWk=Th36f^`0Tjlwe)os;K+2Zq0GLT z(gGe>$*oTdueAPgyAm(i(`|e83{KNoTaEU(e)+F{z#RE$>Zh+BtiIlI#P1g9bR`{I zbPhzUx#Svern1CL(trJ0_`UzGtGQmbx^XF>R4VH_-~Rc)N%Qf|m7@#1hZL^_2Qc4h zZE`--z5VLCcX$4i_QpEV>YoeYEu~EcAtF|vp08j*DmL2jLnAPSFkkE{?l#VzsDKDe zvQSn{6xsE2v)@uo)FusOe=~;;K0BSs2^A5?<_um26x-Pu{gpa`s2|eQ-)i(~xV?AZ zE}EmSSU~~-l`qx;bP&Gg)X^ovt*A#*w%G`stGiP3L;-NzJ@bjEE%nTP`s_03zU=JMIl{WK|bap?&0I0%Ifs7=XeHH zV6f5FUmp@r^RNj z>;H1>xViZfa|A5Jwr;6+z|TA-3EbQ?mlgs9jZ}ZZ zfg`9H$s-j0>nAo28=4v*6(sfD#vqsL8%ALA57#M`mq7_kJ4Jq;NLff9x=o{cU@pGJ zu{g=Dj!LH|7Yxfc`A$dS0_*LGzcB}u#SbOvrdo|5j%!O8S665%*MHXGaerBl3OE8W#k;(57V$|}{%;l zACeMcfzu3-WDRyou41;^qbr2No_~x|@%w}HK1$O?&AF5#yulCl)y9BvrR#1%K!eK9j+3M~drWAyqA1U+^V1n!%1N`l#xF__MNT?u zI-0A>rJJyxy?dF$pJ9+}$LVdFCC2~5nSE}!PRfBQ$73yD!Hn!ilM%~(n(FhM7q4m8 z&AJUiCgF{UcjK0`<+%0FRKl@~AB9#rbd6Rz&|76&&(Z96iEg*D$3?X9unOwGwS13A z&tmkmtR`K0Q+;|Ai)a?NlZ~(fjyl=@{aZ@HiV<^a5`&1W=9U++JcN54kX5N}R4L3V zEG*n9G-T(^3&V~g`accogS7VOb6)cNuduEcQb0Sm{b`LH3ttD?l7*DNBjNrWOA}?} zqz1R~B#w^#ArzKzzX4r;t6FA*yaCDF18C8Z&y8W*i`@WE`GRz<0&**K29f{2)^vU} zNBww4##1FPeR&3eoalI_#t%Hc>PY?{H(1_9Bo=f-^#^o29qw2*hMPMJ!V(a&GqGEie7d9j51TJ=Y{t3PRM@#UF#?t`eZxhHm zutBgz65-@h)z1+*(?1)~W>7!akB{bbSa2pONraAWER9c%c2;+Mx8GmJ-pp3%j2U-) zE?Rh*jVD@st+E%@=v>)|RctSteTa|R?#oR{O>fSe)WW2;rEFbyIES>2^MfQN#iCds z#^2F;c%F|EUcUX3cG^HFFsVN^Qq%TavZcD?=WfU1?H^WV z6MT5o*oGTlEr~#*EXk-ltEsVPGi5@)*jvh0{o}Dz=wfu%C`C58B zs%-(n9=w1WiW633=|43IAm@aNPzkC*Hkg7^KliHg5HP%_?ZV$nq`Q&oF8kEDPc+u%&nZXNLxo}KscKLT(%#5s+vIZS81=_u5h-k>EyF_PY{IAT>u09Fe?4A#j z({d1s$IzzlRys-tiL>DCs1#`URn$m3$5R^`V>&g;S7D>p4XHBz6 zfjEsKVvzznOG%knN~Hn?=mX&EXV{;mjCNuxw5dnQLCJR-`UiE5x7% zy66@^tm;sWD`xwe+S)H+#QjvG*t969YgNyoY8IR~U)Z$ZE|bquEl-}Do2a|jJ;bEw zBFbN(IPs>{#7>km_QSaWCNRF;Sqo-kOT?xN~-N%`|r z&EEZ?eJ>@R-rtVFKb1RA>moXA2_wuwe%Li>>({M;hTl^8cugr?HZ-*gN`vp?Q~(-4 zF9V>d?K7XB{}UhD00AOFSFmt87po1VeHXN`GPm9Lh+3V~Utp&RmGeB#RO)=wgk|)h zi=wyg!xJon*V%ac)_Vn}b65s)Xt>ngJk@Mxls^gfXcm=0?C``VTct+0Pae853Gqhl9XFAvYPJ?tr_THXHPvAph z%uZA(dAp2+VjQ!~JeD=POlJbK5G|^3@_q7`=GYD}s{TpTK7#qu~6$+4(T!b5@9nPjHri=m^zKQv<$c< z-bLPw=L@mbFpK{xL&g^7!;*HYA?Wf$tb4wSgB=-ani+q?Wg9!{eBGhJ0bZ;*|7~Z} zDoW;#S0meQ@T$|1LThZi`4F|)M-Q0l+#C_NM^PV;lrc~xURGB2sp8xHwybC`(6y(! z;@RteN`(OyaLJ5v3NNUO858MnwIohM7#9F}!CSTNG#I&iJ~C3WB#{6Y9gti#mp<`Q@;ozn;7V@y5hrDIB{^BX z@)VrTSil1*fROm>SJ~lx&jBMd=PV3Q{My|%rQqR z3L_gx?brAHTB8>>ufuVi|Kv9Xz9KrlBynb@TVm?TNK0Oh#vI^KI2zLRr*^4#rO~=$ z)}D31brSJ~KIwmF%WP3g;h6OWBgV;b5^L``w_$S3*TF`q^EXM4!WJ<|PF@Ai*t`ig z$$&YuSG#Vt$E>N)G>55s8o?mfULZBDl$I^|4;U)3ydKn}_H}FC#ilUJ{)NWD8x(Tz zrcU7MR_V$Z_}rwLW8Ahk8Ntp`z>k10cBJ7b^gvhDZnkqvwE~YMZC83#r^%N04Q&yX zf#v5`4O{)_jkxt;En0KGQ&8eHzbAY&W>I};JHD9u#_d)LH1$JisD}D_2SE19?b7S@lWu5UHyDS{X+4}jTtiFidpU3C-T8v!FW(DSt4te+NM?X2 zL~JXrVSXeXki&rs3j?s;9#GgD*0%_~`%n1z6$Aon%^LH zC@x3HYMYscjQvv>JF^PMdD^WCVWS^Wa)+SY0ULQLg^PXKRr0M3<0{~E^~rb#GAZ?w zIjfSt$XPVx&pA8MmYulUEiBhkTrNlz>cvGR>yJMcXN{7u~5H}Gg zo!kkj~T;Y8VL_qR{vgP)$l~9>=(m1wtOe6~rt6C9&xr1vXF8 zzsp;I=UiRE-b^2Ha#+5mZ>s)YEMD}WM_wuc5|ykoNi$PbmK1H6HSMQV+%BqMNB=@* zV1p&+^#;rQYrPL&?Rk=R-Kn1<4jGu_Ps9M%+|>}DP2Eq)4!+kFpbdJ>z-Y`Mh3^uo zyN=*zv(QDzNUf+~l-6iZM}!h{G#*~H`*Y$dFQ>vP>pS|V>UT}jUAUSK0;_%f5X^S2%UJo`tKC;o1fZizE8pWHT zv_HxycBY2e9?dB?;l6~qXVu-DLY+7$mZ+@C%3gvu78c7QMqcCuCoOFfJ^nRNn z?HQ#ZdCXCroXPSPTEfLzA34eF#+~1Z(oVH3UEW=k{AdvmGFGHPG_M9K6kDHR7BHy` zQr=}9e-p$^ZwN`k7pw2N97BeY^W#b(M*Iu7mv!Ah)3!KB!TLVw3LuTiVKrUmW;BQb z1pME*J0cAPS_94#OI%;B#nrUFiin6fG2`RcUP2>+2!t&kLT2&?RWSN66uT%oQkM6J zq2nbPpL@>=-(NcB0g?NC&ByTjN$xiQL<+%?!giaiN9i~~%@BCQudM5(@Om1J(?(3j zM5`Ch#dd{&uH3lD;KPPbwZ$e8a;nni5F%}$DOe`n%(Y`MZCd41kdyHD?>#45Q7+C>V&-dw zuT!Wlb`o`xRT9tGA4pB9A}Pk_S5wcX`VqvrMuUA;LW6DA+DLY2 zt|FcfPJH!c4_ue;)O>3BK{B`TxCPm1hBR?`&Np>A{d=by3F_p*J4Iu-i zT3~S)P^eo0B+4|yYPQ}(UI3B~fG!ftu(ZwUNKAZA@j$HUdQQ(+(RgP7(ZyW;2z;66 z-e~QAvS=QFekirdaJ}JU{08JgmV;8Py~;kq+?aCFE9mR)v6&$x>)vmQ2{lrO4z&|IUt?Br*O{G+boky|YuemJle7b1~V>f;W>fJDU z9~8W96dKfrfrA%iT<%tMhL?V?#-RQY$0u2EI`u&IIMv6-q3o@h_ENdV0FRm|kK(WC z76%+Lr-L#StYMweKUOH@1Zs>kao=|;$jXtM_+34!M+cYu&|X-yiB$3?%u*xOjA`^* zvw{>NqX==Q+IBxISD*v%wceLBSp6u3*o~OQ=Lll!x~*SW^SxXNOZdPzqt@~-Jy~`K zwYlwcA|wEs+VjFqfXcinaASABGZqi>1R^)UfIzkb{I4OV?W6;c@^Fm6`yZ+5ot%hsWr^Z~&$TZZX+=CxDBi(3n3lj9#2dTTvgH8YvjNhC>_CX(q_FOFoD(m;@xZ z)kJmSE$Uh}V%pM+tK~@e{TqngjRM;janlp2r^>a=hpH!uxLh5Z?FXsvk<_yldUqaT zFLXg8f3gz4olpBKN}mUYhQETiQthln2fG3+evQ^^%{b!vIT_AaPFf2(EQIu==B0iw zSo+Y}r4V55)q`1t76BGS7;qvU(+}nKD}=9UJl5JDNl~3vxcOtINYdskoA(zK)fT2$ zwQF8g@~^aj%5}`HRNo!@U-V^OOQ`RfHH}E((0>rYCURpyn7<*mGUc?< z+l*}*I?~?a;da`??5zzh^f@yF%;)2IKIi_|GZC?&gL?wZz7SX1G!gp(S8tAnsN%?J zXo@$)51z#BRJ-kAlaCVApUERpf!%dmT^I@q^>1#*&YOn*K=Q@Q-0mNygAOfKs4`c7 zzQ5%zu<+oM@H#!}3UreC8+*$QkWl(FrR zDHE&%{Wdsp%0M9PBVpcr(YHFtz0SqkRv_TiofD zLf;VoWDF!LFIaae7mSL_F|b4Dw<8u?-wCe^)vMFzKH+QdjwRe1;xSp(qxr zGkdH`!^5?qA8-|<>B2PPG@gxo$bP@BR*pD()}eHeYu(x9*Zx&ft*K=~Tdp3iB{@%P zZ}x|TNRvat9amwgf>$dPS3^pCqMPzfZeP=2^6@;Wl6sC2eEjJxbi-FD^=_pX2nv5! z4H4E_;~o(Aix(}#(zVIZn!U1;p{S0YXz2gNh!Ln%IU9%(k%wF7W`w`>$JF%xal?-~ z)AN{@Ck^8c8;dPX%4?2~R_=ZQ2vhESNct1{-!#+#ON*fpbi43l?$A98Xg8jK3H}V; zo164}!($H29J$lseS@sv+UJ6ct%fFuQdxFm5GL&MOkaD(`gg?vU)CmbRiS9VB~jCI zG_J!pqqWwPmjdRbI&nq#{0ncTX5L2yzaO4F|PDL zcW$+aqvm(dwETHbdKEe~Vro%Q7^5mcKzMe#wqHnBFxskBGV&vl?;JQ>Fu~#TGR&w; z59>tVt`GBV7px{7aF#kFXBKLN%tKZv0odH4z#Xy_N^tI}{N*tn9XoTPE|!4l zge_HPH&?QS2C55&4>@l?HWuw6)*oMf+fa?vZ?Tf}=a~iWe?wFM`oMOc`WHAr+RHyI za{IN7d^fPkxw)r_MF_dy#ZRp*%0E%E0Dlyp+vCAaa&>8G=}eex4paD0bh4kDo@D8e)5X{Y%A zCQN%~vCo%dijpALZ60}$I!Mws~Ae(Rt_9UVD1Uv3W&+1ofq3;rWph{S=Rq43z)*v2<*H(e&q z*h9cQf7|Az%}<#Nk60uH%rYJQqI4h+EM=mMsaSzeu!-pl*8RC)McXWUe&R53tT2kk zOxSqVk3>H59@e#lq7yf-P*13NRALU~~u_$@mT zFEMpm`h99DCf_9IGNu9I%utFD(2TPgESX`cgwj=>@Qa;3`|*~3plyn-uvRLz^sR}^ z0VXw_uAwV5HPBP`dH?tWwte*y3trKiNkz8hChw0pF>m8o=(+s(N&OjMHuQN-w)C0Q zDjL}oeZD&ZN{TX7m<|=J1Fh4;*;rzUn^tihVeYi!$&m{6(MCaVHKX;5vOJ2;bf!yI zito#!XU~l%VHJ%>T&#F`?%ohaj;@UljB2Nm%a^B9E2e}5rDjTslX#*nppiqjy3t5! zH#rhGZEa;73U5Y;t{&(T%>QXst7OIz^%i!*Wf144(V@|-qHV5vVD)_qA2f z1)nmcTBO?zHh*ywlYCo!-4@{H+~`UnGz|RQ{ucb>6a|0+nN+p3%&S9P)`y;#A`%b~ zT&|$|Z=d43|INMwIshy@CtWNE5CnCE#>oES+hDr-pz7#B^wn^pIx<{EYCPV{Kwt-- zJyLQq!~}7V^WfcsL0DrjaRW*_Z5B_u1OH}pYX=ddf)s_&$k!hDJGTsIE9DVfu7YN_xg(b;jH=bReSYh4Id71- zhWYpX(H2OPoc4-JX*5!8ky0|+D644^=*+stG+Siwq^7jXIgQ52M5usC8&|>+RDf=V z>zwN%qeCYZn<-W-yWPdy?YtV;xArU`L*l;qro)Hb(7l1-Y0viXDs6ew{IuqyxX!D> z)~FinZSKJ32@EOUN?uCu3z^&=UW$!#K@4KdNz45P3SfV%4bF;@QT1d6&BygR`ReX% zaeIx8)w;iZyZ3j!^J>md2&`v_(!TCj)HQYyoG-|K>QpXFjWGUt)fChx3FO)OAmN(0 zrjAl#A)aS<>Y+`p5wj!~P{VsAAtWczf#DRU9PAY8URIwsIs_f_74}{HQ9HQ^D9Q%X z9_E6XcJ2N!!N|E0FXjah0f5jF`1#d8U^-#~knGk z*%J(FP13W(T`qdPPBdRUHd&t#-}tSAE|MPG!h&LfQJHcyF4>OWTf*EsQ2nMEC6B@l zM!r{2`3WjoO-{tEkYaJx_j$D%wbxUxnY)EE^l-?{jIIXwbzb;WWi=&G&ZIZd&!qpp z`DkFFTjZsDOh|yIn#%jEW&C&Qh`2SosUk^M4W7w4`3MMRaTx8SxK&7ygqiNr$ znE`|L5>u}pfube{;n-~JC=PQq+TNJ{A(b2(l$FK1c{HHrb4~=xjI-eMqZxa&F12{w zI~Mw+#Ne#a(|BDhZ(~QtDVV2TKGL zXo~l1a>eJX+At0r1SL&Gw1l>+Bvffjx`2|nKCIt^RL*bc7sH#^32zQPtq+~&&eNx# z(~jwf?I;6>$Y+{a2TAWa`ufjoJ#%sv-*r;^ zt+0;5fHn}GqKffCtF~JE9gK!S`}TpE5U{&)L=kv!w|=+!(Q=cNcXqnFaf^yu+h<>r z%2?Z1mt3y%m|`OqHTvP$rN~_AXd}P&5O5kM&<)T1Gw+9MJmQ5jH71MW_!=bG;v&jz z!TGTJ%ne0k!uyt`+0dSQ0BQWlC%la*Lw{ZQE?)il5uRMz#>Nb=<6h>vI1#FNc0$Rp zA<^y^nk&;y9hj1nY^dZrFFq+ly930`Nj75ZKl-Nvq1010PRx&7F3_4BI01Sgw#l-H zho2kKxN=9+_$s2pc~GsFBCo}2}UMLehIdc`-#?6VE-s-wkvwJa>?fJWq zS-A&P?O&pU3+BG6kqPpN)f_oJUu6eOzOW8*Q6JSG#>uIJL_?Q`NQB2I#MV4IUG)vP zKF_pIXZ4>8DGv4b|EOOOP}Z${ktJ>7N3>O1q!<7kCzz-q9wKG5!DtJ+SKrXi1ipQ3 zdei?QH;IXCbHzB*`9ma!dLVs$X|iMm!{CmPCz@WHIGt$NwSLU#S8+BD^;F=wrumD6 zhBRX_dn#qohDltE27}?;PD%hoYDTedqawJ~W-oBBqWd-q0tpum@6?G?1*D&EWT68wHK;a=$ox zbT`Ab8sbId<(I31#>Qr5-qCx^HY0rg2{1GfmI*3u>|YQ4#EWO!J~dxNPmw~9`Lc||C&qlpi#P9nQr^dt7FHfBltRgkV4S!+BGTSu zIlw+&wswP<`-7^!AM%z0+uTwn5jNsm098Pq=lpI@iIg!UkxKvbRqcsX3NQm4GHl0# z0!~1e91A73TD49fnGrRemP; z{Rilnii3MYe^YDki=xBr6z4vgV_!Tcv+^+VFpz|ax~(Re2l~Gq;2}G7_LzXxGR=d1^8;=`2)`DBL`Uyv2%`sZ@fgV95qG~o zTQZZ@ZT8{I>$hAK2h!yNF%?g$*RS@Yge`J?Rr7SO7pX-FGcz{?YqQxzhdi+_n0-dC z#18=8(EfUPTtz5!$=~mbrD)1UV$X@P`cEyOpXn|5PkWfHL+$6!|`Gf)Hq=f$COJf16M?14M%O=H~AW;qBFG%1}R4Bty0Lou;mz@w_|;y zf^bX?Hlhy1UEOy19iH5^8{)3Duf_xaxhpN;u7isnIB-G~@#jExoDpwqJ0CvpjHjk) zE9D@?U9h@NhGAo*pcUA=BUxveM(5ST!yp#;6_|qZ2I?PZaMjt}csEf;t9P&R+GW49 zja)>Z$E!r1tBTN!JZ8r96A_@8liVz?tHW4K87G5#-{GnBBcldlcjrvNkWPVJcjM|d z{VMCafXKhtBv*po$tlj`MWb}=L|O3|c}@XEOssip49o#DkQ z&=fYX0LSf$eWB6P>Jmk;Nfi!O3$N$=J0g9d$`5>y6r!@VO{}UDZw>*ezdhVp`hQzm z&fQPo9{?ow49jHDpr_?w9@rF9ziId2ffHh3-xwpo=klv<8A)jH)=W8&Q9IzaS^E8( z{sMB&Kn-9i?-=}zL|_vf;|SYozke6HQ8vG+dp)KAvSw#V4$9NcKrm<|OeO}nv4Z+G^bw2e zzon`F*p4J_(iBWw!wG9%S%g~>0fQ1}`rNAf{JxkNLJhwa+lhs#t(n26N=cQ`XJpY- zd|x80_ny}Jk59f>H{$fntRlyhz$qfSY`6x`-FbK5w)0xdYteg1tBu9M%S(?di78{3 z!=HwX2PY5yUU#4Cf*0Sg3Od{<}9J zM4zW2q0VTtn|3&UPjleCDt}kaYGeUb{-rd4)K`XCW=&U|dCK20$OxQBdmNrxX;HM#M^t5-2 zfSqNSD8upt=mi*Dmikbx*z4XIOcaK$&D(Jf9G2E(6S_eTJ0UyZTo6Jss!>cl8;?Y;NWg5|4ycznuV82G&^k@Fe<2F7-hXKn}ug3#}{3%cx|a-)5+=r{^r zWyq;$o9Mu^yBqKOO}n3g(A2#79v^ZMKa(HFe%oe{j#br1IRq`_RiKaWIq%L!hIkh%kRwFgPx2N3p{^7e@u+*5!QX^cLF7{aHXPe zk&GU}NQS#1@%K-#w~?U-?LVULeuL@y`M@T1x;uljv?2_=kQeqV@ikDea22?eQB`6& z*z@c!dVU%J&BVN%7@VIXyG0_s9UHM;M44>{ju64m;x0?Yes2p-2+Cl}je+?dj^RGX zzU0=H;sjd6Myzt)M9Mx?NZposI7=6`zCuv-S~Qk%#Nl=V1AV~Z;-k0FzjUxRLYYIA z!y&oDmO+Lpa7@heFO3H3lhC36eE`~CLX{gD2xUACv3^^Ru(~9E`XB_!(k%>rFv1DwFURnAb>F-5%zt>6fF2ck)K|cR{xmbQh}=9d z;Fm9(GB4Wiy@dSBJ~xfw013D^Yz3?Tdpa4k<~>y@RrhV6ADDYQ(IYAZT2rgi;lwMc zBgq^3vAdHV=+??D{hri@k8e%R*D?^E@iAlVx1I3`k$U~FC#525w z)X5+zPx0_jMvyLg3n|X&%6W|z`{=7;fm%RP^;DYiH=^D#0L!K#*1vNPmr%eDvh#;{ z=$AbMe<^210=Q+vi<}0fjsYY0e={KYOqIdKC%>)|(Ta>eI;!5oI0Vk3_;BcNesc-& zS_bW>pCo+C!4zBl7w1-i@JM6?G$BQ-YxSg`G*bRrFdb9)i#V=I8kV_iK8CW>nqNvgC!Fo zqadvQ7<_IMe2Imdsmze>tXI*8T&)|}O^V|F+;qX9(M{ED-6uEXcjdG;` z4M^NU4Uf|%_x*R;V+%OLV##X66Obd*1MYv!|HObiWB@CUf2!9+c4_LakM{3-k2PpJ zcAo5?m4=Fm_t%VhVreKEics=5pXiT9t2-{WBqY)7uy)QjSy* zN_>PB=`JgcQ5Js5M4x_0o}8Z6m3j4A%8G)z<*UbiEo}TQ$I@NJ3(vwA}(@!0}1 z@SzS8xwM4q-x-DR1lg-)V|SS!asdo?;O_*1$d{jw}%4g2=iiT>Zv8-X){A~(n5 zw~qsZ0$_fX>k;CykD>wh$Z<5Q#NM_E9F#@t<4ptC5XQvYS8feT3+iX=|1IehlZCZf z*ROQV#p_=??2HRpp%R$Ht^sT5+M`uDPpw`StU!ak zm&j+ujca!;>e`_A+Pb1b)kr_8MmcwYH;t8oo?Z?aT=(fBF`|n{Oe9Wfhw(CVj)Ug@b*>4`C&CBXcJI1|%TA`~YMB&QH4kt!Dug O@K0V^S*lvXIOu;lz)rsa diff --git a/docs/images/documentation-2.png b/docs/images/documentation-2.png deleted file mode 100644 index fb87ad77e83dfd9e2d2ccb898b72c5e6d6ad823e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 542540 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%0DQrSidIYe`&ewh=0#w?YI{1K*@JgM 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

UlLChH^r$=hke|7O1rtM!^_UJE_=_Cmu$9J}m zvN0Oy9O477b95pX9K$l)2d-p~<*2}rP8Zp2`7)>P)y|x^nX?G$zXsMEO9}q!8@5cN z)N!8XW16Sw06uBj5BZ~)IL!dY)BHol>I1qnIO)l4pr4|2_Ak`)M~><_!$JL*9Q=}o zkrZW7fL(xay!#4F`nO&0_8njTaI2b;fQY?@j%iu^mgMhH*j4nnT z|IA4Tk(lT%U3Lrse*oRMiwT#ukvkaXR25&9jv<#CVoCKS+n>Q+N4F_N)HkWqFJbrX zjt}%Cck4C7G&@PavOeQ|6pdciq?0fnp3J6TCZw~!(PC$K?q^}D>@QyK$96~$ECa*8 z=A77Pf)RXO7wBw7D|Z{)1`#(_R?8bhu97|kqs`0c|6&Cv9VD{7^qlN3nPsB|dg2_i z+g6>IfdryiC)qICy&Ca3-Y5UQg6_--j^zJd!S{UdZwv+wHj>m7Mjr_CN9a){?+`gR z;!`8U5O5CYRz3Bl5JP&lSfK9x(fju>k-p^(&%MXy8u%)F-R0pKzBxibTLD8`@vsb zNCO9}A3J^o0|1mLeYU4`Sm=QuG+(1KMQfDjA?^b;sI)FwPL+e1%= z)mkFD7y7~N*GE6)OOJ~iMagrS8R_(=G5kqgCXw9_>Ek{Jt()K6K4zak9U?ytOQCQ^ z!gDFXn&0^j{CIGT7*(mn*LVrtx(IJ(*&RiUW-VjGrnZ6bf&GKV=81 zXfPo|7|?`C3=t_(?~DiCGo>Fa-$hG-fPcONf1f=A%NJ`F2Zu$#o8wxvUH5pbe*duV zd%@T@5*y}iJJDV?@cm7HQf7_Eg&KbPeMG0SmiQbnR3~0;#AAjHsgs@K`F4xrZRxO~ zpMPyUv%g@j<|QF#!MUeLXg1?(C%iBeFL8%yQRTj+2fOf}>C$e&L{_F=5n#3I(m?B) z+st|N9KO9m<+T|%C*8b>GZG&g>cMN;UMesY{HANK^Y8bDqV$sL=*deM@*)hHO$M*UFL$=-252M zMNLg{yVJ>_q>6n6eg_m5*J0M$tt6IQ?bCgIa#5*<{SQ{;h4S9(N#C%SScYPt?~ zBaF3vvNF6L%+Ezg8!dV0^0UL$3fX9>!^O*{^DT017>Fblm3reVXg%yJBPxY^rl&Z& zPA?Hz4for?LS&J03D`WhhXnZh9HTh6wf~F*br|I9ek$SOE~y9R<_A_N8+j{*#USA?01ZfpgD}GI49~oH!}dpr5MPkn{7aqn({KpM zss3`WDl?bR^ZHVUXhemd-C%uz{s^fCZP&Q4NpsRYy!wI%b00Kv%q(ZhjvL}eZ8Ny? zIDP^IOkg~uS5P?dv|fssIqZ;t(ibENmrLOJ?uFSq%LI~CkrIdbh#w)yfY406G-nx! zobXXNs>R}7cY%Pc$U-B0c1Ub%g@1Fozp6*~C8m=&wO`eIJ)Sz7v+r)=7;Ubv)FrB> z=FZnkXX4bu0ek&%k=_Z*r)0Mm;eUfpCM_vm*8Nrlv?9yD9?ihHcja)W3w-1_oFp^; z;Bb}7G4y&dWtDum`m!(L8MTLD=mUoOIhT#qvbU<*@9}-F`A}1BtSxcQuaZLW2cGsg zP3xxYCmRp3RG`_8Y##>NjiCG}FaN_g@b^!oz}5O1q*u|cm3!fMK$@4wIlbF54J$lP zUbsD2`POngy}|%DDlxT4@S4}D8@2^-q9+>U?z_XatEFdh1Dg=UhE$6gTcjN6)5h6vYR}#qpDdAvP*b3yC^jmi^^dgLq9VyY(ii21tvshyLVHHcdRag`NMir>K@>Js+<;cN zfl*hmT3hph_q(+U;6@&JdG<^iT$wFF7e$I%jdfj(wTJtzKIm0=1GUCV{CPGqh}`XUXr4fe;Yo1b9z{)XGNCxAoCNi8c;9G9mN|R z<97MB6CdC>1TxJs4GaRg?;(R#lV`5L2F0T=GLN5Ud9ZCZZCK02%D$`UZ~3yim%lkN z3D9uwSiL{HMjzb0;BAsx$V*7OFww!6XFCy?m`8?@7!Rg2_^Lgeu?aVph%9xI5FY37 zq!w`~MbvRjklFbMY0qVDTtJgm9boF`gp%+dG9f?Uj%$=L-fe8D^q>b>4c3lF+ldE$ z1o3gYOh2M2MKCH+CD{YmwxUT@hYIdru{}h=%jU2!ijM&_;y8I-0Mzt*Z5eEC*07j5@H=uWiwGWcTLpd!9S}g%Ch0fJ^W8uM+o9i8n7|AmJ_^;P{Jo zIxYf)wAH}^7rI@yHuwkql;C_XRV|wvyzXgtO%yrNWMRqxetwl7yFI>bp1R8d8b`U5 z7mISF*MOeqk`>X`6!CNGhNh&Onb4)2$b#~CccGB1*v33us{LSgfPb-7k9nI7$OsA+ znl;+&CN9(plhlfBIc1r&a4^UO`pKXIF*>N=3#+{l=1Ugm5Sfb{RKrkfdowe>{VKrj*1=HS-|` zOX~K**YdB4xn*kExv79 zlK~vuC>^TC;vC-#z&XJ@R|K$HEq)=yH5-&b&qCf54WNn#i~ts9LZ+M;gPt~~>s9b? z%s2F}j-~n_Y9DTRI|kP`q}s-(V3ZmGV;ZPRiJoi#I4_#@U;Ag)=W5~3LZQ<#B6)zM z{a|2Le-R(aZ$5%q(djlAptEgr%9UMkg^Tx02h`v+MLr%Dhfy-XxWDcl%R-O)*SP}- zTfq^p87~gO(I`23aIthcoI}OGGVrD!(L2F5%{S|yYJa#nx#L!Do)X4CYbTfJi63iD zeu=2)$-<>>wOor^)>GXg2-o5J)|Y;k4H3Kw_+?D-Wfa2>H6WGun~4W^kQ38-73f7a z^8gJdfBvGZ1PdIvnj(Xq&tci{`NECaD$WYm%^}zFH*%sU;bPI+OXBY|?@I>5`f(XI z#tpZm_IwU$t;4Jas$gw0KVA3pL;KZ{*#0_kn(8kSV3T@%f5TW2wmA}34&sa;Yh8>C zg*xVy5<{?}=1<M4Z=~LcBs4jqj#ea171H zlIxZoFRoFU2N{=ciQDocvE@gAJ7%4^{Hw!}8pl1FFnkaUw#8u7(aIeI6*H&HAhp&K zCqf{dxr=TI;F&8E*|cEnkhcdBt8~YSSO?Mdfb&X3KoOln6sFk98?o;$u@i@>1eI;+ z(1XLpOGfDq)r6$O!m)J*6{$8@f6j8Qmt@rMNgN7wG&@8k$n5}=B;aO)?Kkk@0Xd+V ztBAtU-J>|*Gam}UrkOspvhbA*q7`jPEHRkA;!dz#cIL;0+EUQQ#buqJtRhoV zIIek9o+91K=kG{d4;2>J9IxGElNFlCy4w&Vvv?Pu(jVgo-e&|$C*Pk5 z+xK7ruP_0pcH2JLCUkfnUNl93-9$SXX!c5vlcG-cHZSkPzCA?d!>_u}Q-zv+3FI+R zA%~L<<700FkZ|2Dea_}KU&iUeB`Z8rN?zNKv7djo<0D#RIc#es>|VyN*Jq_OjKtqC z$W-Dg0OikjUv^pz34AUK(Vu@KSt0(|fcT1TdH1zEAVU>^Wa7;c6%hGV~; znFd?BY%H}H?b9MQK)Urv{V+HOo-|^jn{Y=0f4Mgmg;u_;7a%cp3_&nPE=g5&0h({x z+rbUZGjGDy&jdZp0xO<>GPl1kAu+|ns#b(gR&XSWfA$;w%Y2Eq7z6VZLN#{=b!=$n z24Uinx5vc8c7}fy++Y^oS8?RC z)d%81i(Y(t4{AQ(V}g>bia&5jlM~SU)AlTZ`K$c&=+$8gK#fd&W>rag zj_4IfVTkCwilL#h`oYyU5XtGfQf1k06szwZHBUAA41+Ckpn+ZE40o&u@&Im=>dqe^}k>YeZDZ(QUTf{ z1kU^WDz^UhgouN+^>`~+{)@cVV-GOKT^znUH@gM!0GI3L`tpdJ>#16iCCw!5Lc!I= z35uZ*J8eX$91(CLoFbTRzSNTJj(pGq0uIUplkp*#uWtirAoz|vWa${ix5ENYz?^2e zo=A$}>qX`?@VnIVtc`>h8kH;&d-%%}!c5)zIEBd5pkV-JPVigkf--gv8%o=bZjigf zG1;^b#v%cu0?aQ$a<*>|uQ7%>-Pd$pXyP)ek3u;11K2&uPf7(bD3OBrf?d>QSiVNd zMI?xLCg3Z^A=#EGS}~BOm60b>nDvh(lTe~W;vk48h(LQKg8-tD>tf}?khMo3ux(1u zTi|zt_LnvEJE-adNx&kZ;J4W?wi#wtJSC#|pi}J9W;e*wqDg|N1hcL@{f0dz26f~Z z2_Zlz`0F?GUlaseh=+a#Te3*60x~d#C}P~syEk>~K3tud{(7Whye+eT@STn~;^#g+ zOm&Kc;rerFj!$Rm@9gJO&KVy*fG|*;jJu*PpA3Q1e4TjzljGXY2dmzRLnP_;cxyTr znBqvV1d+cu8Jt9ndyWro8q3{&KkRjMgw}S}m&dV47k&UpcXjynDRgUz`@sUh@mH55 zkkDEu_!Mn(Ad?l6j3K1Qb(}28ube_;d$Mb-M0S`7&E4{scVt`tb0jIsq2G%SaVae! zj#`d5RETQyq!c(0?5J@$@gphbB_|NrzNMc8J3NM#?_Y=(l4BFb4m3YRyy2uLHE z6}Fvm9fk9KKU$jpOoIUYapl%#=3ul~ag#{!sZDi$x)3z!8$(e3Vq z#0tC+ye#)T`R2)MFv9D{DYY@(XET5~M`Lg+QdRrGS!##jhyXrw?IevH4sYSvZ~Q~S zR;Q@HK0D$Arpc9#kESMGB@P%CjsSrn=-LjuY)gh43ZW?jEid+>V+9sV+zIZrrqRLi z`_=XkRL-DC)^VK3mS}i|aO80?#68XRvJMLfwI8+=S>z3+G?vwH=NtJC~QL#+Im5*vD8ciDlU$(u>+wH^(P&~S#8JRs=k@2%IyG`_ob=Ku zSUgSO3$#VvUFZnQpZ@mcO;Mzy#t*2GHH*&=@n?`IDZ)K$PJRrORCb+~H>a7X-rm^cWS&2nLL!52UzvX3?dP?`pd&Q6d7ob~(1dlEUZ z4eLcD2)-C<-LNpVwK-Ms_#nw`K>fNL^PamcDIt;)>>h$><={c#vhPFB_hizglM%zh;LU7w-Y+Ra5rP)lv4k))R(M$VBp`~h*LBf$-0D0)Pk*)8Zx?ubyTOc3uUO`i`K5q9?hlK_yB6xYxiXX~DUn(*D}^{5 zf_9UX71WORf^FN4EI;EPFGY|QO!(*EaW!!~e~TzPR5bzAb?Wy#NG;xU5Qbp+4B~uh zm=}`4^~XM;B94grcp7d812+#@$???G}hh>;LARRWIad0Ekw0Yvp` z-v54<>;8ZQ$yP!WTMGu#!!Ch^tQ7tL9Vp!~p$-hFfcKP>tN=yMyT+XbS-Zy9z8mPK z@MXM_HBk}BiIE~U+dFLsX2R2jtD}iG4>03~CaOQSWY4W)v6(|vM!T!a+QF+cG^5*BRsh|7t1CScC8)(_A(_!w_S z-f~C2F8|-u36xnSU@rp?Q>EYe{@Q+hrb2JB^V3YbNvb}dX>3r|exg&PNDvZo0t~YJ zy^j%xHxkl|b@57!d`2``ANddxmGqKIxV}qHOng}##8)xel&Ck;S2k10np)Z*Cq{(? zG)YrrZnZ1P3YTxYU zd=nqR(YWEdNbxMw4wbl%F=ABY;s}&vBVNHvt!xG?rqBjfZtieo-91`VFTNDt=~xQy zhT^k=8;}Ppm>$c(ICUu99y~}Syx=A>YRk~)f}i$?lv3oXg6j$i1rK^}{?m|wA&U)l zF!rPp;$1U*ROGyN4J-#4?^eD<5oG->0`R5YtfN_|*u^<2=#|oQUVLA24$Tv=ea!{o zm#2qLzIJ{V;<7q|SZPSWG&5eD*=bM+q<8u3gkVe^!?@0`uSdi@;f%NPAoHS=oCVXD$OWZtfw|OjEpW00Y~_3Hb;~pT{Sblh zgsUX4ZR0w_%0yT*h6Q|0fbg0*;+EUv#E2?V{w=Zp;BO18iGA~d>AE;X97DIt6S>dV zqC?oKI?g$%D|^}BW!K}yO^0OD5fS!9d#=9&^gg(5ZQevNN#*H2-xr1t{V%Gm;NFi#AoikIMHEvF_M&D>NBF2^htabeLn)& zXCKkW6hefTgR(I$`0S77tQ}ITFaW|-o-+Hidrom@VWR4zG23+wa*SzU3bke#DJ7|A zvmgNi#6%N@nB^<8@ovaOBLSBnmRDcInIHQxoc21!!{RNvIy?-+w;ul4+o08&kA~tB zm%a6NKOsPT8$x7AyzpM10s&1lv0`Aes;cTc=l#(}&S?7?cn}FooDjCRAP=AD{50oM zV+Q*ikRi&G!q7fgOJT>8ENGE6_%Lt+dkvyt4LCYJY+LSR7_SWCaj8(YLyig*b9J$8 zunT-TzqvMLcq_8sTZp6n@e5LvAi}#ZNDvl6M5F#n6zR3|N8?#0tl;?H1EkutGQv_3 zk=!#(&Qtd~bGd@PEkTh0BqtH(tdEw?lvjj|VBa@-pC4b+0FrC)Bw~Z_OGU+tcjeo0 z7CgbM&hgYLbV3$ua}rA zn3H9@nQ;a&@9YIA3Z8g zWuqK56~ny{2mG+#(WqiOWG_Gt$zDqP`nLlEKbWrO7OYGj*uFCxf1x$?~;TOOT91MX_3uNOa(2=bfnad9|}Zuecc0h@%1Mnzp6yz-Ae`b-$~F`XOHal8ja53IURN zT%D98S*`ag2ii7J^L z$M)GSBlS~EJRz90N8?k}>d>QkFc7#cZ+GR{v{nByI(C)dOmxtf2BKagf~y24qiGdK zVx|GNt^_{zGdnB>LZ87b2}G^-mb`dI488QU_&$npcQ;lSZnoM$wj4>w-J7ignWFfT zG7>;4E7h>1_-g?&dOz^QndbwG^y=+o2?}E@6amsR_G@9r9lnSdbE&Z^yOR~}&7cGu z1M|%%cJUZ76O?@^ex`_XLcckE^1!{GB9VG!4{*N?8_-BE?a%hzj8rZaEMR8OMo6ZP zUz>)k5=*{90V!&R$gCu1sT zev?#Cs<)Z+oYd!LCFJqYI$7uni6ibeTPye?Ka86?Ywgwv%59DeV)6pOoRl zCp;~4P3jFRuUTgR{xiimrY`=_82ad-uS(N#P!RBnfo@@{M{F&`j7csvT0HNwFx49d zwWuY3YGyw_YFK`m2=zlC_yin3m#1Dwhbye6NceDH1RUQPJbFHeo-J z-I9T$m`9hy{pkbuQRXhUP%}0;G3Mufm&CpHNEit{@j12Mcai7h$T{{hBZTJRc<#vO}`i_o(<;{{=I(4_v9lWB>G0im5+$wEd%{TYdXE+ za~@}(sI3|~rRn-RFm$wXsQ4LiSr4fe;pdr7cm1Wi zIzVK?;G{amZnQVn47ZMK1ws0d+8hq!Z)hcW>=nu`Pz$68W- zxN-F$3ex5^G_r5LzIu4XZgt3aUSddMNXn;PEHbKRq!&GmS;n6+GC>kPuB!cSgG z_12fWH;|%aE+S%UfEx++ok4>)gaiYgPAfQsLNwYc$_g!-$oLM>t{mDijnS)uVuz?y zlOkU-rhIVwZ^AhDdaV#9>Wmmhb*VbV61|aGoHBe;?osoQDAo8FQbXsYeCtsGCN|lK6#o$MG+^ok zckFyxaC8s+qf!wTO@oDcVjA)VjS(oiz4c-yQMw8QRMEusc{XDiL(w>jt05rNIEns>Btk2Omo(Ggtet2K7ggr_niJTzK@iTA`bXa^>d`k$GDC4X-*;g_~66;ZCB3? z6^#e+ks1by=#gtOv6Rsz3P$w`f+^e+OdA{`a$5y`+pFyR<#M#71jHyz?Tb=%6^#U8 z#MC4F7!3kAi}>>D$1-3?<1WbaI0Ifqlvz9$0PL-VM=Sa7VXgb0*PsFs!qbh1qOtyr zDgz3VkqE-JmR-WCEDa`%`fSQ}eirKu=n!G0>@#=7u-+k+gm)ym6tUAre(>m9%T}xa z)SuVfcUK$#)9L{lnuWm{%YGt`^9Pp59?*LCXF^y%)Sc@S#`;p(uQO|R{? zZHJw&?$~d4ku^F?w}!1!CE%T_qyFmHtq?1Y_xy^TKL5iASjE?WT9uIm16Ksep9bUy zDjWDN;p|i%){J*XMEet2Bk4+dQ4 z3rv{R`4^b}1*U(2>HjETV*ey{bmuI9ZPVdj>uFmzF#T&iVOb5~|5{JjmchT)6UNf} z*LvFC&bDJJ|LfLao7exkbr{b0*RA{i+O12=!KsIA>j%g!HnDvF4_>evDr#%P0n*Ly zOT#Zd`T&^?*chU*wU%9r9TOmhvWNJzWF`I`Hy!O3aUZ5lzM08jJJn4!vh{7EF`k4H zD+u$CKQ7PXo|l)WmTi0k*@Uz2FeQ_la<**lIjrs|S-?TPe0D?_OV>=lCxq{qLMfQ& z`t;Y~rbJaJtQoqHT859MqW=*)lqBtjc{7qQhs5%ot)T3K5@$~qV?7)mheSgrg7dIi zX{BJB%gS8{(!JU|(lxI~@cPJptO5QBc_>SVb8j$diER*OB74MSE*Zm1Sx~X3uK($; zKv5i0_;Om*9J^rHm>>#A(IE<_9@*-KY~upT4@0ne9+DAAjbHVwCiafHKaRfodlUf; z6v}hhPIjJzopat$P$nMeg2ap2oX$XwSd@6X+z0cD`za*m5MiK>-a@}K1NBH$KaL`# zfj51+<{MyZD-xfDy0p6D8yP$<{7Kk_!#f`jb4RWA#M_Ox9)&_GFJLTv)$3ayl3)vS zvC?uJ>^VGCX?YNv11!`a$a9!5@eNkUqr?z)RQPm3jt~+kC@G(%9#=8A0`_LpFF~xe z{}WC@g`e?dprNuXJqjhyuoVd+scm29BVYUWU_CE?1vu!mtqk_2BzXpT0=fy<&;tqm zo{=8jPeAOa6B&H}Az%r`@$$4X{8G1IZ}uqE_3_RynZ~v#;YcnI6W#wCb!cL|H#DlngiaS0LXw~*#EJUf9+9t$;m_=@5%6aFhHAM0DNC8u&~0E-|y1iYJ#7j2!D|o;iRH0A==AvXL_;f2oE$q zgt!{Jb=|2QoVS^ zZ#!i38sq=|C{~atx~iD+b{6yh=_qJbf$2UOKfC?En&+PYDfW*6$X_t) zY{0%QY{=O~A?z2h2l(&e|GW7AM@#;}iLzcV^FqEtZnsEqwp=;VIbg^ec^E zebTP@rOw8vDza@zE#rXHPW1d(PawmknCqdRu# zJN8G=Qxy$CX(efVJk2{bN-DB;#YhV0JaB zL-Tlu!=euz&or)-dnD8(3jD4K$=3;fLBa?XNf$mqjVMt?;pXa?CG6{}fNcILZIjh2 zIPvk%P5%Qf$qR$p=v%msRXMQ>cA*Y5|MaLWUzmFRIMG6d&1iFcTUGYfm*2UD?cbJv zr;6Piu?th9Rt;7YBDXRB@jPP4x&IA*`EbA|Cj-xvkh8q%9fn)hn_W5{gj;4RU!TOf zILdZ9FFb!b@j>JPviU5disL1fknMQRJW__~y3yi#H3#-3c}w?3CZ^=Ezy|i7>GtkW zINeC5%;dkI9|G9nYJlElqwkFW11FWZ;QM#lX$4!&(XWZbWMvM2eo-%YcyuY{tYyb9 ztG<+L=7F=x?`QhQuJJiUx5|zA3!k`L`zfOCbWC~hpk2iQ+^A~|VT(pu!uRKX=1vw} zn-bgdRuk1zCw*lH)$KDYwXUM%Y-&I6UaSuDhMHt#Pbr*BK!3`X`tCAQ;YFNy_iB>= zM1QOr^Igop%|hXyN?xxOA06uCk6iICEI7XqHpZBvsnq$MUFKbu*`mly_1bUo+E&WQ z)Rl_m%7IOpBQN5P&}ymDpo}T&60&LiaTMo1(&VX*_I)^6&8lU~=pKHj`K5A7*e< z)_CK}TOo4X)6=|Y@zr;~Rf{H{rK2f?@2|dMhzYX8LKi1>Co^M&kT(*wWW$~40=GUVmylx@Z^^!BKdOla5!Co9- z_W{%w@j{0M55$fKKnN++0{;K--{$`?JV!#>uK8r4+o;C3#fEE(C(i4uUl&Nn`TRo5 zOUk@w_(6YcBFzhTy;KwXL0>r@mZr4SF+Rmz8vXcf`=dAjNqg?s;OvKo6uC^ z4_Z?p8-@y?2A_y^qdBLDtVL_-dsyUCDjG2X-)RNp00#H{)aa_^mZHUMw#VQ|Dc55k zLh%pU{MRMY{|IoJoI7j*k}jq&+1&7Orq*en>2uDh@H}hts_fNAGjFh=A6;F2^m0sDa*LNazrp54PKAioSZ$rD)xF6FS376CB-1CeZ*ujE zjrQ2}&=XaZF*o$(=g9c5)=w;K-&}PMK1Nqk&0Rfnc9U zhnbh8IG@W@MJ*rOi~X+W=_glp?3{%sen+;%Y6^;^6#VuGa9AAnN$FWm_NpzmxjJ7w zOry7Ybwh7c??T__wW$~R)SGwBWF4yfOw3-4=xj(`Kb5{jtk#uxc4k@6ry{9MuFUh9 zAy1K%r>LvC(_n4UF;#74oy%>Z&o7gq)Mw`(arJL?lG$ynz6p%%c3mA*Ehl>?=T#@} zdVesFkNSoAW>wPggL!e2wt{R!r@@awx2+d%8fofk{mAQIw2#{CS6WT75$tERtZIs+ zpi)+yTG%QR*|L=Tao1@g;259tSbOebAamW;8)?`1qSe}xiLE6Og_dN)s8HGDsS&@` z(3CixuOSO20Sa4fC$xIXKJ#tq)H&9D@fcknu+nxNS*=ysk{(;*|Fm8!rJ~%G7htpH zx!hZwBYtyf)N#qD#p!H}R(-CW&5T50Rnj$%$kb0fDJL3+Gt*b&b0?ZtjjqyA#F{J# zyE=4QOGb)`+er7N6z8)xHwi_BIWN(5P$3)Xz{PdWQrHTE9a>w|u!+Rzt8G7{dBMyz z%?E>T)8Y81a?W=mP#jx-CVGA@Gi-ycXmcU8!h?*p(mV4%l)XSvG;IElw--_3`lBte zf3(J;;8>TH9gnZ+(@3G(|$7DKD+e%t+4& zZh5a-$*nDS%G7cRnyn_*Sq^Q5D!9a#Yv>dT`S&q3?y;&7aO+bnsT#6oo*~<_QvN< z55V2)TKWwMye<1*q!gT+yhka>Ds-{^1Xa(*;2!_6qBA} zch?^z_c&H9O+D|}>U+~)PjNV4`_P~^DX0OPCLQ=q|^~n=j zn=@vuVivzjo(_$C-^7!Eb9hXJp%hxbg0K7TDN&XQ4G66rI>jBAHG=Ig|AShz^73GI zJmi%{@0$BBoFrXyJlF%hK;rA)B8O@h-~83D#C=5Udh!w4t}bpjYgcEc-{sdL0x~R= z=B&n(i(*o{R;#XZ71sH^``!CKW{SzQ=TvISd+MO6HAQEpgnLETN8B=cpek5Px{u;f z?Tc%Pcgf*0&0$l^)oi=Bg|0mm4SF>lIpxo}UodZs53bWpwD!2lz6j%7we&RS9%ooQ zHrzIJG9~`V{f#-siz&aKD(T5OAJ8w|6x&M^;?m-gYdSq*S(3QbJGamq4^_O$FHbJe zeKhALNH5bCb*wg}NG|N2{J7dpIh^e(Y%aq)$VArdR87MkJL2lH^r9?%gGM$kyykb< zoPn!~zsRvYjM^>p^=qv~%-YUt{=+(3D>qfQJ_%)yTiHo(6a{5Cf6sxv9zuG~C3lUS zONRR;mFBi;-}QajY8tM+ykV#(1OL+z+fRa$YVeGa3!^Sx>zWD-G1KXdG}!Ney&HFz zzsr;aF8D2tSnN$H|EQ9mn&EJEm)~l54QH%)ZI_MA;YrevQ+UkPUx}(Oku6M_*-q{u zqSbOxa$5R;JM+z}hb>6hkIY)e88sRvjT^rz7lOwZk0Z9axIdofFI$2pB$+%X@POk@1$IVtr;I5PM`R(tsa8Wdn2_{a~tu` zHC&eG0t7!8CItx57g<}a7}_{6mu8!(91>oT*5}z;Zd&!daq4t-SZx#4f=QsQ)aus) zfyq4GJDtq{;w>YxUMUvmaHHlwLft#(=4$Fz9^2!roas8Kh3(hb9MnM!JR@r!x&L4r zcHkXS<+SW^dq=$ARNpcQP%Rq95GtlgIas*_`{aDk-&aF??Q$=0k3w z%NLpc=Th8?OmNd@OGtcBubKNpg*2-Q|q8Hu}t)9~l-+W#$i!uLRSqUG`T|pH*fU9v&A6 zs~r-qN!{q5n_YXGtiJJ0i$?NHLT~u935%aYhG#yeU!%5EU42EQj&CHFK%j_PQ>k^# zs&CkrU$2CE5=?6@u;yMcmsmBBcXhH}qP;ara6`O-*^!5K>65I`-i`WEagFTNR}bcs zXUjQ1R@oShU5Mn0U&$2rEaWTQyzy1(6tQ&hYyZ@g^Op={bIaTNRV(f^E!dr+I~|Ls zf*Oq05Ym3*UOi$WZYLM)B3ZN=)>#p9Q2WU~U$eV!kLUBj6E{i;%*zB>ySHHz z=QkA+pQ@Y}d(lv9LTq4S@PtETqkQdtg^l_XhHz)g8!M6%$|d$T&mQkHta0)Gxn<(^ z`(etmtK9dVZkI5H7vG%^ZKg?u;zngMMyHS3Ms`hXnQE$=ctyS&%9d(UvBMd2R4*HC zQzlE)^GSX!buPk1YbP6@& zn6X+JGw&X0zn0poGhca|NPNXdplK<0pJQjFT+W#WYQqwn$-}hxT3_XgMg8OP7NXA! ztkulje<@TNeQlB7xus`m|3IX;S<|RqN~Lm72Z!UP8tWyS=+uUJ5$k3Rcd5iMui})Z z+VMw+mqkKwUN`*^+h`E`4x7@Wl9OfND#;N47_ zF~L<8649=Q1@Qhl>3`QhDRFfqzO9tzTO<}NZo08G|M-!F@2lB+I^V8mS+k>{A(igs ze(jHZO+Cry7po@TkBX5nSAe>-ORubr#jmDHhdzA8D#gG~PMnIf}CmcivWB zwQN6dw}(nzlE<&YKD)jl-PJ|=cAdDUJB8JW$>QKmn`kl5H##`qX_E48XgfL5l7vdy z1x!$`CX%+@us>Gxt3EkD>x+VP!k1UY=1X-?I6EpP#0*|lXuIn0cKH^RI1^pB3;r}o zw#73(Ca8Or;ZVSGoA%16T_>Gz;eOFo6M^3Z2h`2PmW#b5t0!#FvWOHHkH7V?3+m;} zF-OIIztKQMi=RDonC4~}35r1RHWl_=8N1m%niULN>sl&$@&80*R0-oR!e_(e$@z=V zn_qE#+^}hD%IHne7|gh1^FbJoD=#P2RMnn|DZmmC?>cWR9)s;rTl=*0?}Q}1DVlYm z;u*#zq5}uo=={U=)RHQu1BXsLOrX?Hbeo@^&b?x8-4gMgtGiTa_W8cx+n@JSQ*CLL6Zeznb@r?Gj%+YCOR$VuUCT>NdR3t%bZ?mAjYziG zJJ!TW>twQ#4KbSUft8I3;W)3-Ex*w$TCOd%xOL_38}A&yM$tfF_>-saaKK8+t;pIm zvGcfRB!lZMI#^V5WiIQQ-+LF|vXgOJkptKRv|RfdDrzWnS%e47Km@%Q=SC4ReO zLf>KdzU&7_MNaZZV=#w)Xij01~awhKYZk&|gJfENw%&*6Dymv({nZ#(c4|qdX@0j@y_}RQO&0X)id?|dA%w(Gl#3<_snlBwak7leK*+a zTsX^4#3eHn;9T*lg5PStZx$S`_9@YR9;F7yC?l_Uj4f* zb+fK$+$buas`!CPbp_*bo|R_Rns4-{<}7{iLky^)e7ryfu-o<|4cq$W{&M;$b1$ zbvXCe+wEu75glfBu?+FV;zyU{NjSg1?XgxRIA)255@?spraafp$eq-5=SenRuz4%T zmS87~YR9eDl+M=~J>O8alvX)tvd7cKY>lOMpP8NAW865^e*No}|BJo%j%qU9!be2~ z6e%hq(u)X6l_mlK6qR12cTjq7p@k|UAWH9{1*Hk0cL+^EI)ol-f>Htm2t7b3H*;p@ zcjugYJa^W*f8Dj_Pu2?g-t6z$eZSAfI;7s$AyPz~uca1`tN{w!A{`_V<{qe2z{y;h z7#BJT@d~FtRfs6LQ2dr&)#ka-OJn{r+g$ z`*Ug6plNQ4*4Zp_o}4at>T+_8M~)JWYYD=%vMCgn@a z<)2^V8S8f?7(wzSgaX1K$p$|-+2$7 z;JWgxE$SRdpUHBc9#iFVd#nWgsnxB zC^8QfiEI`D45jSwR5_K0JSja>5T=BfvKfwHtSMhS59{941G|%H?rjAH43$};XwZq3 z_>mb)A}M}un}NwOK{p-5ZsNqLhwrfflA39frt`hwsC;Vbzwp{X_rcEYd4-;?C?5Vj z@^Y-l{b0n`UR_XyQq7LDerdI>bx&53+G5SF#T^YtD=LWn2EvISz7^?Nw_8?RMZf=J_2s=47pN=}o zC#R&82tvcQ2(&AGkdq!rH-SNEeSN3x64m?tpuk@{hbQwN*$8ARAKjS6$$qV!@fQj1 zKJ&DYM2?*;IPT#Ln3B~_2D?|+%|~^|)Ag<2d6sS$=#dsvS1Ac~Q-!Xk?@tOk+YxRN z(B*g1cl@98^mTkAv=mr|E)14GU!SQ{Zz0qS=rMxcrl{GPhQ}Tb&tZLce>3 zNJ)Qn!FO>gu=Yay3BlMmo~fDTvElWL$;9eP0rzCY;|&(1NrJ30L=CM7<1$uU zRb?)AKe;N`(xVKH)v?2S)sZrR;l>B|%&zs&d}T}95IWF+`rq@RRa9Y2z_)-3+yZ4E z4HytTKw0oRW;9${kZByiPm*3G(qR8dn4a}kMBFTOsiEYQr+x^L7f8r3}Y zuLWekh!mG|2qx*sNB0i(ukF_8Egu_K=G7j5+Y3vlTtYhU19!H#?L3IXADwJbOlg(rgd5ASH0{*;=QK!Gb!zo= zpUsCP2=v^Y!6u{xS(mw;n$jg9YrhmX3`bUQaK`j3Fkln)+oWf3QHmIEPdr>Ew(ER_ z;U|5tfllZ602tE38=&r~jFZ8)zjUYt@XcUh&q{$-G4ZhFsX%^c$D-oV*GqNEbzH`k zK2}h`Nr%_b1WiG%1{5_Z>P~RbhIBR>jY5NqeeO#^j&4j5Bzj21ywb4+g7n8nNM>A$ z*-FAD)7%;B2!+Gqs(fu9Q_XV2zUZS*3y1lBjOSv z>q7|f71jZCRr}-UrF-jwxaH17W5R${&cU zaRTRqR+`Jo4yhJO8B#np)QuIL61}}G#rWmPud{RKDYc%uGu4LG(@pDC9^@}}9!9^o z!GG}bgfzuZ)tcOQ&q^YUosxZ@V4dg8y7 z1O8<-o_xIs2{-@)aI-*n0u2O+K-yqwZZ10rk%81?64XSylt8@Fu-S-NR6pNSw<56j z5csiDEa$?h{$uUmU!|HYLXdURGRR`=f4q?YJjwsli2b&SLQ^Ak^*vX6XbKPoYGbJ#&VA8 zWxXg5lopFLrRecrtXIALWy7*olLt=;v!d6ac?X?_cGJ_meWh)K9pJfEPE)&-<8U>l z@82H8F?jb5m5wyi+UOPS8A2FW<7;N3R~l#nMoJSyLYyk!mz%9-D#%6ttr+G?zXhQf zxO=$xP~?yL=KZ&ANA)U!jOv~qt2CP8@(DkvA!_53p9x?hEN1YoEqf@dP`i_{fsj|Nl+~LBKk}Mc!-32y6c)oheGj2!Ma9* z5DKVa>B5GwBOhnlSyWAB)Z&QygYyky&A=h(9TB==nbv4N)# zWiQc3(GT5Ghr*{G2T~78bm{vCzazK1P&i8Sgns;jW}%vZdJ~i=7j}i!syTWvN_lrj zt|I4pp%YK8wPI*wv!}#9I-Zg)ptjVqETp(gCPrqDBvuJ)s-ke7X1IFh6BgV01Z%V~ zs}x0Ud!f)i>vAmTfvDnX!m6JPM5{n&guU*?qD-0o1Ld-^Me(>8u(L%#*+$T3d2{*Q za#Q`g6b$LPtA0Fr?pelMAZE^VwqHQ^-vrXeNOt{6+B5$~x!-Xd+(4cBc$#P5$YS|I za`o!F!-gwzJ{ReMk0=vDS~V|j_gn>!{M24DGvJ^Reyr$SJDnMX9smY7j3ut|HT^&|y)oBjx7VXym~ESC|8cHtQR!!Q z?RxVT2#4iPv*IvRmM7izAo{lXkzKK?fX1tQcc~f1vnoN5rm=5jLLL57rRQHwy z;xzZoaH;|Jj2)q8Ib(kyJ-Vh!onY9v5l?0y5(6=y4u|Cib_Ks>GQ>sM)3hEjU*%sH z*L!$T^8dZMO%Tr$Bw*8aqVeIbFKOaoH0qPoHt(*Ux;sfwSC_gU59jtR;LtE zT+x?E^6LJlRt*LXsc5qt2xD9xbS8e`!zM#qGNvlLF*3F{qj{GMy}>5*6j%CM-Vn16#L2Xpk@Ud9!Ic1Efii3Z)7at-o=ZQYd-aXDtpJ@(S;#}4{h#0c>wZXf$8RmN!>g8ue`sJVq;0H_&2D_!N@|q0c4x;s)=0{z6Yf)h9}7fyhvy!Y4r&vwInmSE zrb2W=f44Ps$}AojQ;P4rgQiI~oc@8rGY_$R7ZoOSUZwRf0{vRdEb<5p@T08igG#dK z0?Yb>alML4XB^wAzJ@`@D4_vgJ8LYk>Oy2l^dQEK*2WTSDovNvy~YQ|))|gF>|N6w z=kbhPmdbI_-4!)fy^#=x0!!H`%c&%FE(QxK@IE}X$sd59~q4rV+ zyh(iDwR93Ha{iWJ-0ch|fN_rtW*`Vvehv;dMR1~-Y2daFadpX|xRZ8Q+|W`b@sg%# zRsf>`X(5YY7mZAr)9RfBr?c8x@WP~f&8mMEV_ElG{K(^aF^?z1iSK)64w2N)EQoG?V)I6p$XmNf%#Xq*H*^3iXxVLgTD$1?H8AE2{=1Ze2~P6{7WK)` zK2P|x2|RouUK;&#!cTUOd2p2T6K)37j4pvJp6V_Kc1>~#!BNw?gvX(?J>~i8$C^sJ zaAL{}&XUJ^`ZZboeiOdl1+7$%;ah;N6f5C{MxOHcp~F`&kxeC?BPGupaUm|7i7$v} zM@Iut-_;L#KloX!-tE(Dc+gkDCDZucCa5%@+69M^ES#C*g`nS5xy8}O!)Z@TS$!#t zWUlRo zz_gzyxMT4~q0vn83GL4uKT5^Eo!-U6NW}_T;llIJ(Gw?#xo4N?j|H~9oi<}fbYAFdae2veGzM|+gQRg_Q-r+fdqqUT zGjI{58fJW?oR520(jm{EgsU49kwhJGI#K~epwQpw?S}md12d5f=<4YXZ=rEccjDNe zEYW={>GF#@%~&~<62s%J&)25CJb>Et&cL~XLG)GdV{!IV8QT;#_^DxYu5QTnQipBh z1`#Rby*OyoYsfp57AejyxiG84q)+pBy9i_s%B5!^(4A!2NWs*sYyVinU!hC@v0q+p zXb<(sUHz1WaC?M#te4+q2T=Fj1I3f`tt4@W*ByBdgpY0aplo*sC z`kN^#*NYg5YC$_0`$RBZ4(ZI;I9hy-FDlldq~^;+pYTMQ!mT6Us*c(ck8gQTu>de* z%BQbEi7l+KVnjiKA095=5VSEkfU|o_l$#AK8!&`71O^Q5Hqj^@sj$|6Gw?3bj2cJSHV80PrQy3p2hfr9{A0$F~M zoc;dk3IHVP+PuEWA3>sR*;sEAw5$*013jy7!i(&InsHLGP@1?1y~bRN`D&7xy`I+r zX46nqN~J$-fAyV|OOmzSpM>e}kJzXkMQNOt8e#yz#o63QX6AU)(=7VEfWhNU4Opq# z*{-*)(ndV@19coo5Z=%~>0w8bCI1xlwRoF?YEOxGzQLfm>1o;{5inL8!<2^-Jl^+< z+BU5sA;j1dq-Sz#`_lX;{dDquEdLg@{vyNaZzOmd1Frl_e|j5%i&|VU-Os-Z8F;B$ zUu+b7@Cfp%qnI&L{Ivw(PNTDNT8db{tl%}H#rdk55m>E37%-}kyC{LfC|CBJ=7`Ub zuRU`1@`mwUlMq2J*7-SeSt##V0hISSjyXPw5+s7oXlVz(|adrMldB!a&Z))YKb(fA_ogxapojE(_U9E2K zano&$i!Fyfe!5cZzK5mD!kGyhlOL7R+|T}ipJ!vH%rhwd0T|FUKob}g^SsY2>5Iu% z_O1Zy%=0f?4>1V@>3zazsP*>c~O zhYu>E9P-4lK<2m>gtt0guHD_(^ z`f&`~dumyc4vZNMh;$OjNDS+BTOC6i&r`oBV{-Y<3>~;i@nQnIQj-oph8-Z8R~u(U z#gVXm0BF!ReCZ@0&^XQ6xqsnI0jn`yWUV?VE{X*88RgmR=Q>Wgwb@Dgf6_G%Z?rV9 zI#}HXx}VdBJVF%^6LJG|OVKkm%CXg&xk7~3{P@Z>R#oPxp6QY!lkkbyyOPJ2=tg>r zJ)}&&8dEH1fH#TMYLrt-wGs*^I_vFX=0`_52jXju#hC6J2P6 z*d(klwb7qWroDcsh86cTzLcFVjBsh$@WGe0w5lJA<(Fc5a+-bBCuwps*un&O9mih= zB!poEHpOkcA8HXipQuJfi~R9~O3!rEL9;b(`P`)W0q1FYO3zqNV8m>Cq2>}ws9CPH zjAeR;JqbkIDr@xPPwfA1@2ErYj>S78kN@2e3f*eAYdVONz%$vo#WMrYtg6jtFttQp4s-VLEZYE<|i1b%6^*JZt^}SBQ_^P z3OgSo8(Y?#C2)J?ifc|al`pX{Uz+j_@x$doK+$DeVp0k(l{yoP9gIYYlG{;vbvVff z5Ay>>T!-U^A5=Hyv3>Mxk;Ka?V<<&OjciPrka`Ir_P=Xb&$)%}0IJ>$=w!Gt!c78A zS!;;u>uLX*BoiNH)c}9XwW_P&sk5d8C_yA1X|YL?hF>f?OEt1ORc@D8rI+5RDaMVC zIPBRXCF$l9?1q{0dy2>Ne$Po{#u8hwKYdY0MNfHGV~1@p8Dv)y!X>RXqUX0lhV_nP zqG{#w_zj-kuVLTLQd4Oa78aaCcC|n{V~*ikszUS0Mdem55Dcz^hkm5{?mOGM1?B=g z=;l;DZUnP&)2|OXvTMWzgpBpff?0SRhd%$1z)p>}StO%B@gi79v%==lLj--XjvlVc zMd%cm>VeE?`u5YN6B_avv7ytY_<-2WPA~j()ZM^w5<(iM%KeNxW}xR3^P{3@iQI0b zS~!4$=GGH;2tL%a5(o^fmPx#7nQEe=paT8C2V4^2GPOhCUro-coGXC?*>keUlqSNQ zeBqDkHw|!uT3vpk*MCyEXbZ%2i?)`4Ga!_B2%Fj^gNq9Ea?TSEE5+gsO>{DSZAhD7 z7%!&8=*=Zi zx)L00)9SX;a5mIyV&q)v9UxYm)Dp*B=w_1Mkfsp0$%Ko46u=_-7@Jf6sB;hhq-r-H zTegvss;`&F1EVe?QaepUuPinXu;|>UTL6)~V3zulR5G^14gu=w2_FI6gGiNG!nXU0 zq79f4ekXkATEdluPt`W1UKy(wdY1>PQXaPOWA>%$22Qht8-G&$otSgkn4nb|kc%XL z7G8i4Ua5Q-xEYx`CzwW^e~p4wrwkKYipS)RfF09fEA)JGk3Pi~-PWPF=?CEMD|PpZ zEv(x&>qpt@GTDQac!p;eV_4~|`BPUmiR?z6D1)WMPi2kOnkM;%=a#jP8t1}uEneg~ z!1sP~<=ixA{+^m^&>UExYp4IIE4i@oMiBO$hf8#Ctlv-jWcw?dJu7kr%7z@ZIi~id zMX{CFu12T_u)Blu5+##DeiE)jylC`|S7UG0JJ&gfRp~(=PE(!cYl9`a)MQCudi#jZu{b#oe71 zw#+N*GU^d^_&l$iBY`9N+V6`l69PbDiJL-#BLe%4NF22nTl#1$2Z<9Vho)S zE%}40y<1vNBP}S=9-|1-gD6;<=XT>eyR zr%MUKtWBWL5haOh4*SRMp^cpCcCn5LAk+rq7@jq|XtZ9J*+E5*nOa>Z{mmuivJcAV zX*46JZoh@>+!Q>?y`o9J%`;i_6I(JoBHG@@Ax{P${U{*~baz(asS%a@5jN3$cO)?{ zO?Ptld9_mSppAs(ivf0~G9*NzL6TojVQC3$^L1>PKf+NI0;KRAcs_@SWM23!#GahB zx@=#B9q+y6O{%iv75>tgRDT^^48?4{@czy`$6YkRXHZ-c_N^(w*LPo$rR&YE6*u>!|8A zN&6+$aZS*RWhtH>v>r^l)p(eNVkz4Gz8FxaK=NQ#eqx%80=Tzf8>@t7%47TJ9ixCY zp}(~qiD(QWslI8N9#sJ zX?%6-x%a9!8Itlb0q#OoSjiIU`U)N?644=Cnxw}v`C!$<1bCqOXmC*U=7;XvcORF~ z(3(v?;3@Mo2@3X0X+X1_AEAE}rz>bKfF>b|}dxH>K1J7B*xPQ+A1=RJmA$A? zGjp746X1H^+WaL{aD7MHV#xl^T|zMDs#J6aJy+g0JFN@)#_7yp-4o@Ii+%|i3scd| z9OI^RnA2grZSR?vayp*Wl%RvzGy8@uKf8C*2B{}^DoIh(W6fPEyg{J?is`pVcZSNf zlL}I>92W4yB!zJlyRO(cl>3hj67|56xXDkcywLu@TZMx6EZujS1X3*{5e!ZGCDaR^ znpIPc;$9BtBov7?#KgVDR9y0TppDyHk>(|2uoiVdo=%c+lH+GMxpJRo(1k(!pB=yf z!{DAQj*_n*ZA5%3#{4!*_^Qr1kWBH@FbQD>zA-X^5W^qeMW0ISemcM>9$*RqNrXF2 z=mkRx$KV@jZgn<%esCwt6p~h5ybI?-3s80EpcZ{9NvOnUn9Wm8fa18!inAtA0xI(o9!XLX6b7w;|Yv<`4_#L1RTD@!5{WW+tj4hC>YITO+IX-`-<&v8(9I7rkty$3F9jH2?!OW}+!yX%N^24G!$>cKdA@ zbw&oQqe!$kmW zOd|oXwS+k4hauvfH+FHVyy!ZZ^TnWgcXB#LIEm#V^jm7ea01Msd^wlI!TmfHznu0y z9VX@X@!QP4df<5Z_xYEv@)%($t0&B(uhgrU)`#)0EwjIpLsvl`Ryb_9B4fKiY5sX8 z6Vi)h--zdIvg}!CFus;P@=k|rKHTgI-8J;cCu`JLj-`yR>A%NaJRR)r+ti^oN^!cn zt7_q--%5oSTCNH8dzMd0;{ZJTYpG-zyv~ez?vcmFqfg|lqRCW{_VfO4vIw-y<-TuG+8<) zOABnldZ_QtNjAHMQ$BHMT-sfyLZMDLwtw8PGe9g~W6P^8N)E%P=bA#z_!__M&-FRV zo(o|ii@O4SyOYIDxrlJBuiZ^vJD|bCF!Qpl*%WaIuxbk+RCbAJa*h9#xnYF)6SfW% zW5j~4M*u8Z?hBx7^@INor22n@l!3b8pOCuw8>9yJa?hIh0Myd$oj<_u41Ui~tX5y} zyel3iV>{qj&W>2fYVN{`Dla6g_6o7hFjyOUavU6&^TK9FW6|&qzjg_?$66+Z$7e*D zk28)^<_n^`I4Z@pl0NC2TOH-M1sNb6CsUvBKBZ9w)Jh?GCN6nV8BbX=Ih=?S zr>P34*cJ`&tbtzVx>`KU|Fc&X59)yEY$RW+ao*}Twv*aBZvwLQGZ0D}g2B_9} zOps?v?uj4-ja{G_v@bB6hRNh2$ zYko|x$8kHh@H<*W7@PZY5!&zRt48GdUw0MU<;^)xMtEwX-uEeWhfS z>>yOvCoBklY91)CtxHst933E72$M!Xo0c@DNRM%tY#P3rj`ke{b;(epxcV`yF-0GV zb>l~TEer+iU*wx72Fh;&1LO~K%KsNf$)96`guWu{8xik=y+6CtyH8jlSHD!rr~q#k zy;FI|=W#P1XI2U4dXf+m-k_=tU93*kaliOA-^1Ob1I%brMW~IG!Y{{0&TA1RBoKA&cF7(#N+im&_X9e6V+?F?m2Ktt5xkY8NAB1PtZ}1bv{bi? zZnQi@c;aFyUBNwdnmqr(keV`_@nM;GLHPQ$WIVBF>+@%VIJsPW$=LGig@;(!qwTEp z^-N;_Gyv8YFyWFp$ui!ndjQTn5Hy&(_1IwcG5@fVz;DHIah_tIqMA@C;Y*Y=O@s&h zxY#9Q1fP2I39m;3&{e;e26*P(DNa`YNcE$~`LXDT16fdugenNITUfNTp6E^^YFHy1HLrQ03YxoPk`(wMV21p}dS@288{-y3x^359(U*q?x+E#~5htQ6{ zb^Kfj%`E@l!nZjWV!ws;)+h&C3Ah6k1V6g!r)$lmcnr{=S@cC2%%sBo&IwJ62Zvqs z&xqh>Z)Xrp`A8|Yc04}M&sD?&*Ik>sZp?Ds92W+BRwU;O9Rl$f`CjUqcoP4kz>nT? zN|g9=8~`naIDNQOO@yC1B%9uMay34;E{Z%@-3M|(a9$@9bt(P`QcL6nPAp7100Jw|6BY**+ zZGD-TSJlG2-utalS-z#TyZ~4 zqpjhpiw5NKwj!M*NLCIBV7G9I?pt+4n=tf(RHh_&w#2O%5h4(-*Va$lMJ~}0Ayk7G zASGzWp~(OY88ejYO=3ZvTlB?JsRba4b=TmAEA!42pDV*INUOW%2SvTlC7R$l;^Ihk zweE<89HHaR8+1%8eZH;APwsGSZ^n)x$HC6xtQ@pM1vR|@sz5Dl6;Cd>7+QdL>=vyp z&dirlv9iph{Ej~~YZY`WDK$p?^9{3sZt*bkbpJU#iEW}7y`OLOL zb!u!CpbvcSS_QK)FDQfSzG-|UheW20FN#e1TY6_!Z%`d(Y3T5jFr-^9@%817(8TY? zyhPkO%n&ZCX}b+9-h-gumW5pG0%YP8qC@5_{!RvNt@>MCYk1`0A9bz7w{HYB1YenZ zx}t zrD%ZfOE2UynM;?t!sv%DlP()uuHLeQLJZ7GzTd9T?@kRyGIu}gJ#XVILx_@#!F+%? z13c$nZ+rbT>(+r(n?o1sZx#Fr`%-foUKXhTS@0LT*qJ*C;Ie?9%nw{0eM%H%33-t^ zlo}&1J#!dU$&`Z@9=jrMJz&*ubp}E)@D88i{H+*!6ebFxBB3M_N~&NaZDLSoqCYY7pss>c9Hk z;O+CQz};zeD32#H<7#((`WMriacK;+idGGxQ@+94+8ufLPyLJQNbd+-wPANA z*q#k6sI4%tW@u3RMYET71uQVS3wg|zsV-p?ct#ovfPed-DoBTH;sw4pNbcX2zhI{g z9SBvyd*7-u^8*6UB|>)jQWvhmApS4|9soi|zwMqaDnd^p?~G0ICVJZZbeqUC7EgM-R=u)%&R%}}!E~L&y~de$c^ZB)J6&UFU$j>qrTgtXb!1)%^RfA6 z11ExJ!KHnDCaC{R>cl|}fZ!OmS?~VXJ@{24!gz_fPo)K`d72LY2D$oDS$RTgnwoyR+;VgNNj%}gA<*w>}-CnoJAcfl))WH8WsZ~&^j%m@bYeZ(m z0z6{WJ_qoOE4@yT7}(XEY?+cX@qrPm2da9@Y%9UkKEN+47hgpWn;7M)adqf%Tza_t zn2V%N;k0xwaq12JwJ*8{fXv3s*otMF0TYGyQfxGVGuLuPTuboNS2w!=v`@e0y;0n{ zcDHgj?sj2`LGCA(AXvLmijtr2XYWZ*-PqxVt9E9lkgExHDdSKhtB<ac)Na?%-A zHqqFWd4~(LT+Pf}-9K{}^6wn$ZA+KZq}iI?dgPc9{$@UY%K3-RLenYDj(2IrlA|(2 zW8*low`qXged|!oz(dg*4h}>a@c9jpL?0-Q=YgdM@}@us=>h#6W^Su4#%yz5_i{ri z+*e+vYd7+im^RTANSHDvHCi6>@{?{Fu$io4b-ohiZ@G2KFed1ktt6G69x!lKIawmu zrWzWBs=gU=Z;9I&I|%Oud@jdiZ~Ychn6q!w{++3m?}SiP8{pt8`+NKm47&f2ftuzT zAr$@Q@$$*tDeK^bhXul)1X-vo7~riRSOV9zMqr%m6??7zSPFwktV41qSJwy(E%ReK z;}$(5$>-Ta0Pz&eK7+*@9O^N2KjF~qPuuk+($OUHqGoTr20l$BOt%bL$bq8s2 zfzX0Kr0ZJ&2;XlGaq?8XgEJS41bou^Km*_ZWiKD^-@8p{ucqYu1Z!^CkMC_G84kk2 z0@@lX>Z$^})c1SIO2{nFwO!WY&IW1@AZI9I8KV=`#|~tU(AWj{i_qIzyk|>wZkA7yb1A|=jhJ+Q252ora6RI!Ec2htF zo^7j!1=RTUS(6`ZkFmnm1tc;%g%XyR6+$j5$Q`QXs$POo$h{JKX(gxYi-oN}We!S7 zpwBnKP>z;j;t~w!1a^gYv64`yUE{-cbMTp`U2@i`9E%!s-v%qiI6mxQ8@javnyAhP|kU z`$a31I}~R&4fi&$c02RnH3<*1$`i!SR8w5@z8rIAen8 zo~=K!{)q#X90P&*Yca*h2RShIv$ZE#Eln2tx`%>1>z6}+0w{btZ+Jz~BYfX{g6Uu{ zJDPQOHdpX-RkJIXXvN)&5%fC{JL7fz>B-i$wAn=-U~9YmMK{8YG-8%PvTwPW>vNJ! zv>$iSZ-zjH5wa;L8U9+}7GPrPDk}&Z@p-njHkeQhh;HBHLOD--u=6oQqpq~;)by@A z(V9TNKVsYuH*CULENYaH0n@k5uQ8sjY`%A#atRkd+2~y)xO*qy+qIdc))H|4`j6vDZsO&)P-mei~+)#Y0FCmRDSuBh)S*5e7JF0XNcjh4U7aY=@ zFZ2WFKmkJTh4(CqpCU56!}m~b&852arO-wbLJO$|gu{F7o7!OZ#H-qOGak5}jDjCa z&siKW#n==#l!`i>ukK|6S;`zVHq9V%zM<%&qSpv%y-88WvPIt*n^Fg{@#HD9O`8ae^_rYa0p|E~8-!uO zvGMogRB{(1uNxM_CIYs14ml;sjX!-|W)q%V7q9!H+y!@Plfuw!MT+C9I{c&G1yF5y zwf}#JDgC)x&E7kNhNSMbd>>@-)bWJ^IL2&*gfAw5NihGa1#b44+bi&+x}_ z_abaBye<%U^J@C`qry?GDR$eDD$JK1Gv21Tw_L4;A=}#TzUU(o$Y(Fdj0&?&P2b-N z9O~J1@Yp%&^5P*%2;GQ@Z{_l=JT*jE;>*1V?na;6N$rk8u9p#HuNcnkEYz6R$%5Qc z4<*qI6N$Zfn>I}X$rCpqn8StbFn6Je3?sA?onCZ%`a8=`ry%;uq2J3L#+tBy`11${ zdBkS}B1N7MyQIn_Hl5y^So{4P*euS*PW#EtZS9GXm(SMANxEgDwM6`fVZ%+gFY)*c zB%q5i9$^A&`spst9&gi+-aU=N+}~oxd8fdofo|u=^7bVS?`;CVqNT?n8bm!sMKoK1 zRb3K-ll=RzNJ-OYPXb!jOpq^M&6S4GmtUi5%koh|h5HF(pIyCrq{6(JSR*M^VPb&f zZ1}&63*!450@?9Pgb_vIQvmm&r2bcU{7IG(wLCr#&l00xyzrh!tXgUG!YmgrZ?g-CgvDJ(C3@xZfm7+#bqUYr1+a9Xk$q!_41jG8Cuh}t zl_zh8$_|nC&Qbg6$N*2We4dSbg0{(zh|8Jk-D8V>?mH{79x|XOpQkvV`cd|sj53%H zOY3=mk|DDg1;*JLF=%GaG=S=rl73srg3wbmrE3sZs($ z{ZQkMYjKmyhPy}wv70jE(D7j7r-emd82xKs+%~bteQp)tuvEDuIY89_u_}ZIbS+Nv zq0NKg4;Jdbl}5#?H4_O*`2{0B>%7RrFvDx^+`i9CFo0K^jQa%{Ze>*%eoDytnmT5> z)L>>Tsak+-Iv})V5uUFGKOgy1z#tg|bJv*R{@9QTbXx8axIZ0gt(CIpXlkd+JA#|o zLVDSW$;#@Y*~YawV^(&{2c#q-R`BKRczHw`$w2Umi#`AgOfnDse zuR6IR=XzE#+ovPNh?!rI0>St4g~v0s!-0->$t-fPCNgd>gN*} zapMY=qOuagcu!wmKlNEe&|}WBO^mLe_e<5>g%=bzh|}_Y_hf8F?;1?9w+qNW`{uBU zFK>F$_~?bD!T9I4H;MaFKd^g#-^zETKB-YMVPlnWlTtVse;L%7 zfj5Zn0xCnb@*98`(Bdi&lRu@(8kP)~@Yw$wCOpRS$ zS=gI5%EpwlRkDo$bgjAK^R<>UKlWC$(hW-09Fw?+b`#CkcB5@si+d8whMA0zLrWzj z-p5!iCf#CCssZs>Xd&ishO}B%FQsCTtn&FpSt4qbr^fiqB=3J?FOw|ISBti=u$mxpRFby>kKxfgDuCbLD`y$JBm=VE+FDy+KMP(H$3WWVzEbzuhJB=MF9(}Rtm=5UCUK( z=Uws~-5&IYG}H&pA!O~3*t3p^A$X4%7E0xfMlblwz2-cc*qo5XlMy{Bnxg#5a{*+6omU4c(9p!M}F~<=@`$z%xBz^3iwqdnbW+rYfv18(SF2ctY(_m5nUm-r` zn@7tpHakq`iW!Kz5VDjhDasfP?A(Mh6CF!k5^71dZL=m^N+t*Rp=6!vG zi-N)1hSKD)-j>&L!MIQ389GvhYnbs$+t&>oB9Fz6^IJ%}=9oh33|4@H_MV&&dEy{a zoFsm~zY0`O`PtAhP(GPq{LCkGB)EmCeD|7h5e%%69%h=KGpJ zC)8F_oO9^o>c;m_eKVFm-XYrc=T`VPs$7&^BJX}uun{|^8tXu5WEv~APwvmQtv`6| zxPauufm~gj1ZXEpy>}eW5JtvfwWYCxIbN*>fetTH<2I5itzH1v=&y;N7v8R(W7;19 zO_EDrt}Uerr9>vh`j(KzpY%+ZcK>$mWp2z)`x}`fqDH9E`?=w5bR*D2(&zTVv`zB? zxNBIU3mL-hw?8?WAB>ifPWIT7BL&QHo=2{5Y_=9k{CJmLIagqTPs=;%DQ68)+nU(Y z43cQN#aK*6AFQh}bir)@y!IPqVeO@u2R?e|)4<~l!>n^)?yNXS)b9;(1Pk$8kORKK{_ z0RH{df%)JCVD}nf<~=Kf?9XcWClk#MMC8x^M+hEbMwr3HdO{kJ0BTsmtA9RyXXEo-uBtNIf2$$tIh|cia z6wNR@6*+XW(m$KQr+MC|?XWvgw`)Ct?SI#xt6!(^sl~aDDCX3Ex9jk zH%3C3?!#ZAZ=-j`yaLsdQ4x55t$+l6* z!)^Q@8DSfc>y@Rj#3N293r^c)G3_)^@uo{m3a_D7<<3&}`RASyQyQdrYZ~fqj`PE= zimN5^l}*eu=|X#QF0b7xNGSX(xTVT;4dbD$U(NzQ-?^`Ggjm}hpBei;*t}_uzCZ;e zf~C!^#O?L+fsbbt4fahOrymsZSftSv_Li;tg=RVS?$A~VdGg&?R#rLYk5XRCwKgic zGjwaN%_!;wA6aY)iT?!kk_MmDSwI|QPMS1j5>xKf7z6w7S^dC7n9C((k0{SnRl3=j zb_xs}lI~Y`-rGl#jzEm&GvlU&=Y$q4?_F6r*KD9K_i{1qL_&&QZk8uQ4W0z*C+9ea zU8nFcY+e^`X(o1Y>6wGuSZWUN5EFk_Cs#rh=6bN$r7V9~XfyI@!}sU+n=v6?Nt|DR z92O>$$n7gPl^B#EEVq3#hcFLUy_xJ8l09c5|1Z+sJFLlVdl#h$sED8_NJkM6=}mf9 zx}bn`2sKETUPBQPrT1P`st^bedJBkjLLhXh(jql<2qAE@)?WVhx%Zs4*M83ZBhQm3 z`P!V{m}89jj(3c)aql>wEB3H=7f}50D_wNVj2aB#@Nf4qQ1V&DW26g;CA$IQ$=Mf zmgo=9qR3u54@es)ndA+e^ws0Fgs;&yXOr;^$#8gdv@=*cH8vV=fN>w zXk6K#gST=HfD&d>3ID|A8ib=mn=MyVkgORO5L^NH2qZP0qFrkn$Bk*{2ikPF6cx zS5y>SJ#2kc70`<5+wH36w`wI~$+lw`a6s)hh4$8k^6Xnkj zK8zGV&~=Sj%0`~w03z65+){Wl6qtHyfJ*#riq(O~SeJlU6Cy|u{YrxPaV8#GLWJE@)ZM2o;?d8T`ssE%J` z)wJH`rYyRyvCqSu!NM{yF$!j`;+?sCjP(4qJJf$8z$E~-9bIrK!Aob4y!==v$Pr;; z;>~=DEOFmH+q60;XF=|Z=fBq?4tjw6f}@tw)7VX}-&JhXt}9$G#@vO&tI}(jBAedK zngD;c9$)GkZ=OFLr`!}ry2azvpcBo{jEy%e&>sD>_bz@`#Xi|anW~6sfNP%{?>|3~ z_|nf&96V~0hyiTuY?U6x;npqfM;r@|4fK&e(&*~>vuX!Bje9~CL(aW0hS~T0*&Fm+omJHqmXZG`QaXz8XWLT3sleGWcL znnl!3I8atB7?&9%mWAR*s|!y$&aZsI?Q;4!K3SHIpHYR9C zUjB7ytmc60h`-dqR%kXSy$5XI7w*yaY+QC$m~a})L}Ls4Cp!kA6DhXO-yBWyGID{3 z^YML%(5B7fx}mqm3#o?NCjnB}p+!#aM1PsB>{2`dP!njfxuQ9!m2T{}B?b%c*1oa?X8+R7v{wvLT8?RY!(@Oo@MV}&6EfYKQZmFdI?Sse@ zgyJ~g7(i)ad77qEzS&Iv2zLI2Ph9;?ef|&MUa$}n#cwRDa{WH0ot#ac07*VNE^_BD z`82l)9n3uRDPFPcVD)xUPUZJ(?R`+g^l$0?+i#(?#vud+v`tuh>EEFMDl_~qzkrPY z!*OC139Vf?(%8sk2zn?~)$q8_Ad}KqxP<@r3IA~nV`})TNh;{v_3@dau0S<)=53U^a~UNfl>%B_P`~^b z8}`C^#=o6B{sR1~=C`RIef{6Wv;S7;zu}GeouQAde|x}x^M~(! zlB@Fm_nUK+|E=-=PAdL(Kv!9*wD5BGMr$kle-_u@NyUG-csn`QAGTn6TZI1q^Oi%+ zZch!Vb8sUM6LTSrMwvOV7AoHnlH7H#Jx>!z6h}hdFR#zRzW-I#69OWv5&mIuvr=Oe z|L@ya0In4naICI$HQ#o;JDs>(T`B%x`^wGJE$n_9~VXBa98 zsVtCaIXX;<-$|S)L!@8F^i9M)pa$@$?T@d$&rSRWx+0WS?dh!E?OKrusPk4|Lfl4P zUW9lu6#+CEW&f*(Z4aViC9sz}B=Y)eeT16b?JW>n{n`EA>7tUI4&KjNU2aczQzM|~ zl4}p(sk*glR<5;UGtFs&ce0M&nDa&yZc~wKAKW{q#SeB0K*S(MwlbJK*8Qk?x22;Xof*(x0}vQ1-*N8cRhI9;zXV! zCv^~d-IIQC;?O;+re(H(?)~w_^y7oM#KT;s$;GZu2AaS&RFHG zQIUi-rueyJ?nw}j0c7i9j_siHJvjJ8;KWq{1$aAD*7)@qaa1seGo(fhTjqQZWpw(_ zCiw*iUNg{LWB5zX7ya%|wZICx}wCt zL`!S4Z$0lfWSu`BUW?g+|A4g@b^O+^D6Y>OdqJbVCW;4phh+u#^7oZb2i8Et{ zW%iYHRXM{{*yT|Pk0WV=17s?p;Kq5Ww!hZ*z4G^6C;wp;{<-FM74drEP|-^)=3nKh z7Q62B%9^LGpOCJkGAvR@-ske*roO$9@}0oQ$Y1RK^1_|R-ZoEZ^Ny>gc=hB&gE%>@ zCsH|z9WbNEGJUb*r!>sx!ZvvY?6`;Aa9(Gmce?Eiwn9Y|+6goc(=v}#!*VwZeSMN_ zfhLNtP7F$>VOnpzK>=^3dB~8Wi?&B^%50mjH*|25CvNavVUQX1c9KPs95NxjcO#U( zCIlSu^&cHElCt^{mwxe7SWpqOS%JqsirDg9fIp)!Z`pbu{P8>DdH*2^g)br{J7tj4 zpFb(xQ=05xCAP}El4uaC{=K`N!)B>J{UIRgnRKOAvr0zThG+k;E*o2ndt5XhDT?`k zAF2yw9AXLKi6C3Wv;#8A|7i~2RN@Vwq#^E^^IxWmwKRmUj%gUs1E`PPl%8PglS3n) z;-4;Cb4s`d)Xbq0Q^4RfpMbO*>@f4~2K&8sTjaZ*esWi)3MjrYwzmm- zML#eU^zj5}bGEgEs7+X7bq(|0!<`)w(lt0-Itp;~jkeytY$SEDty>wP%&NXPs+K++ zqUGdR?O0uIHXNnb_Ii;^>EOg1@lU#~{o*!Wtpy*E)&A8-Ahe0x#`Y*lf$nU}vSMY3 zx>M*l2;zBaqt;h6X>)F)o+{&Y@7%^n2O@Sx+jUKTKOPOKm*W{2(amp^>=GrgkHstW zaXBsLT<88@o2{75)GbRaBv=H1*e`q+J?~ap7u6w2kt{$Pi8jwd`Y5li6rM9V7!t>y zoj4L8mtU>R2|mO$SM=_B;m+wTLu2R?Z$Tg^X#%cavN@pA4iZZJRdlcvU)lU35k+J+ z#=*P^EFaf>_{tbKFUbWBKOh^g6+{({;rL#712Fztl9_=H<QIOvEkJA_Mn3RvRkaoeY0K|iOC{a zzwDBwI6i7;N=FfE=lK24#^CR)D47A?w1v(1j4}RY+7!v1`p%m(MVE4}tp~T3@4w6q zp|Eu;c~N{ za#nosmH5Wd(OXj16iKms!i2f-klynNv*voa@#yRxbvlU%xlz_{#8gu+s+Y~!NVc%* zB>}6)sXW@01HYb&%?blLbGhqn!Oi_JpN;YK5|0YPoY=u{G)Hzj4$UfFS6z1O9kQD55_4{wC=gFGl$r4~G>%d5AUFn$7m&Wl9q zG&0-T8Zu;i+t$_clv`H}<~d}%`T@Lq>s-(PkWiF{CpxNS*32RypY3ceH0w2;S198E zrtyuV+a0#shYZfv$}^?B@X7m6SyFS5_Hnx1!o@eX4RAAfpk(8O`^=UM{gbqKd#GW= zK>u=K-~Ch)f^(7AnVj(Gneg9U88TtC8=B%ZrQ~0Yni~l|k{l4X*6@sO)x$L%<_@$L z<+$-nURe9b?5w1`LTx+EH>zzufY0xl2lO@iORlcJLms*(#nvN5^ZW$3=TLUn!yW5B zeFERC&1<_*X`1|!DaC7F-M!omCzS9g6j*xz^eSzn%mw$g^Xo3{QLwUv^vN?5}LR91eVqH*fm9^R#g`=%ORXmV7Get;qg^?L0X zWHOPV`j(Uo+W>9zUUcCPoD1Tfb!U$^?2xdd^m0t2!|EgjY!I|-RZ9< zj(2%_)oIRkruWntj~vmcb_I{yw8kBaPgKX~uXX=Oy|c2#B1?Ix+gv z0HAPvQn4w6Ee$xC{VDVH{egZ2;52p-OblzW{K_2?ml&h;5H3qP=dt93N1pMGy&WlxTh8l`Pxilt=Y|e|C z?}!65`Ig6LoU7};pxO#hb?R{}8o#PCmL{Cn6Zf=r5@`HrDEV64w$|kEIJngD^uE;h zxFgklxkFWLA8B!U%^7GQNWVzdB$XF?z0sGej5b{Xf?cwQGVU&X?vEhd6+B1<4 z4?Slhmv!R&emo$CWh5^{3`1{qf$&MpQz_A4$hm?GE=|JdqNrsehm+Ly*IdxL|2SmG zXgaX+!9lRtV~q*%9qGhj3l?n_`<$~{EwyGXiBPZ;n5WnE(!1X$n_VlecefdY4C+ne zhKz@n9Y6u;TG=S+ROMopS;iKD^ zY!+I-K4>UHI;<$|-zcX6iybe|*fN3(qMW@}Qo29X3lgG*H--FzbdZujc7ut60tfgx zl>ooET`ZGYe>>n@rS+zi4R*f36Bbrm45dCFn8K$nP5pE%9ZK}NqHQ@_+ znO94LvOTMoRq~%PyHC}|400SR+-1yo4%q~4qz+PD6`U(TrB8tzJFJINT#s)Rm&z}+61MUj% zrvnN_ZDL8hkmhoG6aG&l?_`Kfa7%jd+E|bm8iM$~L>EQrvj*cizTt zH(DN!M|#FJdqV&EVK?yl6uGX7r=8tCBUM$(i|q1|H_nR2cb5zmU!4A zv%?a-Hy@n3l5?qdUH*V~v{{hF=@!I(BuF_`jxKQo5lHhoKszCTB`G6d0J-Xs`<&wH zW~+#a_q&Dn-!b+Xft)XCJ}vHK;Vx1i&gb$3-k5%ZZWv>0C4A!2xHPBIv$M^h2Im{+ zu8(d{S(@+8I&HMiXtbD^mLiSGA(lOzPz!s`fpBC36L7#D=&&rLc32aV`?Wf6-b*4A z^SAMEGxg`k0;uPkd|RbNHHODv0X%9<*jE6ip(9ABtL$=z%oC8SvQ`_5i>Y|eLwdH4T>5m zIrAwR#*mt2+2O^=3$dZ}P{P8@WEeMjL2rL4>Di6&OzZ3=E1~1*w}(%Up*xnVqR=|u zwNxfkxh};c<6J+O0dOuPafvp_^|nx}IM=m>kniUg^O!=k^^vc%qb=S7Y~5Kt@tQo=3L`Ry+1_dfZ3Bz9=lJhgJnEilKNr=IUS+k{>P{6L4 z2+guHEr!2?qf{({&LiE-pw9%vV-=+OWuOjx!bxqL_ z;R&1w>v-v2!2l=!DMsCfg3-IaQsR~I`x1Zw2k3n}@W zD(ZIj&-?n*!elRlrZFH(6_0q`PCNSyi2$LomL%5~I%{p2@xFx?yE+2{^M&Z>fj*5L z)wsSIS-?X@X}}aoxtZDoP6DqR_4CGOc@mE4K(h5WHz~CbHz{9i9b#Ak-fU}WINycC z1e^cRsq$ZoaZ4V*BVbx|d-N9|EFDK=B6Ogy10)Z=CYW-`BcUIw0{z|9)69p9+6)M%2S? zwDoPd+ewGIb8UN-U}kqGMSWSVGoBBFY1B0qa%l?rwd|iNu4P~xjGF>!IsBcig}#t~ zOyiFwBNd}CZ)V%}8S4G@h4*#+C@woW93mQ^5gz?=C%1dydKABPx#0BKh~#C0pb@U7 z$M|%c=fOCSLS72LMuiB?N;)6U#gtshkf|O$qoHL@7kyd~c}drg9`Sk1i+fQSgVuP| zrA4N1;7sF!Q=L$L%?eqK?0*ssYI`ix=tLO~5xM6oAGWtO3>k_YI#0)EsL73iih0ad(!7^$5V&cnjT5EmII^@8BA5 zM$m9zsAG=%>fIs7OJ>9VCX&2O%X^j#@x=E0$o!Ik5SGd~>bGQ#mi|n~Fh2>tce0h^ zl`C6hbVn$&s)T3r9Wdh57^?I%b2NL4L4nZzxDnq^+s}h+QoCa1uoU==?lI-i8&ooj zn9Up~8}b7wK1=@a;6Zn{+?6xo6TW`>DfVVzAWKdu<0{vR?`2_djHo zq`@(7Ow2iGHxN98dtxepO`gOPfoRCkwK|)NxdP2bY$Iw6`xHfu3PZF6Iwp4~Q0e40`{mg!=Pb~Ai;ziB3FYudwxpEtVpV4s&4=plV^ zG$PQHA(T{*>@Z!0!hwb#~_)`eh2L& zLnAT9!OP$5EJVvSDSXfQRq!)VN%}T>a9c;*TES(So4p&&g&cJs5RWvSY`UMo1~o?4 z)#%0s>E|aWTGiCrT#wxr3hC5xwi0RTDLoZ7)^O}}%>wp3OJ3F6gD`78KJWe~wD#{! z2^@I|;_ig32-B?E8w)GNICNh_KR8}&PmIYTc3)GsT&TVwU@mw9QU)$;q|zZy`v6L6 z2=RmF_e3-Y#@jxKrSq7)taw3+c>Bq{EI4pwZ#kMVsIa_2eD08sDMMV2i{-q_&7M2< z8O7ViU=7k5mYCrh-K70R`I#(5bgKKX1o>?2CUBBX1Wb{KlDZvId3O2UmBp21~ylY0a{Z;uVIZjte z;H5Dwab7fijh>|SPL3o+Ns>=Zm#4z+Y@uvt@Fuod=;HW;HfiJaNxToy-us~FuC_e^ z+(MoprapW}hS$45<^95XQa+^hu^=Ka^EqVTu#1q5+HjwZ;xs_m;hw=dI0L;_hy^o< ze4dWgUwgPSgYqA|S;5l!*k-K+IPxgR7@fCzQooh--018nIw@-Y#rwsoU%_#k$yJU? zNROJ8B|B8Px{GX(2FhA2-7!ETRgOt8?BsC17Hp(GI6Y^gzIyT#0#TGun3r{F^LTd! z(Un1Vc3yCjisa5V(#&g2Ew@P1p?fuNw0-SvNI`~GVkTF2jz-Llm${?mxC(#{zG{gw zv8G)G1p`8B-SudCJv$@k^={H#_`6l%#q!1?hq}5yVYTQ0pSbzuh}M(SS)5K!vY3)? z`$7C=gEu{6+DAugB$MkTN(@AUToi8ARh#*Y%=t7AV$e&gc2ssm-HR)&@`;VzXXF}! z&PU)1gJy;tsE}+%muD4&3$6)$aoF-r$PnUcuHdWna(kVhZ91Rh2XS=kZ7(u!ML--w z)*K6iJSodje?S9zCEuIO0mS~GE^P{pjWM_N^G4a=W{<+1ddU;GNGRbm568uSG z&xjGC@>2&+>1AKUl0LayJqoPkKG0|g2uW*ny7{^tG#Vf~|4&gpdFMY|_qFbqzg>6J z>r+uTMKwA>xRg^X0hN4uvj75}rKN1mya^sISqS_YJj8wa478^XJ9ECQJ_*PSU2#$| z>RWrDhT3e&-7pEd3zcSj^F2oc$NNn~gpshxJl8)R%w!;tuf|DGzDZJLl<}7C$Y$VOl zi_pAzx#Ckr)V9x{%d+X#LlheT&dAd86&ABsE`*5W>+m(Jetv<-)0E_W?pzqci7*JS zU%Tfcq`~tVcr9Q^xF_61-Lz+0bKK35*#uw2LBXrdx=*I{Vs6-p+b(ii-%7~O&M21U zc#CRjdu=IxD5)@7RFEl6Lt$BmRuGXZ!zJ7mtub`%as5|wkOkR>9+XQ|hT9(gWtHMO zH^{)(F778a{Ri(xX{z$Ah7gLEXR|J8CN}AtJOu_(3{EZTaMY+WVW6(C_sr`X7V^Psh}H9_$Y>(iV2O6-@4O&>0eNt z&@%D{Y!;mjb{!ui4N(H%B!kaQgaSC$8)M&wb(D(0z%t|LQJ_VYZksD>Jg|LzlE<%W zH=nJOUK&VNddI=eZs6MWcePt(W}S|CuL+VlJ1sYURFi(kMwFK0w1lt%k@l9krn3S`E{P=&%hR0G=rKd47ftp8CV)N)z3yF-qX{4`yzL zqSoL-gb96i%Hb8eMl6R`Tpu7uf{!V;-Tp)h20I27|L|fw&rTfIboyw;QTN7(1?NyO z^I(Wqiz_A%aEt~C%O}8p?|A`X_aqmae@#|9fiZkH9U_ip^vZN z6hVc^EkGmXDA_bR4d0=~x5^O4qq=5=h$mO;73^i7W zl8)KC=egZr>tG&Xg5J2t5Bz?8Xe~kRyD96&820@HK2aTAU1xtUjxUi(>Xxr9jia^y z<{H)Vy!knO5ju+V&~F*2*c-gca^t1GHk?+ls^QLK>QFzY6hT|SD5ps0=xWFf?Mz(v z)oBV^ba9HDKru+h<#EB6jddC}Rl*Ew(CQh3iibJCcf!I;7_z>59_k;?8Zx9! z-%WkBH)>0MBbSPUJG?J>DyWpKI*qH``mjim0M4=MUn|3lHyzw_!fep92;}<=)xNVR zF+7<0#bBh1Btwen&Kt(rpk|rS)z_(=a94esjmi9KV|>N1*$@jUHfZjUPg1>TgJ6S% z)eg&WaVlt7hIi7K+eD{U(AG}%fPA>BroT(=|HT8Gd|FGSRRwWVlJDI31C8Er+<+F@rcq z`DunpHqOT~=&Lu&!T`2Nsr?@*M&9_qI&$b-{5K6FmpXJX%nu$Z^vWTI&(w58^YI0S zT)lZ5l~emj^3jKH#ugKPF3M~Cp07&Y@(l-TKvx=~oN+Wx#wrZ3F{VmbqiVG~DNX(9 zR=%y-9f^&gWG#4w2)eJr=2EE9Rr&4GNTFEwlIq^ThV}G@;*vc)G4W~&dzZ9#F*(aY zZ5zc=3x-MIA@eGrrQg|&0Q+99#yPpq+H3xRr8C9s}0b}W}^)s%@gr2nC)m+K?+20(z1M+(k&yd+sl#hJ7S}in4 z0W=|s2D{OC%A~VLT;5riEY)!;Ic~5u+`bON84^b3+^7AK)nR^-ad3ki)mANpmLi?w zD8LgM`c5=3zeS3ROU0+&%~IMBkOs14gcG2NcxW6l2yrM3OwQT%v#zD%)l6!~#>G|f*U$&Erw zzRIMKXFCwxx?hsofAn z1DQ05?~P7hZzmzcJHp0kNKJZ@Pn_FA*L)chUcqOPTch4~NjY9@M;Q~#v`z9Dqjthf zx(5g4q@20jB%Gk(IW%{J zEKxaElfRKw*$Vr009#NicMg{nH<@%Tuvv8Y{QTD1owDzL(km4!9;H}Ow4bk05D%}j+fbGJX<}OEMGmgf)Mc$5 zOt*24taN3xc*YV8J!1n*%qipFC~hf0`Qx9>oNCZhSHta6kBtDbT!I ztv@lwyg>b0xdzK!{l-JxWo~AFPD!cUcI8LQA=A+{WZ9^P`q@weX#+nU3N4+G_}K?r zusMFU!$TitkZF>glrgDJyhaOpKZQ7XPMl5oPY%P2Bz#m)w91X@H#{ds6z4Tk!guVR zmMFDN-`Ms51i?2+snoOoORueBW*uXQx`Lt4gT9BLG;zf`d{j#9N zU{fp*J6k?-k;=POvmx=NLjn|YeTYkM{Iy5QVwR@A?!*qHe?x9QWjw}fx^%bfQfs#m z@PuWQ_r6$LZfb1N8<(}Ji`6W4BSP;Wqi^6Xu9reWeu$KR| zta-!`G5 z%buBs$>b@Fd zY4A@Z(~>`YR{CM_D}PZ&@YbG_KJZXO__R0pvr+zxBk%5^kzq!e!b+C`u+frchmO3L zXyXjVUs&q}-A3qyYOc6a)E zXW8tm;-Plz9jJ4P?Wmyl&C^dEw9Al_NNm=6j*IvEkh#_@I7x7h5XjE_c7#Si`a&AN z0MGWipWRU?dTB-g>l=x0$Kt1%yw~96s*{Lbs7Bmb_b|dLPK=@-iuyS!C}(+nuFmP} zmoQkvLm@}_T&=TiJIgYFt^3S5wq?R!^4Wwtcut!d09M~0*Ril%>i~P^<#E+LP#-qf zV+C0QS$18UeS%L~x6!mUt6L@r*%Hy$wg>aPbiu*Z_X^hQJ})hVG{TkybNShh{9-2K zrhR@wBc?+eb5KB=3&phJ*yD2W-pC zMB5P|jvZXfoBBRpYXsfrn|6lheu1nN0WoR8=K*SFMtrF&lc+BK9I-gBHVHw!7Hx)w z@bg71*mAsf%Y2C|;7A>IWA2bVVs}e#&lv;iNUv9n-VEG}S)Vc9(AiEO=T;Nto<%EO zkw4v=K4FG|N}bEbAStn;KMbr15<#vvN;PEAzhn%qhQsO)WDrIf8O%fbuYLx;mq8%a>X{bN_kxoeDEtn5+6tL&4OHXeo&_5hDCQP zr<-IiA9tSL_M3g`l>f7n>>mZL8L#uaI1A-}vv8=0=mSLxI#j~5r5Yb|${ELh6zc@` ze6{0-%cr$(Q76y>GFIOkHQ4cl+%Wt^j~6^9C?Ala9Q}};o07B>=iX{7bXrBq$}|Hb zR9$;3mGG(S^J-8ouYQ(wQ%KOXu{?Tu#@8_yrbVc#ElNIKr9<<0;fayg@mx>I*PyF^ zh|}w>EGaR@FEQy`r6l+5?DE`H`o3`MM!D@kg0%w7KMCwk}D^KjI|55dV77@r%sm z4%l{(?K_Ezc@PySTOu^)8pRfms{)vf@1xu(_vi~Yp& z`llNQ@so`p4k)FS5VV@dY=X!Qn|^M05JW9iet>#L6n|cR-K6X&;i%O^50%i+J31^} z*6Y|Q6u@OP7-?7~JI`P4fwhyEMG|~1xW#HqM(!IW(&KuOb()|x9kel5_6^Hvg4~;r zj?Ssf_FQMM{j&2*X533B7i)497*4|-!qU&2S=Up9#=Oidf#!xaRt5T3L zti&^@LU}n)-}yL~5yjQDt>zY1w&Rv1J!F$s7>dTuZqwN9$#%#2ZP2V!?=XX1RbQfS6c*MKfS_2!ng4wg~l+d<1LMrva!XQ-Yk#U;J>Zy zWq*qiZB=Ypxu)~dCR4yR0Po0d^*5GH zOg3@07M~PAf9|LSIY-vOOHGDl@O9IHSShJjkDIIwp#sNPGD#ELR5^p>$d88=;Mqm2 z#hBo|BIeqZ>GZbpLcZ}>!M6Hb!Ol~0?9^luz`LW;_SqbwB$$^jUPKBHQTOn`&(w?> zYR(^nO;7P!k2t1^W{KG@ zckohoOejBwLB{U;le5Xhf20?`;3vdq!Lh4J{N)|K5x5fgHjWfBo2pk|o9<{}V$HWJ z0wwl)eA{m?#mFT+JiwABo(@~%Q1^wu(}w#;q7htPK=<^OYcn6iB%aPcexzvKNk)`T zK6U;U@P#4yyU(A-Hs`LuE0f))`n1EGk>tNtd(*)|Kd2vjyc_XpY9BW@b@bQ2#`s8t z2a8K2ZD}Cn2?BkRQf6-c87KHd&-Q*HT?o67JLuKf1F{f08Do=~NN;+o<2nYa2`*s( zTx0TnSzumV_ZNdQ_+}=dWqT5#<*s$kLM#cw)$&YwP*!$)(76;d#ge-E>#Gjy_#$WK z+}2UL(_B{a3by`waa7+S6Gek0qvvt|47};1UVr2gZ{O94J)=kSA7npxQ`ma8ZSxJ) zYweEUk83Ykyt68~Z+zM+W>z-HRy#=HkF0jYc(kSlGe1Zll>`)%;tgzv&`Vytr$-3g zs7%tl`KR5jMXRy-Yj2QyY40=0CDa*tLGyUkZBxi(4}U=y(ifn4UU|plI@~D?KXYD9 zs^i++bn`w|G$5Y(Q-EY>AB$J(IDd)V3^e*}Sf7h-@@D$I)#v_fxR3(pKJrzr9J+Nl z@;VOzV*e%X6M*N#Yy0Cr&j*O%Zw>rK*V?G;Z}x-Lf_>Cyhpp_Hl4k%#1>dM6dDahh z8W;qc96Msx4q(~o@#6!sbU*0v*dwT#i`+mbv(UY4EJlafkme}2nq@6bb-CLf^bRu` z)@AZfruB6^-n3G@Ej0Y=z^MqLpbnK+G$^zdH#poyUtXc==AY)2lz)&n_x>2fDl6)t z5-EXU)Zx~%g@2`Q7a1m0hTiu!<8b94aIup2}dnVyc zASoj{Xf0q=u|V5pfhFMy0L}maXC>czaZ^6=&`d3t@s ztChcFTwn_iTaQ&C)A{_gb#L#AB`?h+T3>WPrLZ#Ep+iLL?jT++A60FbVyiO0u0;b& zryt$%wbEfWmF1gvy87}h8tkwt3lzobeo4ovJ;WpcX?#c1<;r$-ECL_jYnW5@o z#MkLc^agHFfpuA)8V5R(yqG$0_RAL$U&cKZ9C&I$<%9`sGkI|2S3Vh68!xj}>ooEF zJO9S0#mxS4Nn#NefiP<8!oE5w{^P=*sP;=B>u%Qf~Fs@H{D=l zpgO^GU#Xm4RJ~9eI2}o<)|*u$c3^%j|9lU6z|o|z{$_J3id@L2$z4jLhqRR&@lC)D z8~PBgPR3+9n~U)_Z$do3D|cc#p?Q5+Rkj8H3oA{0oQjMr(#FM6%inBiB#4Quv>$7UzExir10Kx z)nu8PRO)gZrBB*(@JMU*m9f~Nq~e-!I`&K1vl<`wxp*WM2JOfV@b|o6fv#Qd6T>Yc z)04@{)l1|}UHcM#Y%&^pWVPDCVPXiTrL)r2+553-kZ8wtO?w~(1C{>#mk%c3rdQ-I z1faZ;-?g*{_Mf??rk`WrZq;Pd`&(GA@y3>>EH_B>n;1MEjhDP}Yp}cz%r3iB{aEJy zBh_*Tg%KZt9|qolYkd4!=1B=jU#;S`T(n`)GRL(##aP8;iQZWQNZDfrtK&Foz2>&C zlm|i9B>p}dln)1CqpK#Woq4+1PzTU^b06X=D}09a!#)G)&#U?i`W`7D0a&{A=YEg3 z))uvb=g_wv0{s7^&r0g?`s|3NQ2Mt%3nt3&qT65=pg~apMhVjMa|zNP)dr=GfFB48 zVdE&D8UHA}guGupML64)xi#Wd} zyuhn=GVO*)3kp?yaQt?`fzZkk#t=VNYt@_hQ1q(Z7oMw}O$uMX=yOoRNOB5ulwiWN zbL*B&T#X>maPL#SRsr}hp4U=%e$_3bQJB*#f;e{USuNB~+n?z9A?+BgD8oA+!hDaC zl=~07)oJYjj{(`+xp-oiGL$9dt!4IhTNDvnoEh(<6~Q%M&Hit=YxXBLUhbP47ta?) z!uQkE$yZmWKJWz3a*9S&y#p5q1mX9*Ri6NDW ziV_Yn&i0q$O|(E8HAL7JqZ1t)YRwHLT^49ZgaTc$a@#6kgDG6klLvLcQ3i@=59Ij zWQ<`XFF|SL>gKLrdn^rULiw!a7hHtM%l2ezFM@}7Kj%?i58pXDw~A%JN|a9RBeMh! z?B*^(=8`h#f7+O|)(_i|;dF@d=nrd?fd5 zcN|F*$>%?jn_UeC(0+?Bdb$>%F09J&X3rV~y@QzKqRxA+zE8VG z)Ov{XlkoeT{IX|S=FQJQ;PbO)T^-|nPNavuva*&}MigmYk?lH~)Fx06c84Dj{;56b z7ikxV(R5Pwb}bK(HwD!O74{LINfN?!ktO=piVS6!Piq=KVLpGEdbyC-H!O9nv*W_n z7rx4BJ-JqZDw!r5f-GydWhEA}PJ~xk`h<>A$b=4>Q=e z#qyQLN_*FW<9nu-1%q6S@xAc$U7#MpAR1k%i=;`>_+3%Qs&gpVCbwYqXV|L@Sn2X< z4oQZrElDwcrM!&zM&>~Xw2y0xTeJBEZx~rO>XGs|&zcm% zqgpz{*gSpMG>G$;2f_q1A)tZ`sLLVUGU7-6k_r?t0E!Otd4m%-iU%$rZ#{4hVxW#@K`N=j_nx+Xma0fh;8H47HMtNbu#UTiGo0M(UWx6L)FH)w8iknriX z-qMuN;-yC6n>zEyY7G9OY^|4ptDcQ08R6W`i(Kkn3w$PxeVzKwy?J+lJy{`T@tAX`N zf9&BbW2>M_l1G|tV!SNbYN}E7h{O5Z3rnSohDx5Q`iPYVALjBC;kUXUsrd6jNUo%z z^NLX1%uZD!Dk1uOK+d)IOzuJfDDt7@yl~pBZFox@k_pONfC!$1t9rbc_6RcNfTgD{ zPe5JIW^CzX#Efq=c+JqQoq3<|;^2Q)q&ln8J)6gOdCqrR4=jAXoWR3or|I+yI=>Lp z%e$Oga@kfQBsYGXt#f*!Ht8)|*nN+7Z_a@FfhPk^jNxpYVSRysKs7zkBUB_9~1okb!ewD{Zxq zwqi4h@N#nqc5~ZULyJ20M$ETIH4M&WB{kHxKRGuVWX)*rTDVUG$UuqU!&Pq(h6Hq# zyFaFSq(AcoY4uvvla`6tuKtvB2nnhBpzX8sVt|2N1G~!$FG;Eq+W5fteM@4Jce}}O z*yw}UCiIv4*1=sfC&zcacOqk57Rz-pR!_>|RW>eru29lPb7p}LJEJmRbvFBso5&9b z{6FlycTiJpw>J!mVgnSVH&N+LI!FMK-lY?&^xk_3h$y{7K)UqayOan>4Mlnf1rllq zEd&VVjrVi=oO5QL`#tmh`OUmD`wxaI$==u6*XnEi)-c~>plSb|#z91BfG8zi9m-on zfNw&P`?~_hRQl$PUD9(--{a#>xOheXxTxO+6RfxW?$#ahDhtR}cG>5XP8&q9HX@jg zZu{8|Ons?b;GE;$gF5FQtc;VU-HY6+(=X1TNx}z2`3%#ml57)@kHc`2@c|8G5V`XXI)<|w`{hRrl8IrD-5>lHIc=Qbb zJG(h3;VJs@e4VjC2wSWXP5hi<-{nmsf6{?h45yYHE*-_c8eH7uMht4m-1-WT21Ut* zma3EXV<%p!ntNkmV$LItC-UlbTqIVX3~mt@`SKmVB~url+BwXAWntp-f~^0SAI*-( z7}`oaF+J15E#$NEAPX^?-iC8@$AY$>v7~8|DH$rnxK3wS0A}bU9^QXV` z5~k42$*--3lPLP^9scNB?Ir(>&0H|15b&^DFgVvzAQkYtk>Tqri|;Q2o?(M1c0We* zWNQzGC3mjY0wI+JE7+oY1!RG~V^3tDnJ)*^F_0dkdow#=F)ow}-mvRM7xStzk_p;g z;)G3=q*V8vijmo5Gt`eBZz&)Vl;7444(m#Hct6~wI32Jy{%J(gflYnu|zwu}#(o*X=;U>+t^VRSqCmEBvtHju*&Jf45EWn&$1f0Amdn)V5(; zR4i}Q&zA<%E>l@0w_op_T3D*@Rx-&E~K3y(utOl|FS~oN#7D7#WIUR z_`RYmU+x2)?Wd*1X{h31Y#`#JD_gHQl{C=mNEBx?%JTE!TuC!a4)8VO&D!@uBGQ7) zq?vw+V;x(ZsOU3`Jq=XAzHt9s{#;A%=!4Ix;Ws=<*zQ!;r{9(^q)3WSzq-$)h#^^~ zp-NtWY*i(I81o|g{Xk^#Dc~ZiUa?mCxiYH=nGV|czDm*y%|1+x^BU;#{QUz1f{qc9 zl~(#AEB>E77~k?ME6H=Rn8?iRM3~p=vbt+0-dH7c{8#Udp%_{U5qh40siAT8`2`Fo z3$GVrzT(XAdHcmp+l!OQclBVCGbg0o?c&9YHSrV*)a19KV%(GfAWPId-Vpy8$iK34 zS^{{*QxG|@gVr;q1_wc*^!GF|T)yU4IxGXB>}P(HtwgRQ&~oT&^y>rKw?;rGL!R2> zfTpMwwv{sbNJ}aGvvdIpS7_xdorP?QLl~ctZ7Nkc@2k!m!IW>wT}hKB`3J%t*xTgawz|qmAZUg56@Nql;fL7X)98^?zHX=TDkYh=d2(R79d3na|X- zEBb$O4fRUV%!p}~=>L^4^f9V}Dgftmqa?)33>J1!Q9tpw>V~m#BXno_Dv$r!%`MTK z&WTPDk`9|85dAoHKAL;O$Sm~YTx;p>lglq=%3nuS;2ng1pmd8CinyVQDRBn#)uX%e z=U2@^Aa{;Yma!cY5pKr&lki2Ro!~ZJ=RZ!-A zY&p4`q{1Jl5fFRz8OCK;`-ug8xvE)A)WCL;XwN6m-{oXBebY&na+Jr$54GpZx488$?zy!o0ZB7tH(s&des*Ex z_2`P*0o_;N*@~gK-}cHqn@TjnsD(izP=uwd1ERP5FOUc3|2B$+;@+JH6c~T4Ixa2a6QEIL9Ru5r7u@1-TxoxG zRol?uJTMv?jB5>i^p*d}TG*%^B;MlxK> z%O1Sp%dk9Mz9tByhIFj)9L`S70~V4FX8d~EePt`FwyS>Ni#ToHIH^Gw`O^gV1!xOs zldd-fijbT}Vqz3G;`1{uv|_2Rk2u5UQuAzW^l=xA-nTdhs+gvGS-_DS>)Jqg>4n6= zQQ~1|%bHeid;hc-l*hwIGr7WebToxHG3%&1;Z=IlXUlLs%+mY(N?|jmzf$OgOK?!C zn~^%!ee1WFJJ{1efZ$`LN`{mP43KlYVV_TLCLnYPiFC=yZ1`xa20hIyv7`3sgBe_p zNoS@$`?tgMY2!Qr$L1#XxBVvTh$8rBqQs6M6p1Aw0lx%14c+~BE_i79Ae)N`zQ~)y zy>NDe(f-|3$5fYdrgwW>(>a4wOi!6}a3#)CVoLm874Giza+u-I#Oh1!A?u>bms%Kj zvxo4EpuZD^{vEP)TNJX%pjiXzam+iDHZ6+uk=FG_^|O z!a0%(_*eJl&-M3y>Xq|pE_;jeTRuC@?(KSJLOEJZ%x=M%EJfor`YT&)7hZ{-!0wC_ z_wN?_=Gm6(6ZS!&pgk>ZNbF{vr17j(D1xH}a~7qv?G~20!;BBSP`&v6FjqaM}OsCt^oHr(aC7<&v-*7F9Xo>x8qX+Voj7kT%SzLuRi}gjd&lsDi6nFD} zFoyXzQXr1b#2^C$^An7+#yd&Pme8$pX@hzG3H^E+ zePK=itG$VB88LVMFOhSMF2kr(yKUx^P&J8^y~h?4uI;>uZihGEVAadFH1 zGnnOo*tA7$=zKl`teY?GAJT>Oy8`^#^;PS>`sg=aYvphK_>~E^$#~io`WN-dz+DCya309(c*4EdsARt!j%MuIdAR7jC_nqdOhAP zac`_8R5b5k2Wf1}zZ$KN)&J|NcuzfQBI0X@|CA#2QH9Q&z@%3=Lgf(pq4KEQe#;|gLb2oC*TBe;;TlOl z{(IXqIspt8S(_OR;Ox3a4_fuqYpcAXwC{Y3A$rRWzIh;e@0>QmncwXZI+ zdwUDOwmyPfEGz$#7c#x^{@4NgOQpb$(c?MIVOO*SqK?_f?;z^OkE;osQ<+7wXTuVo zxRcI|eVtq>cL0wsN=66>+LAL4sxPa~hbjUyHvnU9WXQ)NLg{hdnvZ%Bau!>F1q!G`oiNY=go z#QuKXdR%=o4hy=00KV+LFQe*~&5J97v(?+)@>7X>6u8hW$i$JD=CGOU_c|0{yO?72A1O`e**won0?%w(rD` zNDUJF3=3G*kTv+1D4lag@5C>l`|=SCXx_j-LdoKqX+gUJtnEbD&6Q5F0D%}o1uQ&BqzHqUUfh=8LS(kje?3+-&M1rD+6eH^ z_y1{6@2Y;2YKQjc#UE;X!+nfJ(|hkhKDE?@&7Xt6GF~D|0T~ovVuD>`m*TaAuOA4a1)YSS{7TsDfv#JzszvaqYb5Q z{cp$Q|GK?dk>5NplEwSK^&|b$bNuz9{@=s=6M6n0W&Zz6nV?{{xM6cCBU|FLa5*=P zqfUoA{Qn(dH|UbSIuop4^qC^ z&cpot+Cwp8jNH%cjV0FsGCP=?xO+Nb6!^IRQaIm~p^tLe!OQrFoOI9wBtp z$RXu4zPY|(_v#IH{Aj&-7Q7^lut|Owwqyz&oJY#M@Nm#lon+q> z<0j`@CFC%!^A`wUV(?0-zZ(|A5@YnVj(%;)%Dm)_G`=`6esU+eg|%*FDdGFv!0KFR zRh8Ve+si5vs_*F_);O>}wyygy-5<4zT#9R{qrucyYL*?`Xhr)^6vM%>7LZ!Um^MNTUjG)o>w945RF)Ag|kD}EA&>#18R}yd*dR) z0`z5cbQi|ZMzXa^Tbyx6l%=#&ht$s&vi|-G?0AG06M=e-V3DQ{;3&uUC*jbp88-qb z&pXbmNAx%YyZ6QT6`rywQoiR?!BNk9Po&+_diobVqjxAXTpCV4GXGT$td&*Rc5|$4 z)LZ$8dL5^_7*OG%F%I`sP zZ#fuOkpiv|LgmvhcUzT;cpx(SBCOzjcKwScf==I4!%zI<39a|YcjBcjyZP4X>nwc_ zewY$qM3y#GwK89Uwll;k#7}hS_l~NrK&9I{S{oMusL1$VWMX&VAG!QV^4>qix$#fe zJzVmqEU%sK{rSNDhezHUQ}&ZbVsR+5e9rzYwQp?V9O?%)se}Uj8b|cZI3miPs@IMw)^+`Z z&QWcn;6Tr5>^P7N zh347#++YCrcajn!JKu9G5eOQK7f>nK4BU0l48LwEU1UC44(d z7Q`OF4BdyvDegw&)j~{Q1pH!9)|^l1#*UHX16Bh0~wnIuGNf}_t*ebZ*bfS9=>i3Y*lkLM=ORUw_UYR;5e35(0BpcESv_fMt8h&gC=6Sv_8dHNm^Z-CC@;UwHnYfq0Tl!d zf-K0u_*rB&7YI(h`khk51xK1lyX11pvj(D(jT&1qjf}oGad{T0`JJQ_)bJ_lCjj9w#YZsT@O8Lgt1xMg+x378+S?66GQa1=1gbbqC z*sgc;an<>R88&>aMxUwItJ2!cV%Pjc?HkWhYf?^pt7ozg$Je1{?}mwAOz6$o z@c6;tmUJlJY4Qn+NbXXm)$aM{>fx;Pl>>laQ|9o*B0d6BY}nQejOKCN^%e0oDK+te zbuM?2JM57%Al2qiB88Zuy6%lmGrn(8cMxk0Y;wU;ANv1+`dLq(?!C?^j8_%UHMZ0% zk#B0|1beW0By$glwBs`Mta&0?09 z+>>&5@LuEmb-kOpQ*EO53u3PZmug+!mm4BSmWzBB*{Kwo(F^tITMf93=^5emxPIgF z^uaH9CjEB1K?OKv45tJuE-aSms!U58B0b6F7FUFCETpq5`Z@p)YrSKb$1XxM`E00M z3P(q4Yu@D%2wiHiVoz-buDYf$Z1ilzAPB&{PTfLe#xD3GvX7YYDTUe#`}axC+Gu}w zHCQuXc{dPyP#yktP*+i-{bm+z=0-zr58F({$)6%DMU&`8)ZoWSnV&KbHQUn}mg#ID z_G`V%B_zOr&uz~aM0`{QnAA2L3rl1j7P#g!ek484;(hwQB>^{o36zf&0$z%rurSnr zsPNza_;P^v?){{9uN>~5)pyyC;sD;eVNI#$N%(^W#NbA#`#v~WGhhS~xG1GuwYTf| zO`EncTc5;`(f8AYlLF5r@o?sLeo3^gq=K=*m$^g^etmHM+hXon7IC*j=vbTU7e^x_ znwLQv_$6BnjFquzz$kY&)DiBWp>b8noa$iDBB$b_K(p3iparWNm4mH9ax~Ekbdb8u zf^E1Kmzn_tzghfyDG}3khEwhNH11KOpTn|nT5yAz@BwX{uE5ZG|;e0-?AreU5cVLSzUenB~Npy8;qIKygB*bi#HaLyum z!n9=FMe=G%HoSQR3Uio;E8;gJe$cs4$_(W}S?IO4s)u}L;Q-x0Fn~I0E}%fSJu!b> z{u0px)qoX`dj;=k`ONRS%VMg>ue6*%1|5fWwhOg9#$JJydG;pz>@5xr?FfWJa&|OW z#MS6yheIp{3$1G6sof-?fVhf*jkTPxx=qgVYHgTJsN>-oWWp_r8Ksu$q}2mtm<@Ub z$B7xLfwxW@A3E@Ur6$r4;1Ds%gjg@#Ft&a9OcL=Y5~38R;_&`ByGSI<}j6TjZZL> zAV1{A)>GdrXLqFqRCP`Nbx%aW_6`C;FO1n!DASdKW<64*;QLOi5b0%@Dg11`}qW_IWgW7IFf$tymk+m zhgYyVw6zBDx);$>Gegrtg=+UQbz_jd-5X#`HpFytzCtro!42MUR9+<9q0lgce*amk zwWrH7ln`E}>!ysNaamiJldE*5`okIjoo{gPPDHV`3j)I`h5NT~!NE<aeERk5$f> zzLxE?hSlwr(o2qGr$c>WCTbqNi$AJZ@9ULyICEHkmFT=Arqp0*bLw=JR@;|$DOkW} z@zsk#w(dGw{Ckb*8w`G8(#~-x%kuC<|FBkWkZ`cMHudc`x&@hS0^b3^z(havmv0vZyqw)wL|PxViJ} zGWPs}Rz+jaAm%)v=gh;5K!kas_wWYjkVs?xReuI`&MzWNF|485`@>#}jhjO%{K_SX z)M1TnW1umQLL2^M@VY$wJWxbJSuro^`X*b9KK!xld8%Qf%mT;Qvzf83z>TjPVNEQO zi8J^mb3C47S7001d>B>W#3;V?;i8MfML}N~-!(An&}%BK?wN|($?nTb)BrnwdiOM3 zuB{p6{E5_Y_(zn0vI;oW<9ap4{UY;0Lx-Bkufs$Fz2>XC%1dMG5Pr$$f&5A!8sF#5 z3Uf{_b5qq7HiOk~!u0KMFNYOc_j>~K+Zv5s!dogluUQr6Mj&6@?c4y}tdFSaoWz)a z3{A1H+MoK>f{mEj^dZ8Jv_Xl0< z{hQvPKd(GHnwY#%;OCkovLuV3cbJKu@Or3~rKEmB&`xZ>gpeLcpMsNFU57I|MLP5a zY&ugFr1Mj{Y18KZ^~U z`AYw4nTHg%1d6)S)(1Rit%;O~j%{k%Tqg5LYx`PkbL}wc6~A!pmzrk8Uckd3q{?!w z`(<%uL@Q$4)i$FyK+C}`oru(CTX3!Z*>I)@Tc1zBi)C{%mDw zV)(LaCp$XrsM#WLDov~Yh?;&Q{dx1KGpV82XoNd5P(QyVotH9o(Ct01FePBhK}ft7 zt`~W%nyoq>!9!MCTu-B{1Ai$%j}B$P8@(^P=#%3o>Gd|08qJa}_Reh06>;cwL~Xr4 z1Hk9eCSI~TQa17bNwdT>8m(m!@)B7Of8(Z8WCQPbp)4mtD`@(F-C=wc#X>m~#a5CB zq&!6he%Tw*p6?Ks&m@B*v`tn!OUFEN?7je^O|yL9!d3hASt{A&`Bxh4?iUqLDvSoy z_Zmeb@5C>`ilp_fPtcmDg|8#g{6~ga-BY$@s1dJRyFJLF+7_tMH4@UCU(IpBO zlc@bLcKH;-2`@1psLpK)A@FTGz68Jm^xsMH2Xv*?U6E8pid^iN3i=<=5*TDK)UIwc z%zkrX+G-Z{a_!B(S>>oPG3vIc%CnbNa^aqs49UnF+lh44oC+2gbl<->YRJCtySGC4 z_A0k-c|O%AB!LAz2}bkx;*Ae}B(C7Pj)7lKu#@ni7JD7MH@gFDvF2j7*1`aC%S+M6 zhFb1mzEn$BU#piTfbL7W05SCAjaYdN+?}%HyIEEl{GYAtpgM>=6P{66pH$|-xb6L0 zMv(1kkOK{9@_Hx0=h6fmJoCm4V4P6ECOdd(aT<6Q<_m1};l*n@_s2(}Ag4R`QY0%p z^Vb`nIT3p|{VW_l5b<~&J_1aN!q1rQTgDE$B`Wur??MP8ywyPB7Q+xFy|7G`H=sTX zr? z*{gC(^3mW@Ey$G-4sVfBOO%6)erV{W{N*)!vCO-k`Tldj!QKnVM8J2mwm<{VEVtgh z{AE8TZk^i0#@3xcCuPM^Z;(gc*N)n4*S#ZQ$IyBd(L#u#Y2-P=CCy;Ie26=E zda9vojrNcC^go@fU9I27XBmI6f6$+WUE&~~;cQdM2eJb-9-ot)P_i!-!V?{&eO{x| zzDNPjb6!g#W6z^plln@FXoU4Xla~j0Pl$bkUB*E%hA2ze^TjRgmBjQmN$;w5N$-^+ z#Yv2Rj4#&+ChN#pZel&Ntt<)#p>vlm3H{QyxRX21MWwk&yLvG{xJ%Q`2g;R`Oi?;a z&$$?X(QrSa-H~_|H}HD~U_tRnMdT5ZrB4{nudC>z?6WLPk}5J|d#R_8Sph8vcyyoP zWaQ|hi`XK7^UmdbUgdUDU$xve5F6=y=bl_7yAMkeCp4S;L*9)Whu@nCu|5t_iC6Qj zb|p@Ue)8Dv$wU-olo(x zs7;<#Pv~lY;#Jd=o&vo2$j?kW-l!91Smno=Qp%B6s+ktj(?l2So^?cAhu*Io(M3aN z^5+&d$*`Q~i5>;jRR=;1?gcAWi#Z|1MfRo!rhepSrqSnVg4S(oPfALEfM1q>?--J4 zQ?PrOSGF^oHnHxl@>oTlD>&nO4-bQ|>LZx%)J-#95>AhaO~dF~fZyBHc5y`;#kp{h z$mgM|j0@S+g%Ssdnj3A+u08x|((nL(iu&8ediX83c82XhCrvXs_$QYbZ27NE^l~P~ zd;p}Fyc1Qk82W*5UI^mmX^1CkW>}>f6A5D)9A9sMe3^4&M5J1u&J&{`hTYAbgUhs; zPG8r$>j)^{&8elL8e+!gD;($Bcvad{8pgj{y`K!@c8-jeJ0DdFY3j`D=ELaN^SMr^ zc7MbgJg*IPwJNVFj)a8Yy;Lb2Rn@56@=9v8k1YwhEvL4T&O+H*^Lk1ANCvlMsZ97J z8v`#JjZ6|>lIFd=Q*3JwOuMxFFs>%(@s3FHa@VAv!g!p_sfNt(`qj+9K|2&k+48*q zBraMm!Z=oarcc00U^cBo|I1L0t^Mj}bBe>SYnvyxQ4qJm;lxDGpH;`4Zvw1-g!#z4 z6FUttK%R34Nu@e_$0a*j%q4PHr4p=rpAYfDZb@<|ibWok>iLu{Pi)vE3aW3ln+wd0 zWMmc&5CP1%jnhoeo(%%xv-yoiI$EU-?Um?GW;_{H-8SZJ>>OeQ4?f@X%OL3UiT0H= z%2Nm;-V9X7BtM(U4zuHqm;ABV70EE?b>DP8d=%KGgiputZ8_3lk1nnT9ox1BNP^DM7Eqcg#!!4T#%AcYWB>Z#dEN`z}ySB@Nv# zvPrf$)2qdL;=9#Z{1H@MxyVPk6*U=C%o0{_}Lzq{;UMy)A-fe9sBYSXCL>AGbGnrIF6!RQtGLi?Iz0R9Fi~ud9;U zG2(tJ0qf;G^|R_} zU7Pffullwn3Zsbz)x7Du_K>iJJQvg{Msi$MCZ>DM%I(zdy5vWboNw^myVPcd^_6S( zDfEmO@164y&>kf{3Kypxt>!UVE>hA(eVEYn?P^WO=*xz95oU{R2|jZkzl$+6vFN04 z$gG}8Qre+sy*&On-6FOw-ck{U)}F%vM-Bqui$9y^SwU8jv}XW354Tn-w}s4xIw!B# z)YS$O5PbZGGFnr-pUj8siqnfMfXO9x>2{rlc!sL~KvqD^=FUtw{eDWc)A`(~Oo1e4 zD}iNEp&Xy9S?akIgJyvtUZK^>Ln!r0xnDN;2WIY6Znz9KwM~vpHu7yNToGQ0`KF=m zYF18N>@eT5(?1c732>6qx``(no+~D7QP4CnSk_*7SekefS{BNn(K2e?Ml;FxF)mp<*j9%1{4$j*sI(<;nK5aZmO zTRxGkxM(!>=;Oe#()fJA+Oua{&C9jn@hxUAjE9WYYmTT+vdGNC`Xj3B%1`IR#M^ls zBIflp!0WVU>s}Fzd+fIDZs8W=u!fUnI)LgUaG$#9?%aOE8-nKx)Erd=^SydlcDbDu64WFLe+cZdGeAvX?stvR&vwb1JwYTHVskFb(^gs5?YN5G}G)( z`t$LbbP_{z#$NV=vK52=Y|W2s%mj5=c}{p>P2P1vTDmt#~hJ` z3bmQAMs7pvl~2TDfmuJArnCDLlg)Dh|-LlFp{KlAxvR?Np>Tu?7iu zX1S2tTcgo)z(ol8O<}cu2!lYtRz8dD2{nz$XfMbY`Eu9BY%_D&R^RiviSz;tSTg*z ziG7aV6)<#o%_e)rmTnT(`_ACRW%);^_otc8<LS zG1+O%i$;S0+3M2W6q>2u1V9*uEWy~R&2T{{V6HCCp66>ft_ zEtBkse&TiPr@k>CPg0MIBwep>zYvx+G4uSH(G&i3R}lBah?xHHJtobfzGWEq;>afJ z`r0yx(Y`9nC}TwJ5F~49oVT3Gt_v{@T+?t;HeOj&R(d0a0qkd(VyGh-&9R2Vi#$qYbpl(+b#)1TgJ-WjT^>d@NwRYtX7C`Mdn)f1WSwGwTh>_YXbHt!8 z{66GAXH2Ta?|#D$xtgYOCHRvv`GIpoe}&BYG^*LIr7h9rW8&OeX>pt499y?!@c8pag zzV+qy6$l92NWcH`hMOzRMQbN*vC156vF%>+sePtbE}ionej|gnUec4p#eDpd>SQyp z)%6OTMj*-AQ_HG=SBu^Wc8}`D5~)`6y!GyRzWvi`!`vwmjokw&?bag--m}CRV>Jw( z-^Evtl2|>U@Z6=JQQ`Om)3G%I&FAHju*pYtnc@CGZn3DizL)OPs4v~87O%5IW6sJ6 zEFkf~kyeEG3o^dJGg#ZRWO3T$Jdmk2kS?M&uw6syANtL;vc`3&EP747#(gn6g zr}dm*%KMFaWPFOuByv5gdB%!>B@RL@FVFasnHm?axVyfK#fI(tYtl7LWa>;B1Gn#Bm8*M^E$OMcNyEW**da2|};r&SEj5 z6itSf&-450uMnbXem|rV+ji=ORA%%~z6i$}c(#84Uqtm4A=b&G>vGK_e|RbYLPieA zS$#zBZcdcOyTkpn7*(76K>Z5aXQUigjoH11&)0v7t;5Qv)!c=RoX$|oFDd|g2WN%B zYHEa@ocfS0-p72sZXZaq=u)6<@|};98J+r%C>2~EXu`3vZFMFY>*S43!&Q1#Q;-U} zy|Gh_eb}mjb0EsWziB)7!Z~ImUc+&(Jb1SU0CG>T$oFedN(Vr8VLKfV9GKpYuS0MzF0Wh89D1jA}Ys*Nl)=hf@@E%2>!KjO^U!AbK zD241sF}>%yOMX=rRhR)uENEfPmr|2 z0*7=@qAoH4-jKuQRl};ltY1AO{x6`dILQ(`5_Ka_m#9;`pU3ep-;qj;CPs&=G^tiI z-4MXe_RQ=gGmRAV>Ss+C(S+lz0)jUje@B3J7xT@3%=g-LwGi?3&Pc%4CcDkEhDM>4 zV~c%$8SY40fxVG&mI0^+!iz}$1X8?nFIDoj$m*fM>e7ddM*N0l;Yc!#Q#IG!;B|=* z##7NZ$t%yB0~hq&k#M1C12Wl;pTZi?&trzfnwOUV`uGZg6IEMzqf7aGwi%N%jqPE| z3p)E5aRh3b!REu5hdZLXVaHwFj9bY zBG1dw-TJCwMr%Grtzs`zo^Sh)t?T!-EB0A`=hrjMyTaH{A`20ZdLI3QgD?Br!H;bS z0R;bX@V_&Cqe~yO5ILng5JXrpusT-1x!x%MQMG#l=$By@u6U$yv5umL5&25cz13of za$Ih3XLp_%C+0BPEO3#aFup#RV|Ta?ofQyJ_?)xmgvZPv-qP2r{(PCUy~eY`(>fJ4 z<}-gjg>9KpTqe1c1D$bHz~|%+pW)9Xmz$-L(@wGD-$-P85=eY+MpbIwjCu+rWf+Cg zk+K|h%xdd$9}u!J-jj(n^)H*5jhrIr)D3mo27^m;22~^LL9aE%9-5WWDqhBN-8zXU zYKfg~C6;CJE78rrIUJ$hm_z`)OfM35#l}8eeXE|7CY+-cJE8 zs%LmCD~HT%f!7A53NM{9r8|PpbUXu;#D9yPAE>Q#SZB0rb_yzU0}hv zQYyhH)-s!ewI^q+X4ZwyribOJQ+)?o6p9O$@p6|pUhJY=U;Nl9kUy&sUN_2int$>H z_AXR;Z=RT-gM(RIM34_6A02e4+vx^QOsw}lXzt*$5w+V~INh84v`ETf4{Fmtm;2$k zwQKn9eY>8odcsXSigwNIFG@YDjqhFpTbQr&rV)1q0xYZYO6?Oc@3V5KYe2o}#80wm zifw+f_6^_GP4J?2DDQ~0rIv8(nqmVmE|C)KA&Q$9?F<}IRTrN(ijY1>8vI7`ocL3p zx^$x@l{GT0boQ8dM)!;ITc6cED7Gt5(grvzl39H?f3~92Cou)hP9)1a&I|>mi&W$H zaFH@&EWg=qY`u!UGX2KSTqltG!;vAXP;-vjDmu5)Bhd%>q-vz#`~i7NffcI~Wla{J z&9$EmRt{CZvl$*6AMX5yzP&wf$Yr-HGb5MUFaNngza^>x7WV9U@N&ySPo04NL&3Xc z7S@}1om~GQZJ2&b8`XwSZ6E$fn@Qz&iCz+W9$pgDvNO|TvMoo#gVcB5z8%9aTPyAC z<3#1vh2uY%>i&4LRwprUz=M*shZFn7Z{1brtjLXf($Xb1IF)aeEX#e*IDl)reL2je z>xc~*(O>#LNQ6&kP3wuZe33aD{OPGKtLnEtSEl;%_XDkxD;rBW;3X32TLqeZPt$qX z$Do z^RO60guFN0OU~;VIEhM?Nm91G32t+rAf{^I7s(1;yx`w_t@Yi6OXw5Ms5Y3InJ0ly zX4p10A+4ytc5#q^&2$-?MbZ;slc8V$;A@$O+tT*ndmiz*?nQ6 zSP$`PCJ34d5qIvQ6c9o;H5r}2=dd(dr*p6Gv;OnZBw<=fus% z-g-67%WCkW;lQx;J}tVo+=EJBkwD<(`5U9Y(;4ps1H#h# z1=pJ!DWe7kik{zvzyM`LfQVocT4&0yK&7B9^U@ryTu9NqJncdgZ7RR`DPPy;DyDkU%t*NRelQG zoi}AjEB_+J>yHic1d=+I)3e0bcpVa`F0cj-aY#>i-A@wMY_(Va{IsWRUTxNE_>ftK zp+wL9>BqRsVPfSA)}L&7nW?LW+PB-*nQz)A+4m^T2qh0b?y4QS2d0q78DG&h z&jOz0^ULv0?l5!MKA1B9^avGPcjy9qY^2Zk$!ge;yWvswc2F(G+d_57Ak$W#CR%Y+*^SAQ0jDf@ZI9PTwqP+V|bP_a?qTwL?1*aYeVHiJTlQ zhGt(t=_f`w;3><;L+S3u0@jKxFgQc%L4X(VY+%qN|Ky2!KJ(#6%1NVqKsBD8QQ0qC z<7Vkc!xg{*5SzE@la2Lyb zFthRG^_OYhKsaHW+aL-)-e(|V>l~l-jnAR`OYN>~*N#%OB4;GfIHB8muxcpwWZS>4 z>kS26xTqfKY@WsV_=}`eQzdh_$0VfnfVHH{ZkNv89>Ly9T@mc9ly1WwlDGMZ!k32_kO4N1+_D>*9 z)7P$ayfkQmxtGhJz99~F6_I1g!VG+ncuIE7gF)qvo0=1G-P>zu*F48(#BFu&Oo7PGSZUJ{<)|o z5fSf|8=GZ(mY1|=Hgnt0@fFP1@!s{jt@RwNOUF}Gl%X*!3)!!)_;%QbHqq;tQZ;!6 za3x=!y4F56yey*-gy!k@--hX+eAig>M9DNrW9IG@9$!*>WIZ~odV8%uMzHX##(eFw z%L$IgQcrq6np;z?kQ(C}{nZ$_ZuqkgF@zDHRM;wO9c0U5aYyKUc}88EFj=CKwph(& z^W?^-P*iAXkOHhkV9<~@w`0W9_SF|6Mq@CzVe@4ZvZ@(`KUGF@N+KNVa8pc?IN^1VkO?B$if^5@?cbi&q(%J)Os4btbU zL7LaeF2~p!&Pau8TYo@Sf*Ey7{Y(Q@%H12LLwanwHXPEA&v=HX=t*iS3+&ngvE+O%0IWk-lQVA^@*d39h^IZ3#es=($URbKg#4BpKV89Z4g zYeXG86$g!5bh5t;AA4TNE0wW`%#R~-Kg|8rFsk0IU<*y2qX)4Xv+-(%Y&@O)-4(=) zVZmzW;5Wr~N|&dVY)+fa8@LL@n~^81W*)C8PM3OK7oqo_5hlI`lE>A1x2Pc>P4YL{oGEQ< z1oU!Aem@lMb0r65hfiOj=g*Dz%q-oA3iHT&p|!m^WQY2pN|O|!_(hB<+-=G%>3MJ8 z3vLy$?T)$kU-l+ha`n_P>3^sT68Q&ZNAz3S(P>lGKKQq?V-IpIk?QFyb4Nx32Uk4W zzGzOoyoxi{N$wK&>{(uFuOFey#y9Bkx&Dg_bLHu@2*4r*FL4#@EN6!fnR^)uGT#Qk zVtY+(;eEaZ;rsjQJ`Z%4TQdscMH*Q$bHg~0zR-vbBHe=w0h^@gWNKsOIq2@v*2lFa z`7@jV(}ZbGIy4UrPb=!_~ya z-F8q@sIq=y`o+AvS2NMcYpjiNtT9Gx``UxcBBoL5WG`Pv=gK4=J4mC~vQVKyo4s~S z>wJ~t%6s|U%@++06utq8bD>mE!HUv)#`*;?Y!S(M6V=z`w8uu5ktHf55uD7f%HzJX zU}MYA;|SgvF>V4hedU#EPjMjev?!1HAk8Yh- zy<4nNVCC|YS0nl5RmQnGaJ%JF-TD$-a_XEJaE;)p0O%G)uO>U05tbz1SWTojqfl@u zW?EpFKXv>R5O|HQKMd{D^gl0#U&u1aPBAxZ^!T}#+kJ0`Q6^|r?q6U&3toFhg-5ix z1*WXgXsfYn5lk>9Pc!eZr|)X!HU&aQN>G&D3BDB?y^C^HL$g`Xojg*Y#)2$MC^G!+$otbR z68qe>k>So2zMdk7=)0J-f|{qdL9~8vu#ZLdHNJ;~kQuxZGVK;_G#icFvjzfUxVe@R!M)iUk!Q!w zW3qC-C=`O1G5dyz0v>^d_G2-K2d~p@6n@@uhc4S*DXXDLE3AN6a9z1XXvkix_FSpjZNm)@L$ zfI0v=C)$s!W%r9bJjc1uiGf%e4Uf~T!t>2S^6OZXpi67h{nrUh-McUs&7yRAfUzGK zeR>wAd$Dk`+p15qhGb-8A1fXbW>}7S_OajdTnq?+(@r?K-y+Ewj=uG*uIBu(49-t# zoUDbOZNZ0?gzY8sx9_em@pFT@;(B`>lp>BNw_;>79N0$ z&2vlbQ+S_N>bmK|Pu+;XSolySaX0JT>w4v^M5WZ2m2$0+D}lKABwOS^x3hmuYPt`9 zff#Y6mrBz2dp|^?BtD*$ob2WE9>F60#oGfx;3LQ(Kr#Qgwy%YY=f$##L z8k{};~Ao~ia@o_bt zbm;UOk6F9jJ{8So9tkB&_WVbS!)(8OvPn{7c?HXg&Kd$aGy%3!4G5;uY)es<)jFyH zn>z6gPA#HATs)@f-d?kbE6L(E{&+W*8@QDQQ`DXKMD#O&2F!$&zeJ6zfeZp{&AFCz zz)A`YRn_H6)zRlld$9srprVRx_b5ByXr$BoKLTagI0(2U;gC7UI?iZA^_(7jch`fn z>sxOxxi7tn6Pe?~675mfoi4Bz6H&Rq)xGNvK!Ot6pGA5FT!gBJ9zJc4%aj472y>TCPKC4; z!)nmh9Cu80Z~Z=OA-%SA^Ow-?J3ELeJ_w$xi+v&)c(5KA;0}R zZlx90;*QxX)~bgTEjTZ}FzQ%gRw}TRHGS@e%Qu$Qfa54-jLZH6=jU811F5tSPP{E| z-UV1o(sOGzS+`ymmh@z!+nf_l(-=tj7P$|}6rz6oAx#!|9Zh*#J%0AwvtAO`)9OA? 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%0AY=BJuFnLu40n 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

2||<{@4aEhcYG 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

QyAgk3nA*p8K~j5(ZMJkYig5ubYIU?gjt=< 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℞?qV99C`SJ@G&q@^Y$E+Y14o|b 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%0ALp=e$GS5=81E)8~XKDc;(oCqPKP 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 diff --git a/docs/images/readme-1.png b/docs/images/readme-1.png deleted file mode 100644 index 40abf164543bb4798d30d15c17a0559c9833dc21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84348 zcmeEuRajj?vM%m{5ZqmY6M}ot;O_43?h+(8K|=80?(XhxfsMPnZ|v^OITJZD-+8!? z!_%TycUM={U((gR_GdX6u~!JV2w-4fuRe+kD}aGPSA&5;D8a#iS{T!sv%tW>)y;*3 zexpw~X$(c97AG2GFq z_bo|FYYqGL8l+MeShjp;y(LlvF|dkZpd`Qf-X!PLnftJHoWi!;X&*`In%a++qLu?k z`~`etW-B1;4Gc^lI7(; zzOcOUH(`1Pt8)Gl(=#1AFXt)fw@6XlKulGsK+=)qXE_K)QHVi74B%|ftfFS&%@*!N z#g-^UNOvFf;6f0E3C=@8%47;Dc*ciAmK3dIfvenkKtif}czJng$0OOP&G6UH=485a zA?3)xMj=XMUm!~|z+sJHg0m}+7Lm1w$UZ=k2w zdi(NX$_fK>Gl+!bOZE8j;yL;9;)?~zx~i|o5d#Y*_F|eM5XG00ze6#szw`Hg?h{cC)s!apZF2CH+SaF3|JOW=2xte{^xON#4y1KeDxUw+VI+!vt zb8>PrGQDGb_l_RagWl2I#!26e-o}yaKPLI-Ji^9~h7RU-PUf~Y#6RcNH?Vbf;w2^h zN$A(#f2`BV+~hYU8^`}-3&bGf&o_+B3`~r_<^~Pr`Ps@P>tJpSqWp7yK4zYO^!%&s zKk4u={v`g#V*Z2Ef3$*F!aQ|1d@rS7glA!e|_`EoE>^>gAa5cgIoo94Crj*_PNf`g6tJVTy7;{ymg!q4u*grclC5<}%Z&Ll)hfc?@ z(<=OwR3+wr5&h3j*dsffe{lET9x)j^^y+$DQEA2h1=C0-m-zpqa{qIw;aGZ1iyO_M z7&-rfX_#tv5KLhI4U@lA^>3K`cSHO)O#Yi8{u?I$ zhRMH!&VLCl|G$FH5nWsG-?5xdj+l}S*|m!d6%8%ijFrB3as?R~`DS4K011|L^uu2s zoQD(qIrwRdu950DME&XL)_kwIqEeANo;R`94zU6{Vip_FC_9fh#Pu}8pehLPk7Bw< zaTvD-KYtN%$TkTwD9~zRLfE|>=;E@%{WGmdZ}TIOPssPhCiMR^QTQfH%(R8ccEb7D zw~nBj8DpRg4fk^o6eLm|TSa9oQJ_V6NfBF8Gw;|N<>pTj{+X(w-A_I#0bEF^^KHby0=-*S?YPA!V1^jTS zP|Ys8&b-d%5|80liCmG1hz`upr{!WV4*0<}t4daX$$L=N!1pI7B`Tndr})@|E%i4e z#HAhAi|a|x4={9(ru-j-vh_~)G9x==nsr21hC`3%_ppt|Pd=?2Tk0FF?wmQfjt($1 z(*HH-ho6(~nXrBNJ?WYt#4sB{Pk*&)H~(55f&n*0?rZl6mFRXYh1ecZvF~;yC@tI# z^iA)JZ|=A^6aQcoy#fN1AC_juEeQXi%q!>kNNpxH0#j3~ubvdjRu~)QKlgVB&fao5vXZ2A>B-%+xdi z?Sa`fzH@Xd_GmITwm`8I+r;+__rkr&)Gm7I4_Wy^sLO(MQ}QF<3iY=LI)vb~5L%+1 zPe*$Prv+(ZtZ}lxXU|ffJLF#KuZTgeUkB_8)dUp(u>iRa4=7MNp5A66`Nv|vK$U%3 z`Y>+KzcJGInuAMSlvwjLb+5oIo+cgE*M}1w`eJLw^fKYMl>H!781)RVA)x-qEh}jJ zQ(U{4^)`6Ik&Gn8#O<^LjugZSX%T;~7X&uFpBc6(#P2r0SvYcX@{KgxuU!vG8NHDM zY{yXWW|9QCWeTR4zZC&8JFtV!eY(FC_Qv0R*j2CB!@2ZI%$!|`Y>u*p!mkzql=YYLj=L!Sa?P}_8+^F&7g2y+V|(S~3f*+-0*_N9e*$-JG=(GOt^c|2O zC*TA28tb>}5R7E}n`4;;Fp*3oQ)=pr$87XV|D<#_588qAvk|cWu~Dor^50A1kSx;) zK1fVhOeDd!k@_7SV{(7`zp3TnKKWnxIK;4XsT!|}93QeCu1f0*{{05QNDbuPkkA}p zf7zQKL?U&}=mIK}n!)HhHL6+4Qfr*0SM2$J@F8G=rpLrxeT{u6`J3>W97Os_854Gu z&U;Obt?z)oze2VpXv8AGBlS4<*E}kua(6*mc6Jp)2ACdjIW@hVlY}Bli+>eI^sIjt zAT*<^TYs&uLjvl;y`g(_^doe2waj}>0IBtty1$_g_~~ow2u(eINfHEgw;d;;$pFI8 z=;-lx^oe!iDLK}E+dtb+`!DTZ(D`lu6cB7|Ff-JZG?VHKIScymUikyi;a3uZ;w$JaHaeU$dtotKL71nsX=uM z)*G(H-(V>b&vZ@Eu(PieGcYj7v#$L=dK*+~)fQr%B2`T0$#1Ssb^pgvZ?IRY;ZxJB zShn}9a1xGl4j8QU{&H-rKg$`Tt>bmSt%DY|KxGb*7ceI*rR0=nSvjNre;V}$`#}Q{ z-HilxV`GCwx)>DFcI}@uOp>{(vwpz{WE^f9KW&R%{p9N}_bbB3r6)vuwJLb;opcYR-`-jj4{C7z@^lmQYcA0;n3=BFwT;LD}gSo`jP*_Zq_v6E+1^)7by;rLIU0lBsP5{I6gj3FUCNT`Hd6-aLG4J z1zKL86O~Bj{#wxZhcz1`BvC0%zuHBa&d$kob^Hte{?N}+f`@&^=TAMVv7-y`4z4;R z9HD=i2dFg0Lk5m~?It>r=#$jYz#)~&ff|lU-!)sTii(Gq@|G~dXuqt4=%35S!4el2 z7j_dy2c>po419!@_UEChmwpkEgB7)vo%25Q_EdBbp2*sMt=Wowv?>0)KNClrS_6%= z0NW)MYpH>bm53AxyyGJ)eOt@ofXyg7XZwK2NRsY{ZR9^ZG4&Y_6vw*<_X5+kByN|Z z4$}4`vwSVOUYCSCJpgm4-h*J(%uYS~(Ov$nZs9 zp1R{-w!vwE%7SXCeo#f@Kx!Xbf9a} zX2nmB%47!OXt^aY2E*Sm?ug|o1j%KI^I6|lo6$rI{-JLF2soysG}Knpmg6uC+KEQT zU8m^eSbh(O-Lc5SALWLNb=GDeL0mAGkepq;*N!8Z{x%TSb|sZx9cKay3!81dSjXYC zH{q>1GO&B!#?7XF$1~QxYcCZ?=?KdGE9AiM$GF`t-l$le_c5G0HelOnLzfxW`u~7o z&6?M%n1mR+ATWqv;a`aD^*G{UCL#QMZqHPJ7y+aCV49VHU49a*h{gG;&gGz?4#1q@ z^+o_bidF_b+0y?4YIe*Gqb3BwZZX@8M0TcF13xWUMdaE+N5k*s+xv9bKOIL08NwNO zOf=jLk7@>wdzH(b&2rQF(LyzVhi43L#F}B|#N0yMBf@z6(m$kn z&U%U@#;@KQ3%eZ7mDUQlR2uZ24KuczVa1X+ye`<=RE@({)tE z_<1pw0X(pe#iYb`f0%CUig4TQbU7#JbYn)z$k;i#1Ox`QdcbAT8-1#b$eaV2qMwff zKUX1Cob?ewZ9TmGq=p`McA?ajcfOBur!mI+uZH5Wh6nyu?k_ojges!VJx6AANoEX9 zW=uzs_))RA)1!9c5M^LtWi?ftf$x|eP*KxP*MG3$;7&r zm$fzd-Jw}Y8ryU|_9+?~8qE6ND&)ZRMdF1_Ovp{oj>zzr6lY{)%pr3IH$DZ2gjia~ z*6B3Zfh(+@Z}tlvkXnB|)~K}z@_l(yEe^4HYkS3(JQjD4vdG}=Ss0j85M%feA(^rL zQNbk;kKJai_J_{&PWWoh$GDn%*-avkymBtj8!LQ1FKh~l_>Rs%*xI&>v$F}dnD=B3 zWi1y&v@zqs=I%-3Rc&oP3XvaD7+n|h0*b&YRCZf6{7q5mL4s(zn`_Lz*A)6l6%szC*BIwo z;Sob~Q;`L!OYrv0G-q_#JsARas>eb%6c6#ljrg1DkzleYN+rEo0gF?xAxgU98BX%v zWZpf01pvu6ddaAg2a&Kdxn`4DfPqoQwmXRO_Gch3xc2lYh1pWhM>Kk(@_jFF4N(W% zr3U%PG50G?v+FC^H%Z#LaUI9!`sYd^EjC;maie}X?QW7o$l+PpZ_5U;q3Od|6+T%7E!6H*?2fna7>JmkrAKY zN2_@v`^V^(9U#YNq~`KI9Rhuz34#U7rtilk+nS9q9llbO*)W3zdM&V3LLo*ROyhw; z(Oj1N`ZG{$RFd(cZ2MZyb*hlK0jiQ>JVZlPR8JB&?y6ofoG!Dc0Doga>xene7qMmK zfda7rwfL`tAZJxF)OXPxAw)g}$~UF;D^n0c7ZnYB-oUl`RUb*#4Rw_%qBq~DWC`gz z$loGui79*;XtAbYzfslo&VAB1#ufi!K24HL9JXqAEUg&2$~fl`P-}^b zS9~E;v|XhOKR;~g@h}~IPF;4|dYfLiF!GJ%lsSXhFt|<%#eq?$Ma`2D1vq_3k?oMX ztdNuL5o`V=w!>@FT&B}n1dWkfzUp-;ZGFq@d81Tsytu@o8+HC3fp=PUxzRDdt^%a} z{qlH|!ET?#X{%t%A7$$x#Q;n#Kv4u>fMZ_zmCE^fFeppc_epuNG~GaSWuQjONu$B; z`;9ln<%z45`WOiGM&AC|*JpjMZQ2nirX?SotPCjOj3hI#At%^ekYkAG2||Is+=7y5 zZ?ba{Vx35Cp9_pF2(GvDt>Y{noFLZ{9gzn_VD`!OY4=(7x&PWc^4Nk$jrgqao`v?W z)>?kW{G?ND@`_xyPRGb-YiBft1jQkqlq@BhkT=;PC#S89&p@G$)8#0MO_PY(9M|d# z^#15qV+C0Em^3KUm%B|A>z-ZLRquQPMM9;wg4#Hpbl|S=W>1&n{ANGO(yT1%dU|>~ zCMQwvFSY}rjIXnP=np5-1I!$~+gj!d6~z$kBqy4z7OL{XYxL5gPAJ&V?=N>1=_Ma_ z^tFwf*{lI({*E}W7|3dK_fU9Cbldq{j^@9Q#>Jz)(@>_PQ7+1X{xQ8n{$nw5?n!r8Ul-au(_`;%I!@IhoX~Sj?_c}3tH?TxrF-7wG1T3=>UZ9-T=p(rM*_x*AQxLSI zYjHd#L!aQNdS3231nxp+fWYyUsHJHe%y8}P-T+(+pq8Nxdr@duGyPtp6Zdzjj$niV z+CQ<_%L0KwgYk4;0JRmo)+=l#0lZ0C>8U`IcOO(mx05m9-w%dF4p%T#h#xEf@6C50zS}xNMQ#i4s3~FX9jXR$}QZ|TqQIgLT-XVj}8#gvK z_MIf{7oaf2qQ~T+UKzcE*K}rETjaha;OQF!3%fVJs=DroalCs1jq=*@ap`fEU?5D4 zeEkglbS&gB;qR(uGZ+h!(f_7+pdBsEp1 zjd$|)zh#l$ub-~87N?y~^`j`J@0hBeuGV-13wASVgxVyFR7#V*`U7f|6<~7dLGnEI z@R9||lh^cl-yT6;*bv?P^;j2wUcz zx;Ul9Snf<4PgA~0Fh5SP9;X%yPuW{lr=N1`WdQd(TG!53E}-7po&{MvX9}XaM4S%7 zx+0zle(e!@Ut5-apRT_qUM{_dOV?Q>tkvE>-j(0YFKY3~wzcJYv8OZk^!VTjRwfuEkC=78KV$0 zF70zMNR`Cr-NaV9TQK({Ru$*-%6B1H`sJ`3@7Sz-m&ui!%kgO|Rj!ct+>0}E8EnlP zLL)g;HI9M>l+wvL&|X9V^qk&f)6aA%bKCY~?Wkm;X09YsDQvG!+a4GoAR*(3*vy-3 zWk5zVM`g$T9s(_&QOqlg=RNab&T2(ndVE_PpO@@)?$z7BDqHCxKX-gWPt4J8v;B8R zQ(*b|+qZbd^=dqCPXM0NYfd0{R^#3s{3>v3AQ3h7f(*QpL@3y7_{#Z*~-Z#XG`nnpFvgC9Xs26l4y;3ABlX8Hk<3=_SCj%NJzT< zybllHF`B~ugYc94W?$siu@QL%55D5ki7H{Q@@jOAqNv)i5S1}hQ$)m%OVh(c@z@^-d>Zw_1H>f9W5>M zm0C@;)FgQOIbi0Gy2h(tpr)TPqL>;dDk27L1njEsGZXJ&yH9h z^lWXv*5aB3mB#Y%lmiuj@qce-h5XD#(?fuUgqMQZ-^lHj3KglWPv6ibf}Pe`FOCqF z>UReNAjz&firQ>(ye&1z1s*{9PZ^sMX}ajDT!Op-sd zRsKc1LsEf0DKUzG`vb~d@*G)f)BfTCO8WF_iYM!`E?^t@JU#YfHKpm`a!YwC`08ju zG39zzRo5JxXL@`8xE^GL8XXFAtr@4;HIn=)02jUkuW2CvNsZ6QRb9P{EWFMz0TM|3 zveDux<+s~$K)S=HtW#Cw3-U4kMn7`bfKQ)32`CiQm`#2tZilMh{*c1+>3XJ6@eGvj z?yR0B0T3QTKq-&{#1EU`^<015HQ*?<^JI!4b=8O6zJ96>mak}R zul_K$Z$f^)%0^K>Qx@)bXZqqai14_>-FU|2HTn7Zm~sgnNrVH52^8Y=C}fX!o6)tg znHR5x?_*oW-r0ioP5>?xi{~G@zL6`fo|f+AqouSc?s1bz3RlF5Z91TkSo=|(!QDxE z7~~XcJtVnPX9F|0))UMFck&KbZGnrn?X!e!)c(fssr%Lmn^Cx|Lq|e;Qn#Q4G;eXX zOk2|v6z79ch-{j~QxbU=45-NDbIV&=T57Hk`ClF#FPd&IN6nhQpj<>y?YaA&fz)M* zf||x+iVUc;PVw>?Fcsv>cn7ige&Q*IZ+OSzntZGsv~jWUw`vqH0TN7aRMSI-;G?O*Q83c~yiT-#g8V z0NFGy*5R5%ch*noQ{wRvlf(}4b7O$<`M4`y91EF+t#Q{)>5%3c%hCWNlpgNTw1wtw!T;JPO z+6u!mE6JmDq}h)^C7=p0>2DKI@ar~8&t;2Fpn$qs>Yl)|-g zS2Bl2MSO_I9ij9-Ff&Dg71K?Q zdooK>8VR3`RV2ihMKy^)m{)qqF92Nnbm5f!dvf$fl%F-IM5>C1J@<|d{x$k?({#B= zrBw-{_PN4mGysV!D5>Fb_WwkUuk}di5qXEYYa^V)r?rAh9Y-s*Y>$epIu1dt!@-Xo zC@meo(wvDXR_QTlFrs^h7WL38=(*O^bYKbzIiy?~4#QacArO!tUuMOLKK7NRb(yF2 z9=;Opia#Xg&~-iOR3!fG9sa@DmBy{ojo0W4IUAjBlC5cN0~M;MAd)E;-s44a|0^ApI#=&sV~meY5h77RKmyEPWPN+v7l1E;?*eFUT0P-7wNziH_yZSiI>f zL`(M7#W2MfjR&iVWg;VJ6nRkCo_P}8tE-Jrs^!(? z*M>fC@6DAV-rRvB%TP=ddh#{^dNwuAUOC}jfNQmeWcG)w6wDaDn;UxoyHBgm>WZ^c zgt&Qn##wnZqr6E)_F9XwyiJKZZf^m9FeSK;XbE$Lt?$flClXB1_uZT*u|X**W-Yh# z>XnK}zo4kYS0;*wjjtle*SlSfi|t-lceUWz@x6xuwm(5$?Be+af;-0YBSsN%)B|m$ zo^QPA08T(~Uj2|{?f#9gwkv{-A2V8S*LTv`uQZ$5d+p zEnK&M_{pckIN`cuZUv_`xHVj6)S_d079O64S7cxoalZaocjVcU3{?6oMr`>Wst_n~ zoN=!w4T*n*Poiy+aW;bT9N|e}7{c9t!h5y%!0dGb!B%&(w`5D#RBs#FvY_qPptjWF zg8^Gt-i6{@(s^6~m)edElOQh*UCrXM)V}~&=_I7N43;p=R>Vg2on^!a$*PEi?XtryIIGQyAb(&>glK5X{4sYv$;AdLQhxb9Ki79t36XchxTd`>5mXs0c{IMwZG!3!a& zvz+9+)73V9^jfJA?S?p3_%X+*GIWL+MXTyFBK!TEPek+iG+l&5hh?rI%; zUUy@mIRbUCI(XO6ZBLIF4aN9ZdAY{TH+%eMOg`W4*(kjn*f=xl_PGY9&(9AO%flXZ z5_v8sDc{WSS)DjmHE}ND-Ilg#Rv9~b@1;={jSFUZQ59Yeu|?jjsVGn<2fIs*Bf@Z^o^V0>ECB zNK=DJb~2iH8Ejg16>rABYq*hyw_runYH-dBx0m)||IRSGo6 z8f-4wW`u>;%;~hNably~Ckidux8#`tnT}Lzr8nC|E?D=t!0cYqra3Z={y_*of}9eGO3Nj`?4~dV1EJ%)mk~ zE6iOk*F&YaYIoG8)Z4Mofq9CVHtgDoog*YnX6g{Q8C44%9fOqspHPlC4$5a4&bDYq z9*!9{PRU6Dna{S4!%;G6fGh!*>G1JMwPpv-C=Vi`iBhJXqL}>bYCMVfqR_I|gj&S| zIlZoBygSob2`yx3zzvSylB;*6**<^67FNyy25i0+r+k@$veV%da#WSM6m}xLPS|wY zuHB&wcGl)`>}~JV-c&a1(3UGp_FaTYkgoq}?dxMb9c9a`;%@?Vb#mGTuEVP6hWt<9 z$1>2QrV1x6b8sF1oMk*zZ0Z8K9Mv=1gxY!MeJ)|OP$66Vv#!A8dGbb_@=DxU z3=H2OB4hkZUgP9urSF4>)nXl%;16384QQ$3V{N&s-YT=jTH3JPvAkp2`;0xNk|$G6 z1}L#i_i{mMBwsq68p)s@Ev2;wkHjPutXT(5*pXmOrDor0eY6IA|`N zUH47-vXPK;K&_)rR*bLv1Ed$fUrNqOx0!C*Kl=e!9t{bylxoqkA$6n9>a z!+U@d1S=)EMaDf!otHaCEmgtQYyKAh&0PrjS}77O?|fj1;#5gZQ4N3jU7<7n~g9a-Ud;|&``vX`3*C$m~ zg&hg#zF*eZSs3&b_-XxDO!afQVK<2sh+KUI z{hLjjjkHNyy-D5SHQ%QdvhB;FC{@bwj{GOf{2Hn>;9j-i?7*bQ0f`^%*oyG>+*5`9 zHIG}~dsSWfv>(EcEY}oHNiE9s<59XUgdbi!dQY|4X|RcI-bUVjT@IEUOFX;L4Ob|G zAs2<&TpT1jzh?B^ycqRl56q@buEme;rKKWqS{9?8^T=Xr0*oXdaE>Naob<@lP#Zq_ z(ha>}=iLy7j=K^Fr+#}Sp?nlP-u{A1aDXMH<8e4o^S)>qwV%u%ciXaq{^DI-YI}0a z5{G_>V8&Q~Xfnf{(}?h+`<7Aiwt#=p@>zdI^<+VhqlC8eF_cD)+2=?x2>BvGDy#Vh z{2-!r_W*8Xh6@<7!~&Ovjc{d8<}&YVPeAA=xDouEfbw=NmCmE&c~@uHqFwon={vbc z9&2>IXCFQWZI-xdlo{RgGh_?zHLVtsis18m3fFUlq{l%N>~n`!rB(We{Pr+lwvXa>W6zcn z;Moi_QR;={)3*7*jz2PFTX)yK_Y4KIa+VNNsjs1w##Y=8^;=uX_9i&KOHayCxc-{@ ze!b^Y-wiE2wIPX*is_8;y~DP?A>VA@PoEHe4u=!?y7Q`1##vnH(c1YWr!CWI9Jxd< zxG_9LWx|Rr_V?Y0HFNk+l6Ji#s|K=%ZsUgSFxUCA^_a@F0yib?^_vm{)_1_5OzCNf zy}1^eL~FhPR&H5?9d2GGcQZVxI~V9w(3#XV4OVwwD<9w;eAMdg&Ef83g&1evfnjMm zkuhEw=rR!xsy`I#JMO7Y#8JQx;kcsN^*m9v@)tvJ6{7RrgWS_~l@cPArzbQXC8jZ% z$Rg{NEL1kzmCuswGu9V0K_4P7g4U@!RW;e;OO)_A9cKDRTSM#*hP3BB(M5B=1g(=! zLfkW`W;2CvDZG8xQj}%*9qVj*P~eEy7rtIui%wNRm;?w^x6*7>vDCDR=TBth_xBK@ zH9OrVTJ_fa4inSeCSa{plKQ5BmAsGCvn_yHy8vuSx8xHS#;m_SDRW{@HGoEIHVr+QNo!{orb<|Jkp zFun(!NDC#yUd#8VVLs)pVYnjIhW){_clWzE9NzoHZTS39ti$Df!>kGcHG>K4EczMc zOg!2SMHKAzR!_cnC&L)noH?uxroKsg`QXqv>|(D|QRaJ-e67Txwqw)HyO zxGU`%nM3F7FR3 z+zPkWc{7)oYHm43_T_!!*ITJ8<#4oOhHbO^gUGqM8Q|89l z_amG;dyKpFY$PL}8qDIouZ=5w6U@d@+0&f|fVAbw1a@oO`E*ZPo!7c>!5U&nTH8NP z&_0RVl?m+NXT zun0QM?)=WCWYr#aH5MdR;2Iwo9Xi zi6OZoMu>u68;X^2xKev$*Bq`V4W!I2GR8Tfe-X5%T(N}k<@!_@8eFfDJj|wq2 zAzqs=UU>viCh{=^_g)t@viVAQGm8O^ukuwp!$N@MZj|AO-&iR7QBOe`MO-xFD*b}8 zwrx+p!63hp08q0mXGxm&A-p(xkszh#e9myo66^*hHjq|VjE@Bv= zQS$Y+rv7?$d;>B8qF)SzaVhT0at9ucL&ma{m#eU6atYav&gBI^O`FN*N7S{LKr_aB zGxMh^`Sb_4FRgDJWp3wR@e%m>p!tZDymOEmaxJuufqy}Lvu_DHX-Wm7FXW zzPQ*ttgG$#{l#`-fW96Zki{1!F<9g)>@2~`IWr_TE6lJQt4J{ zy_jEnEQ7wYYl5m;X1Z%_yfctsHTfEug}yPvRI?X!=PrPI<=*KuKDsTt2RDui=2j>69NF%a*>|lpm#^(eME0>u@#c zm$8IL5kSFY?`f7}xW_>3`=pzhrt*o>YmQV2C3%q$D{SBm7B;ZGtzgTT2gSf9wLf!A zxk672dv0_oFjJpbj$_vm+ug#hKh#FL(qI==xCeTp*}S#V+z<^TlRgYzheEzx!&r$y z`%P~+R*+$`qcSnRgKA&=I{l>eg~eT2a?o`@Otz z9KR)cx|B*SJb2ywWwXc1H8S+kLf<=}ewK!V*|vcdM*)NAz{=9_#yCZBy*r4U&o1;S;2X%3K*hs zKo|v49b4X*xW{wIQ9qe1np7Gq&NASAu<6v>gZ5J_r?N|ax8lcndw8}z_8mkdqIa)Z zzbK@T2kZ3h@zpXttqL3^HKTxc!*(nmuAj^`qq@mMTV>D0F3VM_p~fi@_NnlWw6A}i z+k6_|G50?@7;V;@i>{W-R$IxD7W;1dqP#cd2^W?2s>?N_C0d3aD=W0=P%zA^9y^=` zw+7FT87(-=dii27V-s??UF7zq6%_gdE6#5t-Le&ZvGJ93PV0R|hPhU=tS;H@`*TU# z9y}3~6K2}e;#>-K0;1@nKq~_2k%YhQo_6^@WTM#JwGJ! z%~D+cIFLQ{dYj<~68|%kG_S2lx$Y2OseKs2-`CmQ!XTI}K1Hz|9lj;pR6pWUiA+uW zLK{-dhR`RdJHDgIN{*sH=@Nm((=_Bubgo1UyZ@!$tZ#?TA2Z? zUbiR-Q@%SW!5wU-n2j|kKkB*FL@9QMsX4u*p&^f~>0OqCqALB?o=UZJEl}51+lhMZ zantR_+QfPP#lK0DhOHC(yR6jELTPzfyJ!f0|ADjS>u+Y!66Mm87z1-q{BK|V^S1zg z6t3tq9n6+wj+L6FuGEO(-Ip@Pg{RmJ1^H+A1o>DiLM&9ZNR=X+DRp8$(JujcrBt)s zK{V-H&)iUXxn{n8Tq{B;6IrJ64Yn`c@ra=}sqw`6WhVcd7*ZxnW!qy}duJ$Ny4Tt_#n+7M@+||O`)%S@t1s&sKzsT?j3y^obKOwF=}oBU*CbvM!0`+`!hGVY4R=m9K-PSqkO&Ttm{F{JNdlg=<$(3Am zkAkJ81*iA*Ez}!@xP(v%8~W5$g|OGowu)%IVHm3Zk34oWWIU8)mXWaWX4NzYg5P%} zK1pP}#w7(86LTK>WY`U09r~axFd<8TA>ddS`9aTYs`tR`I#Z~&SSfobKXJO2cu$S$ia*Ka*qe?B0MjFx1q1&m-0oS0l?iIZ}qGpx?gwNYU zPQ$4P3^!Z3?09vQUL;3t0H80Y$`Sq{S(ft-MN_*V~jD8JGhpSYs$7jaeszEYoFs6^C#a35YTq~)=DN0?u(6{7V*qtphcG3z;b zggM&eES%n0{JMzn$ZdPWxK`%-*7X+m#M~e>XVo`O%`~mqS1^q;P2deUJFgn065i49 z6{cOV8k~5ttur7)6|_8Cu&VBw;m=nIE?2(JtySf3h)hFc&}N5uLF*=)5wg!!9P#el z*HqiJESjF-f}gkebrc1|8UDg0C!o5_ph02n`fjfwkR zbh)Q6)=zo;XfG22pgJRdL_gp1)PVarjUcdTZo00S0P{0WcpZtvtHAgWejp`uts+t6 zKfZM1c?%BPBBv0U-m6ZY-dfz|sWc-j8TDjzb9$f-!RY)pQW{al@TtB3?4I_Ooqf0m z*k-Mr+pN<4H%(TNIu+&0!>^@kO{UzzS0zyG+5PLToCur^G14u zyW31?sQo9o4|e%Lx52;3OiU4cL%Nj#C~h1RSTY_s0yTtOc-x(3nI=}4XulDIBxqcCuSr~P@Ojs%KvSY!; zl*VN_R)K@$Qtkqpi@#FUzt-ZeMH^-_K}K1+&i!mF(vsa?-P6V7eUukF1but+ z3mXYdC>8SrY2m!GF8T6gyoIGRN!J+X+tV6cciXag&OM}9?#aD<8x#}=%jIH|`@FguQ2e}m{*e00>`9-S^^p(mQsty z(4RTjU3oM{S*yPcmZYGI^`0 zV+hG8ka zE%>yOZ^Ifps~Bcsv5GIU)44RF2I+(5dWP_Fe7E z^^;0nl`-fu0QHdS#y>(93YDIzY4|ja__A#m&(_F((e`kgH?3CWf#j#`Cg^*~Yq5i9 z-($bHY~Byb6{HHwx)!#!A-c~i{=J9Dq$%NXP0D--sIJ*0WUY%x>170Jd7$$;Q2t2( zj$tOB9i2-KBTAwYJkB?`u@_}9tJ#$qut3u6x{e!c*!`syF1%K-JAhYiViG3d!6mpA z!Q?c(731qFk>BRmr|;>X^Ea5a<(PFP;5K&ljj=gRAfCC)56m_{<+TFWYoki#>DsBP zj1@aa2e|K^v-OjjcdiWU1yE!fTW282H0OdsNZ4ivjiV(Ok1Pi@9Bzaz5Q;!)O=32@!04Sc@ABJbMaL6o=yuuI&#{3|O#;H1Q7ZvfQD>KW_$o3Bzt6 zzI)M7Wf_h;dYpf8)>O0&xgPr=Z#Gw97o}oj)d3rLBF3>vR3N%2x}G(ze=Me=)^b?)A=kNj(S&yU{VarNhn8lwk_!YN*5;pqL2Zi zV7yP#u+qSg8OhU@CMvx(2lyWM*)+le+d=^zYPlXg?~zmGQ51#K#1J-y zlTJKsq?ig_E{p=S_P$&)Aj?-)0%nA!C{H)+DX(CCO)V$D0~cFAk+M|+w2^O*B)2s_ z+6_coP#B?eNNV_t-2*>1)32J^`-8p>xmG%iFP^HatE1MeEAiO494`P}?8LO?O%eNb zL_M=yN0UkKTfW?algEJ+)3V?}ts!a~QXKnZ9rW3~-1N;&A3q+!j1W<_k{-7Lbfqz0 z;18ZS&GD@H7!#x7$&h>FHE`v))MAD=3&pD`2YmBBD&VGU;5RzJ6Bp^#{ECGzB{a` zWqVtqsE7>}P>`aCbWjkH4k94EcL<>L4uR0SfDKTj_adFp36KzqAicNHLXn<8=!Bkc z>pAD%bAQkGhj|o`y=P|4nzi0}*Sp@YF|)t5B7E$?Jysbjs2jCEHqq%yt$}czs}~a+ zt0IWoO(SThr)4Qz`EJ_Qdz0_bj5dFfsr@D}^jK5O67q9;Ku$DTm+J#Wf}ql7VupOV zq+LzLjy;-4||N z85gkEYzwtj8Y`V0O_pZ7M=Dt^KIejMyhOGBLUdl@SR^gczAMbE#Ara3cQj`H=*?W} z4Sr_hKz5w}bpCcfP4LOY=g()isoopO_9iNQN`j(+%}~tW+wW@ND zQlr@C`=ghP&n(s&ikZY;Ime`lpx$J0!Ea~@ed1FK4|jtNtY zLGT3~Ij!U%GHg@g(ns0pWLCyi=3kkmhZ`Fveq0a(iwsYS2V;tBp+Pl2lhhL%K1E4iVyA99=iI;f8qfrP`Y!{#(ppSlZq@(j~Fw>f7TArhhrGlkSW z^<&pA9uLYES9)P&o&S0@BCy2ip!6}s)m zY5rptOf&CI8`jj8tQX|o%A;etekqQr3;t{2UTx{@=Rx+5JP+nM{HmS$S%%kIS{srv2H-KsW(A)V>x)z01x)2& z=jJZz=vZ>~ypOecwRk?pBhs(TI3i00SCX@%n9KY=g)51MEpI>J`2iR1PX)Y(F3BOi z5RH{0%Ns-wUV5mkSLF;^E=7Qkd=WjZNm#W2Y6Nq$s9dj&`QFzF8oW_Hw#XKELi#cP zz4Zm%I4$wGwqz^140V^s$?PHM36hr^fH$9!D~G@OQ5*g6l#>9@O7 z0V*=?lBa=bzuIdVUnM$6?YBNO z1(rNyCCwYvu8V|ex|be1`0NhI*0|xE>FG@nvcHQg{zSzl zT%hgaP$ZJWLJ1qrjeQU~qr4J&!`Lx$;Cv#J@-@BguS`j?L9c=amD&B_6`z=rQ|PMf zALVi7%b2BjP@crDu8kWFA3e|0pC*Th`ACa?=Rm@{^?^c4;4-M=N=xBJMm_A}+p`8? zP5loBHC`RmT^33*CDneaH?AuN7|bECBvsccB}SX+YhPtwpLjhV-$;1?FzCz7an8w` zXE(ayPQvXi7bQQ-{rqxXRH5$unm(sKy-yr9Pc>H|&j?t7A9ktairkH#8W8sDMIk}8 zO1DWTvz#PV>=zEI6fQlTs}d;nITlM6@w_1XyrePiTWFT{ao`IlVRd`^v)*{kOV5s5 zslt!KrM-DN1Zh7UZV+U@nQCnI#TvbU%-!lu6AkIX5u4mKWHQY$M+c_s&TiLc=OSYA z@SU~ZOy>+N>LNb!+rDWOP6I9$I7;2Ro*yLdHwF4mmRX&-uV`dF$l==2dGO}E4K)5^ zFJ;d{Lsehkk?8S5S>DKN6A}1`Z61q;TrFn-o;QQhl2)44&&AH>ZrF`M?w;wqym`yx zUf3wCML3NZIyA?AH-A^pi+ zGTN}ZyTU~*@_At)t5EJRHUtfAi8Wmk3uMg-rWqGq)AN&G(z!=M=|L&XR zdDGIaz&PW3?`-h<*rsIDc)kLji{VE!I05|6)lYG=h4exQi^W?$r_+};9Z&rC@#n=l z*>Up+;-Y~!885tixgq`Wwb}*p=O3?0N=tAC(0Nmn#~Pt^bfD{39zIvKDg)6)~nE`C|g9>HAmmIh0r zu%Iiyr5?cN4tj+*qEYn+zdpJ3i4qp9fU*?XK5?Q?uXk4{{1y{c{28!8&6{v?-yScn zMY_6$rh5D$R@APOD<38gi?wW%X zzL=H;yv0=4VTet5?O^X5B-I#;>HRnK=7dURNVD(Fkj{~`dErTZLha~lc1*`_P ztES6F-b4&&yiHSWI~pIaslU!?fYt}JO%kEFk{x8kFmmQ~THW>yf`?jF!DFk6!dmaP zOI>a*d>oBQ7$M^|tZ~BKvynxV7s5|}bGH<}Th*R5cf;rgPYw~@Q6DCMXQ9v4=lxEx?Y z9>B~F*^yWBSaVQ1zy%x}c&ty=OL~9vL1_&pG}9055Z!AZlD}pVDQ>$bRI4C*x`E@r z^4THdQ!ftIzSgog73b#08iWzO5L{)ws2*Oo|Av|mmb0Wp!}^d>n#0d16XQ?kyH1zo z3MDswBBF`%PAgneQR$CTB0E}}5wPm5DY)`_dbQk0c(o$L7-9VEb&uCfjt;nR?F~r4 zW|X08cdDTn68W0niYix=`oh$gS#pywc3TyEgMS732UW}2L`R8vt@_>3D@W5iDJu@1 z%MNYsX8IDk=4}QYMn1@LxPAX6qGzLMdD|^&>Jk)Ez!ptEvl28?kA``Gm2>RwxHWL%%BlgV;GOqp-W7FZ9pe!MD&E|6|O z4473v5D-l#p|e>?p4tA+H(V?w1xssCC$IN?nY-_yrdCXRM~B?=zrD1YXR-Cs+7}M? zdixoV!d+3)s~k5En8DJ|6%2g`6l>P{^$Dl5wj@Y`o(50S;U;?c%F4oV;N_W+9pCFC z+Cw%fEp%DP%X3obQrW1`a}qsAdN7a zsa$bfk0l=6LYP*~MZ{E{p-J_T0>`D(cp4Rv=d69#9kx zV|dDPW>B_ATi}v@O!Qb3GIqCFl!;p9>H>@U5ig(GuH7;^GV$|&vXHE93SJ=k8-*nJ z{Y+U2z9E)pTU;yijYVY?DB0%+a#E%ECz3$v=8o#xQY0X6p$XHks*7yW-yAQ`;DSta z`hBJsoe)OLhaAvr{mj?9DIYiH##x$z)bph1IyY9WN7Vk+ zl4$N}KLMpx>qp5&D{xre!vZp%>W^H{2FOBgrV6Ma4)}o3~MlGKUk)< ziD>1T$5*w6eXca`*7Ze$-x*5~TK*0%PPGfA8Tp`giMlaCh@gWy*w+De!ox;|x+ATNv?VLlon?Dpz2{*LtdV@0S=VDSt(OOUQD}_h<=O>oxlk zuoLHkd$e=NO2YkH_h3SMhBJFM93s!sIu?Ww;7=NR*xh?xZ+CsMx-8%-!~9$fnL(w! zWz?~!fVNU%YNj^>Lt)$oeIC2?=i8*CR)vI|o}o&DF}musSMxk|V}zRpzu~RXX;E)V z8j5wXs{1AQWpJ^8_{q!Lsk9kh1OCM1WLuBvuEx)oNiE0E&Q-1SE3>%4P#X`Xk7wf} zxTTKlF>YI)Hg_0u^|e5iracTtZ})^khF$o7s9>`rEx7x|9fbu{0*YIoqIU7$<3>v4 zqzL4}`o-OnUyzoTMZY5Ki{u3b(Bx{>#Cy2o)c04^&Eal4 ztD}W!XU7vrucg>|msU#w(;~W!(M(A?tqva0hE1rfR%FlFv~VZ2UAG{{n}SB|=cI9i zGDk?*!XZp{`n!kj0f8!JrOSA0o<;8YX>c>yvT4wLM8n0i~@G+Dn z_^M&tDQH9tXl*8nojjzET$Y5=vDjG`$Lx% z5g^oMCx&3oS$9jy*BKz?5gRRoO*C(QpAe37LqMLv@4!^KwbC36_^n=!!>ml{0`vw>{v{51;OQIw6 z9jIjYlusCQzKxN!E}8)l>Yu3ccpZF4S>~1zA5*ZI9@k=TU*z4%F z17gq6ET1rLk+5vDSkMDLSEqY1S%oEGHJ^LD>+~iTM8-T2b^DuZHMz%>i*;YrN~#!z z#H<8}+rSx6-jcuiK|POy=?jILM!3;x#1ARtB`i=fWrL=Q3&G;wQERge#wfa+QkR{` zsCms0<6YNvk@B|HwXPkrogY6GW=)LjT#vs*;}unuMvGH~E8BeEuwb&)FNTJ)wUU-U-9zyiXBfPo|MZM*Y z&nrJ+VpcOSf@0|jgsALjSxq-dM4}eqI9oC`Vv6TYwf*>a)N0_B5<6Sk{C?2c$Jdh; z3Q2G~tpc2IBj$C5jqB%3OzvlP`L5RrTcfT7ZUc=t5xQUaRV@^vptjsp+M2@>m3_u? zZ5XsHJS(RIU2463-9z8PCJJ85UzJp<5BJhFb0a83VO_W_{7|>Pt|w0VvJ`K@WkYSO{= zr#=Tx8$1V7m&U^Bxl~h;daK_uq{6(Citui31nTzsp3d+0z1PYFleoH$2V@!c!--a8ji4qf{V&7*B3}Zml*?q_?%HSAYjqw8B9lmKdW$Mu>*oqkqmoBiW_*EhW!Cf zxuYkjXxwd(`H1@UWoUw#lA+0QFb_30xX*t$zpYwZgIe^ z%=(eh7&rKpx!+#{X%F;r3T<3^|H(C_C>rLq(bHyRZsI;P*A8)=3|ar^3Rcx+9x0fo zmlirI37%rf&$4afNbAHL@7=m}FB^eZQ!`L~9beGaf@phi*(`e{3w@T)fXO!{LAerU z8{Sfe>`~pN$%sD?5*kyG8FgxSafk%T1;$-=X-F;@dXoFaJ+SR?F(SZ38nO3YMEAZx zg8az*@Pug5x<69c5mM$)7Fkd%gm-RS${ejSYIVziz3akKdfAI(^cpv2Ph2an7LHxu zBtQD5BDPfyeDggJ8ePhP*@eX{vgYZf*m5YnajS&sIRUuSIAVXo7NtJ|@9BJ>44tsv zSBFC6y3SSGS=(y2E_{`x--3J6PEnIP0NO`;(_)kRLn`8{yF)7O42$_G-#(xmFhV1J z%M1+Kwn*ps*7{;IfsZGPamKDc$XKja2eXg1+8D^pmRB}GzwByN>GaiG#A4B@(_4MsdGImo>sk z10a`bASz9~d9vX&i!p@Ka}}g5F#3G}w5zieh69p|;MK?+r6jN1dH@ClTYpXSKO65V zh6mJ;R<5^x9p#+0*BhH}i;UZwDK}Rz@?4Aor%jXKKhYrpt*K-e*4k==a7Sa~`gIYe zKRX~mVwF5PjF&0LqGQN9-?~^+HvPC-_Kaa%R2Q@B>h8^<6D-_y2LNda^|dT0UyA0EzNxAGgfWV$Ao=i}uev8udPs2{y8=$(#!e zzLjpVFX`<3(ez;in8H#3n9IS>B(F8}kl5VD)y+=^koVO*u|8{nQ~eg$JcL8*JI*2jC6fot=T;^5p= zuL5?R;<_2X=YP4b|M~xa+}UG^CrKN@w{Gj!`_#5Z(3V7hM@{p?SuRNfAyL)ouyw*T@?1G}%V}X&L!|RE!uQnMeGnIUO=E+8JvufPzn2iZ zbmV+ocZR7zRg0||+SVFRdT)3j@y1O~xVGNv`L!87cf)Z5p3gKz^TGl;s;aYoZ!5)$ z9EHV>Cl34OHp-CcyT(01i`IC3sOV%`YAW5AYMezw-}5p*CZ=YsaZw-i1iH7?-uKYZ zce>to>z@GtqX=jq`;xHk$f16{b?1=Un4O7#ysQ1pSt{>%ZoBc@k%Lj*udKC36nOkA zq*rjck;VDK-@75&nuL5wMp|nyukd&U`LmGUwdsnZ7+jQ_d!lO;TnUzJzLkAt%V zlq&!l)A28w{vIMu%y@23Ti}ej+j9aFd&{B#qyJ3N3&}Sq_swMW;oSSYOq)wXi=qsUm@7bZTKk{ z&gLvLMt{3ahR0*N6s*JrMO%;8Mcjg~YgdGpeJs}2gZS$fD-t4e4#uzr8%t*}T1Qws zyUq9uWr*K_(PZ_8Gy1ygI4%(H1*6nsQ!Es)(^7XyJah6zL($O+8VdCfOSDejd>HW7f!s{|~rxTB| ziWxkjmTDLs*J-{M=nKAbzbdZLP%=@(r(%_*BYq4;fjQVS|>hibHN6P})oDo^@bF zeK%4AaopDLyiVgeFF#fWuy6c}mG6n^M^2CFWX;4E)2A4)j*@ouy(o^EK7A7iu5nKn zORZ{0PBUA&4=`}+`logJ?_(e{@A?MDmS}H8N_VpvRhBUx2c+W zlsXr@Z^^^;(~&%6e3EO*Ir!LhG(xlk0d4$%AgtaZK*xU37aEBuDy5-e>Td`P-y%M3Iqc_+`!SZ_DZ`&(8nU26QF74_pZ8LyYN zmgLqIO<#TrZWJiCi0&0CgQf14r(wqNmz=x$;SB8}Li;g`urq7xddbcyqn zYw{l7YXV$zNB6YQ=4J*tyKXn?9^iDisU~y1X;GrF>ftPVrr=1J6 z+z{{FLgsbe6wKQb;?nAE^fE0GVOT9DB$wszLgzzAzJ9eevR4!p9N{>ldqad+7W6Wx z(!Py8yY`%0IR_tao4JNK-}Xb%%_-TVCej6QudRzPm+`@dlIhI0Hf4z9FLv&Bu|6Xf z34yC3dPqW!Ro6ZSQMs`-krcni+g+WP()FbnC{MR(d*w;8=IDQ@Auvry#3T1ac#efe zNKcXkiWbf7crah{t{Fhthr{1${NP5-gYYRvP%lSn6USvb?JxbHwOqc;bV6zag)L9-e((Nbuw8o1))swd_x$N!yN4Lia^-#&F5+mz~yF8`#BsPT_DXB}KS+@KFH7@lGZd#;G z%;ju^ZD{BP4w3HgzP+@?pv72!yQ<7)qfMyFn~wqxs`JyXy~Qyv7GH=KPc7M_t>zde zofl6->3-t;baGUI5>!gwM$sdd6nl<6v&B-%RK;te9y47`$!v4Ll_(jw7k*4an(J-2 zK$6z>8@4)p>}P6qSdd=R&j~jLM%R&1ch*wv*B%sqmb)GIpuyo3DggiC_{Dm@s_L&e z>L4!Q^e{O(dO=>(-hHtWix9_s+9#JMCQARjr=+i+7$@gwE0bUHu>#+{+fRCcUe1_w zwg5VqTD0i9c!>#rDrMtu##$J-K%wH&FG8gL4*DAVF8nYw*_G%4b+AC~a)`Bdb7UKS zurKeQ(5-iR-yg}0SgM`Hqs7nWK=5U&Zl*YoDwwUb?CM48ld5vMPek34xW_%NB$J<9 z37cU3W!?|sDX4bRI)WF8J`zr9$)bFytTOFa4?ZS1@l<$^)-el>$B>rUIS?TAUDMb5 zp*l8iRn$LH_fP+_(KR?{(w6#)IoHB!zb+f>pVoKWgJ~H^nGgB_)o9Bo#=}z_vIUEJ zcT=SL`c-vWTY?4~h_!NL#r1BXNog?xwa!jA;zCk>#mJ7A3Cn31k$tr_+TTT!pE=z_ zmti-`3vlS-;&20ZwhnV|M@Zn(Y(FFFbd^Bk+eV65{s;!&JeejcLIrHQ`_mzj8ieag zbpfYQF-b3`AM)YeP{jktm)Oas z6qoYlfs?9Yvvx^%@ipF&a<8bhi(dd?-eWjT>Xqxz>=!|X zp)SI4f@hoWAG^nsO7=?!q73=k38V4PEYuZLC;Qp}&M0j5-{hY)0-3<_C($G4} zh}(Oj>j>)GhD;Kj5Y(PTc*jw3zxGY!t~+G5{PN2oSr0>gR^lem-ThiY3w2aARhB=o z8nm#7>0#-CRwRAu836;&5nZbapIp*t9@%`pbN)2`OJY=!C7$6sxfgV6sg?RqonfU4gyKn>F=JiSu)Dh|gt zJx4dLy1G>w$yA#^o%FV?_gkx?vz_m97@yx=uXffwHx9{c&JKhi}A2l{r_5tK^cx#@*!@@-jkg5|7pTWGr8C z!}Eiqvg%`c+sP}FY2In5UeSg%NX2obpFg-xM#{l|yeoY_e847j*K@{ogUQAD3VT5$tHTYLF@SP2W!AMM>oXM*%hVBTet>?rp!j z!T@oAl7d2Q_qO1zzh~n_K0SwnNI^;I+{jDm$G_rp@ok`~@s}5A7+Ifhr@~#JK5eS( z%A^>^GJg2qr^MetpFR+;DJUR7M$0X?RWuhnzZT5#RtW#hrD99>b+ik2cTcF9Gcy1NgE>P##5_!GG5r%*{Pl4^=Nx~3U zN_(TSGcoy3`No1N+M{4k|HsyV6yGRuf^tots=xjikNh3<_9P1qVears56mU_vzdv9 zg@$(6m=T=Z;2rJ7CNqqR)PYhrU(3s9=A1pKalQhQeK|V@4L4F+bOKJU#oD>KuXoAO zoZ!GUukpEZ{(oa2s)@f>MFR-}cgaWW;xPuf$u1vbd5ntz(R^C~#j_d63_(EG3jq3? z&PwBOkHCx}NdrebFDQ(qKUakz#qVe}3{7Uq6d>_|{Mq}+fdLo}hub()eQovngK;Ez zbgG1xK0?UV>Y>5ge`jRbzX7OcRUDgDKYskEyYcjRw@dCf+phQAKB7l2sT&`k?3ljV&e!YRiZIh|1yM1$X zbTs}C4)_;3BVm2T>3sX(@RI(YRSe^L0zlV2@lT}y`TkW?g7hT-fi4H35Y<{)7x|}n z(s||gU+#i!K=4?2j-{)ufem`$<-d&J0I;{?M;?D*GuGQ6?bP=H&;(ufiys0qA2PX) zgs^ZhaZ!f5JT$Sjw$&ejasAJ_QUU8a@yignJiaM0TT1G3sS>t!8Tg44o!LyWHtfpt zy_m$PrG{WLGwEBU{FPOe{Mr8kU7Q{P&t@QVEB^31-gO!SQT5Xcx#sCeB?au8LIz1d z%#T&sL^LC(r;I7)0s%OOvkceo+;IiyvO+VVKan{2FG%ghEzs1t?bms6-)y;!>kao` z!~^2(&o-Q9cAAnWE3GxggMUxq!3BM|-70BiTR3f-(gR;s5vJKN5S@X*FbbUt^G@?I zCdn}U)f(tm^E%`z8wW=h2j$M}-l>_b+`o3{>}vp56I0(_oJCNzi$$kbI%30IxSQ}OnvgJ}2cjwlTO+Brwe)uD3Z-rbVn(}7R>}>k>^PevK zX}h~uNzJlx)41Njy_tpsa#78M8V&z1 z!xVR$HmTK6$iJ+K6-p8`{!>{#Y*DfiG!QFoe}i1n0P3c$9z~r8Z>%CM9U2_GLrIw( zAAc9PRpMZ=!emO5|7>k#=j2q3;n2}GnVp-l+{=yw5Bi_&Fo`*xY1HnaRz<*JnmzM~a;vg|$@u2WkBi4jrh$MKD_zNr=>?pRshp zP%Gda_3J#!EV|<}fMzkqj`fE*ss3l4q6f3V)AQq&X(PfLby+}50y;WhNW4i zddfIEJL9wF4vE~HH7)F1^NnX8Ob{~oI-nTCmj-+M@L}>xj@&5nXcjL!`;=PHV)}|;jyFczS`2N ztrVO$w^iz;ZT6MAlO-Hd+jPEs=emWcuP@cEh}W(a<)Z2G?RM_hU)L`mgawewr6C>P zPkSE;r9fo7wIVL7yS#}~*BCUD)Y4zQ9AA}?gn~8II&j%3+1Xz@<3Wa*Jcx++GKDWS|mdiX+$4lTBYK15lMBGMJb-%l0lzV&dPY8P0gZ1U`oVz47t3YN^D^Vl zZR?}%@2`2M7&Ab#q-m4F2Z9bxkM@Ru23|Qp>ng8FyYS+gUspDU!|1(U-w;1>N_9Bf ziu7+Ucjrp4cjWl*3dk@2CVEaYbEW$oF2-^2^N)Btl6V?h({WV1oeA3)wam6&izC;L z6n39P*RtAvJ3a|o@?g{r@hoUk+<4i{R2=_ezq=Zr`9g-wNb3AK)bh2o$yUFG2 z=7P>CprXv|-qp;0#LU4g_>&x-lbwy+HoDi@-tOAu;BB_W8YP)yX#usc3bf_pH{g42 z%je%RK1uW0+1AwdFUan%o0>)jQjlRl&@i>XVSn)iu3mWgLpUtGd*PQM0*H840LChzyq&9Sven^U z+s;zY4sYBJGS>VD{bK$BN+ovE32h-Yq{_k7HI5KdsckO$ANF4s0C% zY_ES?E;vDm8!@SlP1^|^wU`kjPqH)xhon?*_^c;hqCsaAE!RT?qY3+6SC6G{0%v|_ zPUDh2vT~s>Tp#X|}}%6`acXvmOLU%z+NkWUMFm_aCa}kaDux34nJoJ_DUWYm+|w6dJ?MR8SI3Tn4x)*bW+-%Z0_Jci%nsEgh&=eq`xCSBk z&o;*SvA)%MJub!-YbM4|Cd(_ibkmF(CnEr#6ebLX)Ga02)|rMgh=YNSQkikJc1>$Q zA18g_52CuSyn3A>E*40sG)Av47)DoZ2_4}SOm{fD3rlxg2pir;(a!5gC=_2m=~9wg z;+P6V7Qe3OI*rh?K)bp7%x>)y99(P=gte|tXxiZx`6Jq&C7=AZS_ctI1`Qv~4Ky*> zFOy8|r^hUu&uVn#(+^37g8zFVIXD)|`Bu-9=`v2xQ^ohv#rOE8o~qg>OvR3) z%)6*IEWT=!AI2H`ZGon#ZdP~C0oL!; zwtbqptvhzOg9A24zI)VLW=Z~WV$A2*1ziU})gF8^_+4&aUHfeA2RTo4OiQUYz&1Pu zcF(j2>RQ<5Ptd}18DnVm>8(cINrmzh=hl1RYfj{5{y-lGFTj_O*BP4bh)9hdpouy<7_qPY^}ff+c7A--M{86kaEKiYsD8O}zheExSQVRHYKSK#6zq3i zWXyRHeQENKJH9+NV;l2>Kx|P`>*Wf6`;a5VJ1(DItM(ap8~?xJ1z?u^w#U1#SoeMJ zstEw*rpXG~k&HPQ0DXfpwHFp6G> zTpf-M+8!;6t)cUOvJL=SBjEzmD>2=r5(pIBo*BbAG-U&hq~T|0`I0unPo6|N?LwwM-X&w) zr#*A2C4jy@NNOul;>nJA8-rqnNx3<0q7F9o5#9mA(J@_K z6qpI7LkBK(`2FWB{ykVn(5n94^2X6#b+&wG{m&M^+lte0y#_A*?JfFt`O zd*d}m4s0hwhUanfHO9#P;T;sGKz>);;+N<8v%dZ5l2~-f%!3r;0cG|L@JWN#Kz&vM z4@eel(iB8G%_!u$;7YeDkV(RM`kpgH3j|x z?at+~XD}!scLKp7M^b#j+^0FVK#QNV&soo!Z6YZ#zrB-UdcQLIQFe|cLc;sqOR4s~ z?% zLlxsQuOxK50PhrISG!GrKc7vwaOKFBd*I{vx4k_=gH>CqYs!-Y2P24f(l6QT)m;Q5x3scKtJvK-dc}`(3rT%3&uV|XbLn(v)%nPxHJ4^*KnJn=ZplFv z`D|y8EV0xAY2kbDakLnm#Me(Jd*eZTN>&s3g%e|7s)!{NOt1d)LYt64gd6VY@bW$S zzn0xx)}%Se8q9dSa2AKla&KcB3{6ca{^}k%Os&zNT393BDL+#v$0L#%dHaW3TZQ9l zQ(>R>f$_*oWx@LJqiZM)CgSObi>{8vn*of{TF^zgayT1T>DN@RMd>}PeC#=-(UYpL3*vqW$t!wJ+p~j)oQb$cn8%fX(sji z0OIP7f#;laLjuov$^VAVRKbn=>ys$(bB8J2tEiYrZio8ArR}E_7J+}?s{LcZ5)VKm zB&2Ioma?epx!467wL~$0gjd?Ik`hf@p*-ve3prynf;NqRETJj*Zj|(lgY)%0vLUy< zbnwTbJMnbgP=SE%u_^`JC^LtMMag8WuM)CB+CwuMs{=D^WE=yC73PvaUaY5^?ZPW_ z{ORr4$>Fpe6}Mqtbo8B&kVO>INif0YYwlC8-Ia$7U4j3+)<16?ZLJXn3~V_1 zsx;lChR#)I5`4d+xrMoB%7qIT*jZT>5+0Nl+1YPX+Dx*(|JNERHar? z_id^|&!79?$0XI=FLbVOfdMY%f!l9Y04^HP089V0#woSc&>cBVv_^;>Ic4MT_baYr zoIh2!6&TNSxoXnAA*X+6e3NwzP*6}H*2=pyYhBE_^vYjp z7{b{9*>-=SvA@x=Xq`YT)>stWIhN69?Elghn!B86m9yk~R@|y+l%={is#aC#eGKPL z&F5Jn7MLlFUH_m8JL(z6tv#S|h5E*sr4;P0v$>F#x~*BqRC&^)?wUg6( z4K~}vdbLhta~YS7d4AwwMkKryUHFNZgs*)m!a74aN*X|?^kM{-9jF?S000fI{c%fv z-y_$JJ%Bi9Y_ZfV@vl7nudR`u2Wk=`W+d%(M^^03v+!6q&Z21i20}i5y3$|1(a?Vy zDGoD|Yoggc(6YJ5Fz0sEL+*}UqxKsu0}Ae7rw9%qUPCgD`k|C>eyS3SLN~Fr` zKQ;^4kQhiypWrHf+>jsU=&3s|?xitP6IMbRS~(O>L?~5*z2Ps32uZUFjj()T5E1KN zBA!EzX-3n~LlCL;_V0RaZ?D@6A=;k!o`9KA+k<>G#L?SAGon5)wL%To`7gJPTC1x# zvmdh{?eNb+E@^ykv~1x6nXCQYXogk1yi0Dl(LeQi?$>)Yf#6{TVDXAD0kH^*yJi-I zI^Ca~b}lO`1Ah_jldO~c$8B~7Gy+NiC^bObCHv~<*ar{)K&`4rpm5>+k(~|K!77DV z+a0*QeD77w?9LHNYP7EX{Q{Qc5T=e5$!Dw+w7AA|8t4=>T zLRhf)i&U^ygS|H;tokLwlp1vTzB7rKfpbe-`pGqb*k<&b{AtcwWjC&Fnc}9M@vZU{ z=sP_E#B2P(jU3N8@Cw=5f6ky2MMg$OiT(6{A-w?-9lyzlf|FOqD`M`HN$djw8vx7D z8M$;YU%4u>S)D+KuJeT0|3ALIIxNfO`CCd*8ldy3;B-h#XTh!z-~z@iZ7 z{~+vOffhlFeFBcKWz@_CDZjPsocx!-$ZQ^#P_Wm2|>>&l*Q0$^(6wS~t6Z0r@d68eHmuoFxsN{m|A*tq_?g445z z9-#iFkd)cr8Lz(R4`ya_4W7mmC2OQ1c>vX`S_q`A`SvKq^(bzoFU4K_(h}pgFLq0B zF#^1C*@QQ=-E<^ZCyXZJ)Sxk1mrhLq876-Q$vYL)Djfda|RjX8@ z({dMqV>VtS^Hf$;XYl`CmohB4gqOm(|JU3%a-O;b8_0jA{8l&3kRP+4+bAazRAla8 z+|!%1fq{Xs9oLUp1N>t=>bBrt)Z_T&FZ#l5&e8#>^_OP?oO~5Pdx{OUeCJSq|9+5B zlX7`)mjpy1=EU}<-}d*~Z;ehMcc>p-RkH;n{n~MV=KFgzcf(N^+W-3TUw=EPz0+t~ zuSIs(|NnRd$xY#{rw|4L0>b0nnx-Qx2Mq38tG@j_&u1Wyg5&j-;r46Y{sPoBFzj2z z8?xqNgFZZ4eEb}6|K7^DMchmu z&q46Q!omXrfQ&d!McBMeLun5}cQopD?>0w9#+ixMje3j=P*Nfvf>AzLT?6 zlLuh&fq{XY#Ubp!_~}uP@Q8wUh+qb5mLuxWnpvzuxT6$)!EIP3w?RBvOGLEXrNi4drF)JLSLFeFqX>afWIj5u=A{ z0csv$Ffz23iTu>`cIY(^bGu{kS^cStVdG>?Jtd7-d|= z)%?F6p=WTwYbg#5WT0|0{r#wheUjVNsUZY4C&cx9t9-Sih=^Io`v#6?OeK8yFdN+D z?XWEBg0nU7*>A@6SYsy^Tq5k4K(BTV&JOrN*u?VRTbD#24!sT@MWaFT9YH2fJDy=8 zsq=#oItO0Yvmel?g%2nGAqKz19TqKf3|h}XY4gzV=$G8wfo>t0qpem%(ir&N`*TCH z2Vm<~-@#?O{tLxBAGaw*9Qw#Xt`*3AJX$RzE60cvUmMfMSu!%X#fFRuq6g1+Peoe-} z06Eu(jhh%nX=?w}M4o~4ukq?)G@Y_~c@DbYV&aw}t$?bNwW^K$gQ9L=9#y~{qSaG+ ziE3!oqM$>?fDh~ZTEIZ-+YPLw8hiCGw;7aNViW`~-?XfRzO2&AQl}&(C0*reBeYL6 z9?6Y6NCrrOtQG9Z=Cc+R6NAA2c~|h?C<)J{rKLoxRkx(#@S87_0u`#{uWu0Xhr9n6 z35?(5y30mG2mbKopMs3_4EUfv$_A8wWwZaWdH$X$$_%hp-x*ZVeuqaPP^N6_N&WR= zo?fsZTz&j61Lq}`Z{j3M;_k_yQMFY6F7MZp1n{tw%gvM6_d$z#%m5yf=`DKyHz?2` zz_j4eu8~L}@2lX*P zFXNCfxnEsC9~FRxxW88oYOEEq<-`AFlNu>TS$S`9O_mupp4CK=nX;4a7yZE$9A035 z6AQX9{3^cvMeARyA(4Fw_$Ok$@c0(7|4A=$P?dj$jdCRR_rYjUBS91g^$5^kKjAi-?`kH*w41?DdsW zj$+>a-K+gBMiYQD=$l{!WB;NH@Cu(WpsBaE_3d`03P3b#JLfX-p8V8~@_JBR?KcYJ z<*Qe`zTj9sX9x~}H^C7pG)zpgAhq`jxRG@i`%((|>ME;=*6%@C#TQ83S?8oXh;DPh zb>diY;gz?jYpHxgM{HuMKYx>d0EMUwPG8w^Yz@YnsItug1(Jtq9f{z=Bf-LzHMu6&IDRY6>PwVtj1qJg57~a zj0OQ`@OK0R#99~@9DYESYx76<$R@)A55WfbrH*(bK&9SPHG=Xwp!n<@J-4nd&o?R- z-@gLI8?|&>B_&T8W6kbje=l>IXd|$G?ntK1a9bX#1U$|i^c1tSJ|O;C{VjDQE9?nM zP_jWjVTLMor9193I2aFW?>P$4*87h@W8i*CyuKd8s|;y+a&rCHTQlF9V~!DQ-AjE6 zntl$TXB9mSy5w(dmp_J4AeH0B{q!Sqz4haNs=ugAAio&PY1Y44>KRD#n%$=a{vV){ zq%`1i$N0BDmRkT0T^aZ-n}W9$kY@xY0z+l~+V72j1HzMr#l@-odrO0Q84@>Kbt%rU_Nmt6lp!>_be9Sqzjdj82S`w~D|+-!2yg4cr5 zeW-jVsW)NpO}vk&qm(T#aXe)$(|gs_+r#M(EZaXh@%Mk{jsb3OV09%Dbm{gTsUR2e zfg%p~aUG~!`u;NqjfH~~V>ofRK0FBS>elZY!NA1CA|{61$odW<_b9gqhv!asa&mHUeSx(#%ECXKy1xi_lGkvA-1Vb((!#mJ#MATK zujr+{S-+Hh$l!_0s*zImtn@oikS1V8*u16^V78IOL3kiCcT;`=JqTQIq276L;8Y~2 zNJ%$=)Up{Sz(JJqR6iW>QBY8<9^UlgE{^7@t(vLTxi}`jw%1-=v1$4(8^7tWtL7P& ziY9`?R&^&UVv-mgo$@#Wz$MGdBozdGAz7zc9v|+l4d=vc*BnSg==|XlUGXLqMVBOx z2S^cLo~x$txeis^Td1T?9LdBoHI{d6EEcvAy*l2WE;Z! z@Un+LMczfO6eh%%?{f`{D$rlcuzv$o|ZOL%WGDopw%Xr*+45Sro< zNTx|4M4KaGSDdaSumY&U3-vYXZ?*qxACm0GG{jd%4nZdGxbDn=<2{wNTn`nJ@deUN z&sAJaN=DZ`KR-3=hRIe_kx@5rrXi=~BrMX)G4&H#8o?b%`U18X*Au|4Op`{3^ z@5ffqkAAcAwR#JeWyz>7v~*8psk4j%&4)B$h}|@=38xgWk7tVQJ~9w9@_8Mf@g?!!pg%=fo6tJP(Ead5eZEl5p;c%OndcQ%j26XIQ z=uRW2mHczo!<-%Y)rw+PyKfJA;+Yk$kNcC>$8yu3yu>Vqj{{w8>a$X)p(!6JH5dlr z2kS?PyRbJp5*U=4QxFe3+pB*ZzNEE{{{lLigm>JoYE|uaKI6$l4K>Ch(5FC{3p%%OB(FiGev*X>AyA_@eFSmbOHL)?{*qxQS0~LwBQB zZS7e-oN7!nt{(Y#aPOhjmhG9fSV7E(+Oq?(!{%mWpqdOhZ1G13_r0M z$+Z75JjKbC8m8=58{>+Ih15^JU65qp{Qj+e=oZOd|sHEmMvZy?#=<)hMg!^SM{|us8b;)5b)TZ8O3ir5cdXAZ=D&2k_J3~FS zV=(?r>!y!twLys6g?9Rk!;Td1;V#dMYZiIyRt-otDO_kcFNmCYzygEX!ZlHzr2=aS zZ}rsgUoMG&2zKTJMG0nY>4xbb=6-!`KUnzX`IE&Q>WR6sp?fi2H5w|CFYFt)s`8$` zwxrz|8*1i#SZNt}7v&JfBplgL zbIahfYQNH`FVj`J-{Gs~FQiz{LsCj?ZG5v^mk39%Y75~2v6MU(-27nYq9adO-UzE(RAhzM>sv&^)Oj<=CmkC$8Y1ucG80l=m|+x zh#@g?T1HArIx@F9>{O|&(g9cYOA38B-^U!E<7q0ZD0@?LJ6noXJcNPH>qrwPne5K$@p;_GyR6E-k z&!lXuAh!XC37gTb#`WNdSf7<`S@Dm>pwKrB!^b9-UPGVE3|WIA=_N}VX9ve3XV$Jo zqV?QbqbUTRq^4PCpJ#p=A)L%|%XG_j^t^4j+Ew+%Nfm8$a^^aVWm^eurh-D z1KH=HjfD-{HvgQViu7SJD|!|c{@xP-#$cCcgCk}y-v;Wxg3p%R@_lGE%Fk-rAd!u+ zM5`E@>&j1%uR2VizxJ&J|;57i2&7nKddgO|%II)7f{e@1>}Ah4Dl;`qOg$q-1D2dj#q zVaK^0DEwIq7;!uRE}`G2R?h#K`pq8x&X5*<0&8`}t_uLO5uc(+1s^ zE0ou_z{NDuhWaWgp-N~iT~I7467MP(O8e4~;D3R;pVGH{6FXI^y}XX^UuX%^4{e&* zU+y}bw5Z3jLpcA#$J?V+FHmfpKR0`ag7*@Ikn7FnSYbRkAiXXIM5W2e0F(oT;B2#( zrnkb{-(KjAU;fx&&7L5ct+8 z$`MdQ-|gO|J2XfrMzf1|f#b3XKw?mev3AL<9+X2~xMKvP{ey$QV}bXX0=ww7TsROk zXn@6!f~3V4kdm$i{$jRDar#XmH>hH70a3cqU^-Tlr^HY82%KVpoCQsNq^`lQl{8!z z+Y`;nDVX4Bc65I^o+0t>caE76lnwNFj1UONU6#Y|w58Ottss}Ll>1sxq{74F2Lr$c zB*b@YC9f<}w0$MS#bv#tN>dCCddIh1&Us0-M_h{8?u7|ZkT!I0O1>q}cE$xkldzp# z8Koxhy&oDB<2lt5fn`IsrB=_xJz)R-7~g@Ck&(%SN4yty=hmlTR?I`^Xtphyg&PeQ zM2#yUt}ESmd5X2;gKIW#2Gc`?7>Oc&|3F~)QGbE9(5+b-W`zW{+Lo7=zVHq)gSiEI z9tk9b$2-nKPO}xT?LhKvD|y`9-2D4cRwSjR*1>BBK@1Y8e|Vf;`CzeUUv zyn97v_v{(;{rmS*b8E}YrvnQLn6}0WQ_ip*K8uFTEiNu9_vm7fcp3|P=-9DV#eHz4 zCKnBE00>7ZzhvjzFV?HQA9(dz#h@Pv{Oe;Gg78@F8>x^l2_qw94C_OzS)p=RWaQ^z zP{t@J^`5rU&}SmS1cZ%mK%TU4kB1ZrBa;vIu|bR#<1nLisRU`)Fhwex%X3M|Z*{&> zbxJXL@$@t__h~kge11~;H_5>u`=KWQP(K3%M!Dd1qrOld5RcMYfSTOga*}11`v}ZV z^w;dfANslUJENYcs$44F+ng-#Gw0k_T3t8^f~9;O>oGPq7KFt^_8)|!dDbvGiUUp$ z{o3S%$XJL*ZE=srSuI4tx&ZK5UJLp`aaU#eBw_;T^fO4>TP6*;;(7)`BZGR{hhKYR zsOC1JJj?RW8D<0W{)1Ms9rqqfecRo&Y;I}Mko#hcwDlZ)W zl`MX!psRetOMa-p@!@MCYoFVsa%X`uFu3mV;N<;lR|$lWGwC$@-&YGk!e`5E-JAE4 zNKMB8%YC)OGy3A!^#2sg4^aXjIv&VXDSrFeWU7K$UJDxtcf24F?w*Q1b&6Zt^%i~P z@u0P}wZWdXtlZ1a-E41fe|ugCb3;`Uz#OM3v3#%p;KR*(AVV1%8rp1*q2GP?v70rE zZMdzYBdeRw6wo7669}Uct+9(MW^SuZMbeill0P_5DCgA}aLr{fE6h{QTfs zL-74@G#x&U10N zu;b|vzlwE2a)L0OJL>aRsBSQtxGMMvsq7Ue|Ke%tKDw+$`l)Z#{xpuL@Lvy>esS{Mt5lhrTjFlMDe*HyT`)2x8%1 zO;B^JY0PWZ^Iupv+>z$OY3{)d`E-Glc?gk#eJS;pRi?izO1D=D~t%Q-Z<>bzg0pmn-+*7O>m=-a2lr8Ef*21~(i2KweD zg&l!I#LYbdBu`sy>TZ#<)@zcj&<=#}S4dvxGS52W?a|mat<4++7f8S4x#4S**B8pz zn@^xUh(l!EQ@d9)UhVE%oDf>ed))l6LZqxLXx(RR70qVQ*%H%A`NPyAZFtQ-vhWm} z1~(%|J-i4U=dO_(t@?RQ?r>UuN^U?M)HGGo9I=_kMwvx;&AOR4l8f5LtNkpp4R+x& zA-4;LiqY*igyziTLa>{2__M-O_BUQSCI)=$y-m0-6wKTFLs>e%hhb;MZ8V#+^~ zHgipf=D_DcV?HxnN}BYIqF}u5H@s`cjUpH!kz&7(S_w~#g<1S+)YadEVV~41GIHvG zxWv@)5DM;p9F3bz-SbHHQHL(L7e`Q3RI~@y(kI6*;&^a=9>+{+cn>sAq;eER$=sMT zkjKYSY0A3j-4ulWka(wMbHno8eT-vrv*GGpbI$wg9QrK@yGFdNL;;48AH`$O5qKUV zd7FsqR*03tJi?q%d>hK($GK56ji@%9LuY>78#+Jr<70fRS!bYMFV#0T;j@%?e3^BZ z>V)5GytI2I23#swj(dF*^q#L0su}G>mRg`(Fd%GZe=8q;?3}>0>%SH9bZ@@w!xT%x zJjI43ve74n<)j$|YfUaMMuf@im-;Vh2d*~RBbYTssVy#zW2FS1-j56&YuW5}y?!U< zfQZ}tK5cgso1PoLmuu@j(czf@uRL{#kjSF^#(^o<)HY}SG)}P`?lkJ?Twqs94Y(+XdK@gPvOw1Yv=sVO|}kf}r6 z?b+KhI77v+&8Y5`Ye!NhX}H(NrcWWTis!${%Yx3e^lFJnGnWf9#2%H|9;Emo~wDmONT4wd9x`-N{YElv6ubgcxBsIAVuY6En&h$3&>pTEXK zNYD)6fVpSqha2#<$@|Y8?Wf1b@jzE(e^C&(-6ld%%gdRthqLBS;tFimUXHnq81tY( zXJW&iOzuCNZT0#BV;N!!^G?g`!6U*4^TllHFhN1xG>?{dHfd26qTXaTt0csl$y<9r z9iAXaxgVilHz%s9Ah^I+I8pJPI8E5(Se2c_ZnUU2hz?FxCqJQNtwv}$)Ju;=>d11W zadvL}O+l^uL~Gxpt~0N^fMy0lR;iF&a;Xx&PML@4&p;C(Tl*uGRk>r87ArLk5iLeQ zKR**?>o57|bOwepW!PVPU zh~?9EYkVrKNMb(GH)q>6nzAhd!OWMvj-Ocrb$!FloAJG#8;~34Lu>IIZ)!(NT>T{X zgj+^TyngiQ`Mn;mrm{61>b=MA`c!^&Cc`2JRnaeq38l_fAale0#jv*(HO{YPf4eeYGJf7Pn}i{>%~gRK;p zTKibL?29iNWT4@{Wb_jmViZE|0Ro+(dm2?*o|dC?4xP;h!DR9#=L*xi%t_~&8^La4=9Pfq*4!I;MH8+yj=Xzmcn`%RDP&%8MQU!D<0mE1?et%{|)FZYPv8B6np z`9SY*q#r_w(FXqjo2&e4w#-7)HRb?E!}h9ahlKA8tvstqTD#8BMWNa1`ml;l`@|)t zy1tOu2kQL58TR{G_}rFrK0WkJK43Lk1^tXQ}k3im8cdg zDYYHd{ZCSjf-fg#u zm56{Kt+3!@7*(87YJ=9^oq5&jNHYYk%Xoh}UVnRq6;-fg zuP!G0m5%Qxf-ohu2=leUk!^*LF{bdjK_J;1cpKNwhdu<(?1VJX2aMt>`w^7e z>-v~t$9G{Ej$)FpwsyoOq9JJ4i(9I`WiyaygsOLXn<9;qk7jy&J-CjBPbH&mo2JC% zqEbi`p1$|ds&;PfY9>q{ibUAFEUHb3)bqsYYz^rwpD2X2amUUwnT_c|p6%!P$cv9U z;8LmJTS60`Vv`e;pq$Nh%@9hid;athy%UP0y3#^z5JD4;G{t+Zmoyb_lN>nX{ofw* zP1n&|jAlPj>f@SjR$U^GeWa_F*P_AZhP2K+zy|ws6ZbiobE`Y^n53u?;x5`Z<{cv* zaf!WmdpmpM*!=y;Mul3Qt~4H|N#wM?%avriXmmKd3JPLBf*Pt}!;k9sev$Yuy(o_X zExj=Ch<>XGG-?{d%Mp1{%WbCVBWjG8`??Cm+o5h5`fuW!bwp>~O%b+V<*Z7PQS z{<444jQ3|OOH+g4EB(`1RtRc{rdMMET@2@~8pfX~mR%?6E?bZ@b4CJj>Hm6eLn#>( z=qbMdnd1{s>i2PzMOl2#7!zby?`~Htq976R-~#-rdScL;52Jzs_6^Hhje>(u?g1Vy zR}mCMGX~@G0~zD~^-r!VtFNQw9&H@ODSswZm6Ux#+m1--2g32wk_1FZ5ajV^5 zWd3{ld>SYx11*Kb#LXQ_w4w8s`g4rLlwWQ=c&@*|`H|UP+l0adu5I`4i1cS%dJJN% z%VW#s+*{8xySIcB&+I-Zv9yFYzqBN8HmWl3!^ufLjh1JdQn!gKtCzLgzdj*n*0s21 z-jIsF-q6!h#c5!xK|3mPD`KLfQ95#>M3%Z@-R%lwM7{5>L!k?+E!TI0+|tF(`(ne@nxu2JjJ5{ZQ6Id+Rg z9p>>9xKQ4T^eZH9ObkU>hY4Rl8#xjdsp={gl7B^ly%Ws0a+i>P!R!281v*Y@JJRZ= zcQk@+Ii**U1rL0G$Lq4%&=1TRKam9m(G8(HK3HvJ`9l~I&m0i49}TBIwq7Du^UQR{ zG4d*7ODiVYwyiQkZIjX?J|m3dcIz)HPvU(yKmGPW4L<6(s?1GY1yjg`M8JW6VLh4K zh=cI^Bz+T65P72aU*erRPN9;cb8Xi5JHj?4Mg|Wdzmjz@h#X^&pT`)#E6AJdQ(S$x zk-V;2mqaWyA{`*1k`Szw-4khV%=@Iw74g-X5l5v%Q}MuyT@(A1%ZC)G4CQXG40teB z`F;EcSQz%@FvWRR>xeyf<@lQtV**P;QW|l~g@RrY%fj=~?|6t!6WI~HS2x9`TW-n9 zgLEynPaI;>Rc7^{i!|16ykat>5U0F+s=TD2L7Ul^(|_(AqmJG8Z`<~p|1|e*n7<~< zQGMPN>jdrFP^I4+>+FK{#J)$s@_JMo!QYel^l?OV?fs-iLZZW5J2FX^(tD-GpCVSX zrB}C6Kl^ug1_}oZSCWODgAlCOoNpqr_glWVsX_!cFXFYi%-7Pst_cI-waGfs^_48L zH!?#L!>^JN+O^3u8pm6Na=N&yvEpt_^0_ds*LpyElJ8O}F`4T|IoX@pyhG|!o61Al z@G-P+xSV1!jm2*K=lukycN%!hV(g}yD?^)dTFiFWj~~wE4vvHuuHwAtR^$QpYJlTF z+Dzbkn9Ng3JnlNN{clQo8pvo)2lZRF)F%942gh55d;e4czX@_PNPUZr#=;$)BiNu- zlz@rD_ijnSf^@Z?Y$;`udckTBqGYfY=W_==`a@V=AVwfe74N%T*J8+H*wOZ~_R!h1 zkPa+bOsd)hb&DLkWSnxs{0LVwT5a+=FB5T06VVd?X1A@@&vVVHYQ;wnEx!uck*X51ey4Hn#I!9S8PGksKENmapVhL6mwq z_gg44M-N)gVtBINBziWfui|}S-ZkS@oK$C#eKJnC@jT#MO1jVb)_%CP?`|xxr=+k) z9N@meW2x}n{*`tdW|ezAq8-uF8c7}ve2f$99#1KWebtnMbd+p@-UK}8_+A&WK#4C! zQ;o6H`tGAC+tkLPXnu`T^AUE+cF3*~>jYm?(V)1>b-ximRNmk%_vxV?oH8tc3BuFt`(ntUP-VC?dXF;pm^htcs}@5~00GG~Vl0 zyMzU4%mc^p{%}?(O3o`+Q?XMo^N%TAxu%bsQhM2`Rv&uuj`kdb!oZMN1v+J=d?G{1 zZ7P|$R(NwJ>)wG8S%Y&qfktjtS#7stZV2PjIky+Jth=>Kpq{(;O0fLN0GoMt-C8?KLJXNg#~tR()WS$ynS zo0w(J;IJFqjsbbI(-JT4tM?|h>#VlJ)ns$;3y!HB=QigRTz$5NfHT>C}A_6HxQ zs^>j)UOBSY!U5IkxpzU-@szzGZ3G!%{n)ow#)#nRdE(JM$KPh~-vimS8|MMMnnLr; zYsV<1nujRfPEiV`I;DBjpUqJ-#X3H*N!x6igdv%>S*y91?&-uT+i_s;Pzr5jq=m?Z z81sH)ueaDynh_KkP0834SwDWaKWaxuQ>@P0NuzJ|n6dZUh|)k+k&gD?IQzeX^&1K| zlv07@lSyAqtL8vN%==(^t;X^m4c2HVoL|o7AFt{6E9|ZUi1fVAYB7`ck>$|_l;yW- z($wRi{s1_{e?wTe9;<$1%a;4sx60}giA?j+JllwB`{^|`-E;cU?EWKD^G6%vgCDYC z=-1@8?*z-Uump59d|j7(Mj{Xc?Y8?;!u;;nKPdK}AKhr36E#5OE46~RFaKKyRXl;l z)`fb)QuO)>H!yctnwr~|%-{1(BVIbC@RuN48!$Fk);z0#6VYuZwErw8+Y{~`*e$xG zXYet#ANu>DAf+VA#IU>{E$W~+0EF$^Dht_y(ViETxYI!K7z z;@QtJ2oy=O8htWQ=1$qQTB9_?Qdzt|_Jz34t)eXHeOYYiWj&c4-O-t;>QOFrfvXbL zP@yU0ynIxbbU}<>#LKkqsbtv~`(9kV3MYh@Ww&Pobs$?LOA>(`sOk-KAGbuGVP`1DlU zzwE9%^XAk=1`4m*cgBGs;ZIcXhXHe37?y9nq)XO5bm8dwB8)fow7s4f!Wk&nCC_{` zLsm$@X{$%Y)}0hY&2R2GE>rVVdA0 zvEz=o>Tz~5+NQPO`H>KPVT&xOM?C!b$-Bvs`79hw&n(%Z((7l`5zeovp&3*%q#1)X z<_)ZTq&U|TBu1!SDHpfeF6TnpKk3JEOZZmSDQjt)KQSu&zHB(($#-;uc!whjHlNw^ z?G>lS;u{Cf|CaVlDA=emWj%&x&zuPEFP(&sCWH3(BD0o`xj?QL=3!#+NHvVe40R}< ztmXL1Ac>(C0MoBitAm4M(5%=SIo|^}F@Cb}g5cOU_@-0_)ubA!6 zY&Llo1L3LoyqVZ@;?)zr8 zlF@gDhhR(f?szm6**VJdb4Qf*nI9xoeumnoMavvcH1}rYUN9fMS3`e1zLsTpych%F zYgszZm{DDlAsRo6XgCK1H{h+XX3A=(RARO?X=; z!=>EZ3L`U?_H^H1;eJ1L$u`%+cTS__ZCNNqkuq}$ef2^`GHMsIFzk(8!}r>bEgFl) zi}xlYusGrq-(^l5_NSW={~&$}on*Ymw3B0RMMH`l=e0g-@#2}R9O-@!U&Zo~)tlL= z&iSFM!=^)8mnWxUiP0%=JFUEX3%W9M`Qex7bGo&&=}Xfm`(3lI@0)rk4m8MU$+VOo zu#*to# zbr!%_qF{GQ+L!OK(u zj8GX!e1QFXQ_d!C;yhz!6MgXC zv$oO`T8w({O?~Cwz2Z-4suGsLtwL@&sLdri-ab6o{XuJydVpvq+#Q{`I^^nb5=Rvl zjri1L8vV*mWUST}9h)&Wpks%=#lE_(DQtpoLKP)HAt*-E6}eE2f|l1DM$v*lYk1?~ zk;smA`KRxjG{LnSP;@g6u=HFBM&cXN`k*v zoD7fQrUWs}6QLmG`6WYtIzvc##Fx-B}KT^S(5rPj4grNfO8Ep}Mg9T9zvBahy!c<|Z7|Mm*WqW=?J^LA?qJ zlZDwoc!xge=uK&mGE;cxo#>QLE!E3`bSZwM`%mT?CVU)>FO2CXpCHe=a$sC-ON;YW zU_fdQ$rf1hn7L0yK4mdY)tgZyyS>4ThEx(dyW=qP40!EMc#3yPHUx)Wp|*uC>2$}Y zclXyVrh8_ze-Ow z0^vU*)_gXofe&(D?4NyYKhrsaqPsq$w%K@#ne4EF*{xA?_dGO7)K1!S*sW=PA)wm* zt2O4V?*I$I&UFD=5O(+1ixw%SgU?iGQRP?=o0d$sI4iqE%vO+h3+Fv>FtB zzBzy$hgZC}{KaDo1L`bE> zeA2jbodc;sPx3qcoS4NcHzMV;uaU8p+7Q?LZzvCc;0E-odiqb+-y?UjYk46f-CUBg zqPvkSB;MOD@=@=Z79AIo`w{VXf`vfdglL`R_e()9(m7mCM{GFOHWqxUMVrJdn!Ltm z=RFX{7d2BT6L=U}E_@IRbq*hPpRLSR>_o5ZhMIfgWLyWP+j^lrn1(G*GaII9xyA{g zfNuTq*}>>Ud6Qob-omZwP8T+PLoVk*p=k?r@S37GoED!K95+RpW?*C^{9Ps9#3V1K zqxsN+mC#}3B2x;3i*u-x?{5n&C)oX~=25!U>dstrW6M;Yk0VwkbS|KAo(j z-Vt}kZH4ryzs=Vf`g+}c_|U~@DBk{0RxrZe>|cfYT>O*TUNP)G(FI*F9EnDvqv!k$ zgXPEA+$bWR z_>H4V`r_xGZOPFGO+-l3rRkt~Z5O7e37I_wIbv)c`Yi;1qmrR}u_8c>8)(UoC{1*6 z557pjmgYU(IiatALO&gsI*qWyVL;3GNwH6AZHi2W89JfLA&)X1q$Q+6TdS z4Ca`3A49#{NV)bfl}$7KaVbbYhpfHg7ucY*4JV0Pu6Ms6+9u+~-BM!}D#gR6Kuom`8-Xfzu)j^0qh_*9F$?l@dm7-x{B5R@IB+fjApG|t+ z)@Ql1k4XJs0F~z4h(F;rMy}EJ?+$krQ&E^3@y4x9S6877J}aux~=mkOztuaX#@svU!rl zWOaDq0lfDQ_q?&;Z}u*zDI@k|wx=dc`mN+vSFikWd4;wfS{9~Uc<~%xB6x?Mq4zSh z)MMy&a|Hbj#=#hWg>n@jJSnMxA+mKIYcu>CMf3ouX>W2P$c9Vu#NIX9Dnrtn%rOgF zbZj2eLgzQ(A#3X`VkNijIR}*o!IwPS@TVo#W9}ah5WG35@UrmJQ+K;5=_zB{*$~$E z=n|(dH%EVLUEmgZ`-uPqF=mHHqJz7X2ye2^hx+nqqB_s~_;<>a(rOtK+O4eI(rQ@N z$1$H(uI8d!ddlo3bE?XE+|p`rhjC{?&cb9=reG=k*$xieb-qj^oz|YjEFUtqqr&my zzaT>8>;9NBwvm6Ad<_vmizcDA)*a(Yu$$yR_ zCY>TAs(ODxn(%9Wo^zpLBMxqzHa=f6DxLrY^3tRgdQN=0kYgL z+yWBI3c`^bON-@}xNkhV|ZSZ(Dj*A&dR>$sFv>jCmw<{99jIM!&V3#-M zr1&ot1?2(M_sP+P4oTv#RsABBkY+cy6Bc!vSGciiU}1RaV0e zYlRH!>QF&kk5 zv()Y|ifX}mtlj8{bvbD|usewbZOoMnN$^m3Nm^)s0yVtuPz}9gr!3T(r($ZNYcZDN zc;HDTHFg?v=9^&03^o#*cB+S=NT&s4j%&0<>30!oPiK#+qDqO|PtV_X&HqkA{*E^V zkSXSt9?_&nRS}-)e&Ug*ezHu?IZt2*N$|76Om15vuh$vqZ0o;x?<>x2*fVBJE%N0# zUoNaH%ku!Z2_BTJw-b&n$x{Q%$!fAsk6{wI>#*9U-)mb7aAsVs1s44J%4g8ixK2)eE6u#azkqDU3&a2pF_fcYmZ z#G;oNR>nKhF8f!-v0U!ld``~j+dHRW2%Q4L2XzjX@>D#>gGesrXO0)e2U^sLb z-t8z;uCrk>RTO#>VlVVAT@J^fv^H~C>gw~>PzTj%$eZ%@G;Xo;K`~?b(LRT!`DV*p5V6A3R)NZTE}4ytg1WpmGO3~-7>b~2%A=er z(^C)6-}S_)3$XH{k&;BOcE*z!4qE%!K6ktc-%Kal>PgUi5=q0<%=W{=S;2xYNWSyK zgHqp4v`B2El)nLkd~T>9z3FJWbys^!J-V~0s|Pd6r_9@@7f4TvhfzGXcOlcVn(Sn> z*Duk>$VGdWgUJ{n5wD`H@@rxlYu)sM643pR3*5fCEafYQv^yR4e|`~+0Lia__;5$l zCBI93WfZZ5{Z2H5(ez2I^ZnU;<>04D>{o}2k{8wv__)oO>K}%}79B1^dQ_v|;(0#E zL9rN-e^RLQNg`uDc0a{WxTNd~xzskh{ThBKOe$st$)ZrtXhio8(>#{ zYkAG4RONS&a2@gaVr!xJUtByO`pRO-REXv};aPyok-)Z>bcNL=MdnkfvVNycl8)4;L{)V#Y2g(luUMLHh@2>Y^0&``0G!p|4 zom?nA*Z&rxuRxoT2<1|Ck4sEoA!4GE2+%zy2r{wM9dOrRogbsGNh^hn`QcuCCafM0 ztyK+fK2&~#zaXd(6kB9 zU?=BpwUi@E(WbF^rz4lad289KW=grugo8-wsc4eYZi_a~mFl>+X`bNvraHsX(r-Xr z01x&#C1Opj#@wQQ82g^+(h}}+J%YX=UKYD;(xmq%eGRQIlGi>P%d{SKa1^&rVSLkJQff5(Yt?WdWdBPs+4ciI@vM;=a#Rc z3tdL!_3)tg`pqsGdp`AWhKRtw{Sw1oGyQ>U-COkvoy-UZ^eAK5A39gE$~AY-N)vtmMSGd(d{`lm{0o;{;uL$*^Vn zW7pgY2FLFMwqU%zzge^a`;2!qJ%4i%edJz|OV-4AHOovzZBCwwCZgra5}w1|fKq=8 z5}l#XR%%G~)(BUQV)iu$2XbHZBz$Wi7nd>o<8f;~90|sP&zD+opXtPR@>HJe)$2?@ zDZs31&gOP1{1VXeyvBqhUKmb2_l9{Mq7Cy(hyfSVur8CqLMwbZ$0b}0c_;oErA1S+ zI(A3l6rk|a#Av#NYngfrZ&0dvCn>%nk&>*)zH1HQY6 zM-EdwpsY79ps2O^(lXT4kT|Jkfwf|IJ2=b@(rMwc5gJppRbh(W$hSTt^6h#dBQa)+ zF=RRK@-0CZzS?@}wQz7+FYs)CV$+`YF=fei3-|^cECn>Y&#MDCH2i1T+I06B;Y%UZ zw1Ud&J)6)Fx#pP}!mrGyo+A^iFN^LMo(cr&MfkkVF`atg&L_5!3$o^#6jQDw%6 z#&e9$Adv=9JtH!h%~4^MX1PO7Kyjf8qh*-DI5%DlO967W{s^W=?x~Wi7xeV*{+L0< zN5Ryi^dn^#?5oo)O#;qv28$ngWYt?v>o4j$1=u@$UzeHWSSS6!A7j1m{QuZ{%b>Wn z<$X91LLgXx0Ko$U2p%N31r6@*?mD>dY-FyvmJ}mD-gOG^RcW zC;VY{f8L-jY)g1i+za+hakwmGTZy-YPHm(|F1JhOO(xN?+-%ue4U%!M!3o+kK|?{O za2K)D9`hmdPhf{y5Q|pqvY^8 z91hiMEsk9Joh#+XT;>-(?OgBSvpHTKcSX{ydJA2)PRDs|k2iO?=eWUT+)0%wg*{I1UzclywEw$oiO`% ztXGa&r%m2hH)}|*PmcD#aDh--X{nGGt=^~LoUwBzaKY*LrgSDAx(63g?et7v4ePY; z#XCHnT3FuZzzO@$#zxdtWK&`lkb3U$J~;plvE*!Qpy{wVwEIrujEe6!hKZuOU0S#}u_CdjHGA^QIQR(uBD%rJ+O>KDoiQyR#ANN%GRdKS3h3Ss3QIpjFUU7Sr1oP~ zoE>p!n5rs*kGQaP_@`Pq|Bg4sqDZ!x82YoRHx$yAor+!AU6VNyZF}GGpay@{uc_Gq z2(?~Ax~f^8sWOVE^o~7mLBpP(LlY-11*Mgn z&gj$Za$hp;3q|a*rt^Y&cdVzimXG0+07d}9YLtaT+_yn^gI=XB3g2~&(bBAp?=#E0}m2#~6!J22UeiMFrtxA8U4wmgsCf$=jqG&ZRL9WuCG<6Yh?@_*9NnEvMGHw*n(-iMdgF`gvH&XR_q-ulhP(%0jAb`i{?;L z3v=~`&-%D-htIFNqpo}U16t!+nc8Gtm{N#4T~EcaiSMTdI13C^Od1!vAtM}(<)FSs zO@nq=dB}0ndGIVg-gic=eEB-ieONgx$9KAu0{6AKlSO(^lQNZ;2q|Zm6rnKl?@M5ZefUb-sX=2`cr%?(5S)?V3&PJrFt>}_1D*lz>_9@gOF-W~hdoOr>08&)L8b?UTG2zSJfmVyj zC1ZC>)e2T5Pp`O}{RKgi&_JhIL}Njp|B#d+g_mU-OkEzkwG5@NZ6Ke3IKhsu;|e%- zN?UhW+2Lnb-HgKrv@rKFv`Bn&1SI6%Zug288tkKp>2Bt8GX?CZDG%19cDUm0QG)aD z=%;;pp3MdeFS&#zRp8MqjY8pKEA!h#b8sH#FV0l3Nd?Jae$U-wm3v_R7MzNaIud$o z8`>gxS=}|34C`>$PW5&vgZjb3fpLO}>M$#a51}?b7^q|E@@Vv}VH> z2HyQ1X@QjRdh4LF!U@~^nsD_|P+OK*63UUdf;3Pul{C?WKghDU&puy#rSb4`p1!tH znDY-nMim4p+#%Pzc^l^N`p|6#5nI8$CE(0P*8s!6#l*8d2kARDlOWI}7+Loo%{;91 zeE(2WYkvOd)S|yrm;syrWY93xzwRyb9U}wml%3AR%}r-T`);Z`qHCo15vcKgJk#v360QN+L6YnI=kmJFEm{7Ji(1R zG<>pmofKuLOZSc(0!xgjyJ4xwS}LxQ!ls|^c&vD*ISrTIeZTTRF+lL+2&-*8Xw^S1 zUvl4qF9U(3BsIO3CJ{Uz9<~-NJ|uKSnXg$nO|$Ci$B)#QAe|lR-#34Cb(+>(xE(_> zT+)ZF>Q0WmjYf*>0HeMMMr?%su0yo@C?WfZPQ=;n#w9TPAaPWEo{feVzt5t36mS!SH|@Vz_w~r9d5Yz{C#5z zn!Q4!b$pA>s?N4y#$f{?sk2o4QDJtzGYPp0-K)zW!m%kAk6ga$l6P8Y=Ux^sFc?OpV|lM1%D@hkh5$oxpy`553C7 zDTo-@Od0ep{aJ~3Wi?24XK2MyJ?EsGPt=ViJRIH%F<2Cp1 zQ0N!eEY;OsW_kzxe9mX>w6nX!a@_IE4BM;wJ$;={Id?_i@i^3WomPtBG=pS^x&(6U z@Y+nTXani0LzVvw!h4k)d|qtLFWoSEZ)jDN>?aV;JHGE9$JG{hORy8D&7DWI%MpN74{3+`F^`om?G~LB<4LSx$pLiQ|-rblgasf zL5dc)lU*bA!r1w|i9nvARqyLU!fZ2|J6=5E{>95hr2_IAEsEgC?u{VCeZgR!+kSiU z^2tRS9F7~LXu)_jYO@c*t@9cKpQ}cn?+b_ojQ{86<=uYJe z{^{rXyO=T~|u!@zSOYUsnvF6tk%WUEIx$gk5a?{nd3%D4uO{zC0!dIroG75ak8>Ha1h)G5xS0pdHC}WOcs-lG6=v17W zBbQcCPJN}V$@J$=CbtSIDhH(H`C{0m>UqWu|C)I3m!@WSwVD708~GwBmCNt@o290M zN@4|#l_F7KIagViQaP3n@&s&rA33Sos>yFV4Q@POnp4bKMQeU=`r7z&b0CLLABYId zJ3&N@s;T&>X@1p}oen%}(=@Vp&r>z%r>y6nZ%154+UFZlEIuE^Q2Mg=N246y+XYTl zzSEClE{NYuE%h#?KFp5vnd>fAo!0Dn4>~Az=<6}jotN6ZrPi=dXOJd!-J<>_RP~(oNvFNVa;hNQ7m$Ihf&keC%EL zdVTFxq++>49VUY(tNNY@xaiHYcx@BP*rr)vcf`4s@fy9jbvK2rPJK;86YFd#vIgNVv;yJzME&Ff`@TrXk1^2#@z z$vTbTm_HL)gi$0l8ZXeeOx=-#HBLYa8apa%>_BN8ehB< zs<%V_`>Eh*=$8u{YJk@#i_y@=7jdQ(?OI&g`lv0r{qeOsK;vY3FGL`MT}(=Lhf==9)(bL=fZ=0 zylsCa(bq(IkRc^bTX+iciLf8_BpU9x8(};-zp78=`o%Pl+6r8^=g*k87heO3)?4fa zoB^GzN0~fOJ*7NxGXZ~&CHW_aN(kM>FBYP=f-msWc4UgZ|BhLW!Tj8n8ZGt(LDs0yetS`9Dk%kbV2^0kIhzj0!!3fvIb7nhnN37GH5 zTUQlM0f#jT7NNRxPB$UT$;?_x&${PokErllReENkct?LIxGRa>|I$*pI_z3L;mjY(;5+u4W>?At&j>vVq=OA3_9Gk*Vw$@erEI za7L)=srRjl@pQ7ezQ8zkiV=*n)TUa-`uqZl!%5<>ig@)2KLlZfTYJxtBLMK(6OpQ!EoE`J~O5^#E(o6(8E!i^^t@@?kf zJEW(1EEBa(a%R^UVEJiT+XPqtf%G}k&JU#@RuG(iU58Pu_er7PjwQ$uM zjWf_s3M@B84u+c|HC7eE%j&cUIyG*3B?kKuiC$xu4F8p?8-85AquS7~U58vi%Qphv zBcMgj%I#ruH&CyXC%qM18Tk4?DwpyJrRYUbYS@8B5*jH;D*sGjDnf-Ip2+h<36hiY zDk(HX$Fr9Huv)UCLs3N=x+!cv?PI!~?UYww)J-d#iJoCJ|5^+fkH1u@{1$|9qcQ2Y&$%v`q+~L#`i* zF~z7#?^2b*q+t1nT1gA8DIb%3;=ort^(nftOlY^SKgI4Ci(dqGD_W2KmH^u^_I$gmwbpFrbAxdk=(#g30cswO= zwWe8CMX(;ft~!8>9nL(fOUK-TV6bZau>_Q9n*G*mjsTC@g#?Z*ER#TxQU2`AdbFWD z+P+$yl81fa{7V!9iJIE^(VKJ@qVm^E>d;QG=BMXhD6t5fncx?x89W}__yp{Ku~u#(F}=O6G32Q_8IrfItM-Fa#~goyKfvk3 z;U~RsNBC)v(Yp3uB7YV*dq?mm4*biE=3YRl63aOCkG{b{39nNt;Br%RdsXX5(%@zR zx89o_vMGJ?nlh12e*=5;ecwwltCuaj!#*(rWn5T_b>{}R|VcCYanXxKfK?snOpuq z{q0}rwbZ;Gj6|`X?IeFe+)uHMozL7Xt`0R=C3gLeF3l%z)N3+e5Afdv(GzMnUtLpA zAe?sm=uo9xa#HtM({&-$z7%3<&X>Ub()OwAELflcW|lJrTFl0r(yO{J$?dfJp3tk_ zkjE}%)JGCICz_+DIeUK+zEGp7oJ{=v`cF6H4vzPc&Ie$Jw;hqU1p|$nHqc-*f8JXs zh@K&)b`iSfcK(S6DZ1L&ec_G*T+4ggu}h*nuQ-_Y_>g z-)aZgZ34*auOt%~#ilfR2oW+LzE{9Xc^w_NC`u~P@ReCY|vE#45!3q|QRUPoH)v;siKfLd4&P)$Lx1pCk*-H_YjtWYRUm>1Ff{yL0(w5N79BKDCSb{l_i# zl!KnV~51WQML3VMV2)M)a4$z|T-eW5T^o zD(;h)Yw<6&!h;Wjcq+^ef>tZb`#V{?ucK>{3&YaftQ^Xes`yalx9;*Df#{N_iwHn3R7i0xOCmc zF3_R>v`9}g7pMSWxhyT*ZL)SMiT~GB|K~(`I{tY`0=zAJ8_+5~?xPkDW?=z|XP?kJ zI=V7z=&s!V{Pthv4_|)LJT-QL=4N7I>gei<8SOLvE#7}U%%4haB9wN9Yg(iS)qwtw zq5kKv|8*P%L^JGIJZ41yQJ(vE!OGtUmQT&BnjS7akbesfnB;_z{QTVEPTK#PQ9uJF zSzvq>^5$DL|M@~dYV@osRK}<_zBb!qxpYORG!zpPu8WImdT4P@cHxvYN3n%p-}8UH zSnm6$D@9 znE8F?hR*#EAD`e&i%>~QaKC^`JiR)2Z-M~^lSQCl!5WViMa0kp+sj3*)D*;CSBdDE z7=$On*qQ!LBVdjn2G&k6OgfEFLGzcr3o?j znk);Ar;s2bpFl0#z1BJ};(MP1&X7BO#|Q0fNc)z5s|b7L>xRlO5&v9LvRu@Ecy0Gj zeW7j^Ws3ZN&kSRDPnTYK&hR*IPBR{4lpRk)WQWVtA@2Gch?>W2#Vxg@<2JL9m9=d` z%yE5o_6i86N*K@lB_$|rqy|?F5vc1LP2E`Knq6?6CB|_rcUiJ4J3L+2vt*cdB&1;e z_P1Q@jv~+5IldEMO&zzo5~d0ER|&JLz!v(4Cm>tDIKF8vX44Vy1dhHr)_EG9@1a7 z{^|uF_2pZ^ZU!lRhf@cpN@!+_f{!t&RE(TmRTHOhXbvO+_j+nZs}45x;g`D zj)3ys+PKm!sF*#`mYxHtqo`mrjM^ptBB>cS201>HcN4>6P!@)()qM>qRd?_8`)3$S z-J}l-^n?VVYuYtOjO++@kw}OH$_ha5>7D0RQ4Qa0e@(2PYP&vjaiP$;=eJRqo+)9& z%!#x<9xs#ehq3Xi?${&Mu1X9-4F7CSIAgFHnTn{rdCYKEv%}DIchB6gsJ8Cp!M`W` zXMc!1M2K^yfUi&wh>&-)H*C4qL%U+Y>O6gmcy#kmztBCw1H}y~sh7{JHv(^-0_)PX z2JP|f=rgN;+8K97)JLWQ0`*5nt^w`NbPNKA-G?_goXKC^7iZxsJMdLPkitvP`DEoW zXCIfBTw8kOVw`c@0dBG{2%p~hUo$yu@aTAw z)Wi~h5X*Zods=roFiSef^@y`w)U7UYYg*64dc9-tz0PTRFh=M$$;O@agMtgb+f+}* zPxf-b51cB9M4SV#_J<8&m)4zE_biIdB-IaTb&1Ai>djj*7-P6$P2r*QIw3YA6i7LK zpbw3Y7v5vYo57;?oKY$n?3ePa+MAOZ$wH|OYL7TEGOiE8faw@qxr}d26)E35Mqa(_ z<*Im-Zoo*uq~-MZ;wJ0G=5DM22OA^gEn<6JI^?SlX)U6t4v+Vj8{fI@Q9lkGm>W!7e@OFq`h^E12cL&=hWm!wRPpB0c@|qahi+MK8oI~tK$l`P>*_OSHF)v7heXzFVLCJpgqJ_vs20E7>Z9TzsT((){mf+= z=cr|Mjvb60LR)Ga@2^$(lzyJLMv|$FSP|BI^`m0gjv~_*`dp%#^G2DKO~C<`^Dv(G zn13^sgX6fZX2IE6wGO-#RW+p3K=pE#&6MtAVKhY@Y_Y|lPIli-3$B7VsrrMgGARCX zJj@;|NqtZb>@z3vxSI?!^<&A6G%VT-&)rH@nPb?XV=gG`xp)ItxWCHoG%qK-xD3RL zx=e1^Vu0Y{=N<9R-f@<0Y?8lsQRXvui3DaV?cn`(B(8$>x42P|xX4@46wG(_{UlNYq#|H^5)gUcVOscWi=;6bjA_= z7UHB2SwM=)A5kB`ZZ#lu=Bgm8oLcDSXcnYCP8`{4^0lU$z++W6Fw_SNv+GiyAb8s}*4!X5Ch9rAZ&44qvDo*UxD9BIoVUM#UeyU|wPJ<-7ZeU`-J z^Bo7SW+p}jrt*jGR@-76bcr5|=uP~?uZxo>%@=sF6opFFR*ybo*U>OJj;YV@Z@2I* zB+wYs>6wx#=gSaV&u2>U`5yOdU(&pxNVT3FueDBkF3!mqDib;k(N}}1A_7+|(bt{C zSH&4^t2{R}{pRUG^qZb(w4E$F-d(C+KL*5ey$YK_g{qmlx!5-;Rg++z<WfJLYKWmUvOFIJ85@;>R!B&-VrXp|pA z4B#79wHFw$b-G~Tn6F|{Quy_lY20WO<%v1BT1~L4g7|K4_W2DFq&u@Hf=e^Z<@N1l zhxUKH&aZ#Us8{RDDpgg1hqkt!g7~fsgP*2VjR5MjV0Y=09-=T&MqOZg28m3!tQ=T6JMYnpsaN8DyPL zsnD2nAFT$_WEZ<^P|#94c}scDJFs`1)i2*uBj7N9?5fzw%g>%Sucs;v11S4CO#n6& zJK2*M?~+0M$E$9K)SV5z7$yBc+9=!pwF$7~lErqo4rTR%H7zur1V9vZV;t=Hwfn^R zeo(^A$mC#f7i7*%@|h7%UE^jfmar!~B7Rz=xU!6DGtK?+;X3tWb231h(_1mfI6qNn zm4Wt31XBmo!g0viY_{Z}B8BK;z6Qs6uhb}7PF|?_deDNf2h2fU&NOdYPUf{YwJ;lc zTw#YBTYIeNHWR963^LnQHaAPfKfGRcPjTA$VI^j5W{tq4&Qu_G(RsLED{!PpqOlfo zYqIdN`Sfdx2gLKX3XWU7#BGiooNj)I7jJUCp2nTK66sn^n5X9-*lQxHbp_B z3L5;h{Up1+L`GyJjx$Lt7HeJBvk_h*W+I$*5`erU@z6L50}tT%Jc9WEyIi$k-GAnd zfA{XOe93;y`LHoo9^g+o<9a;h(z`tF;WTf)znJ0r%nm`V#W>l60|NLLB_6L+A6aG@ zEAnfO+Thmifkxd753^GfdCn?{?YeAc4kxkAcf`utsxHT!TDGk(Fgie8ozi(tj+D(z zrg+U&1_vSYlnqC1Kd#~YPdK9oT&yHRHPlHb?&qePVw^pO5}U3%X?)RI8KXzsZ^l!1 zK3n|~(~^2(Xi^5903PNh=k4D3m2^;Z^@cj5@jfdxC^4oMrJ&l5O2RBvk9{NDBw`qD z`u#NZaIJh!RfTQHksj21?7daUBk*KN+V(&vJ69)Q_`c62GtJM@?1~<;=;2fUT7Lia zuO;|($zV^gWZ?+skC`l#%ck3dAbB*OZi?atYo~+S%~clOEUeR6gVYf6Ejk+>2`@>n z>KJr$Jj*d5Qn4wFgc6%hGzC$b<~wUsYhYIKr8#XxD+r|BOgBG#BCl1M!(U5sf4I(w zh`Yob-x|%>o$P5C7K2lF+Rn;l(8E_%VC#Ra+{D66tjbGJAaPE{&CTET1^Hg!*qI1=X4=)n)qq~G|Vv$@Cl8PB$`IV z09R(_EEH5^rat64YI%Mx# zW?zy?VPRI|ftgx2ZnBBVw6(~sX^Mo%+Gau~lS8S68Q0)6%8iAYT{d#BXRESws?V_wK^R*oCuK5SlQ<5sMXwTi$B!* zsrJkF>e}VoJ4vK>_3hx<1(@n>hru%L<$K7N9j4;B^6PHYhOckDnGoN$Ih6&=M|M{>Vvw;B945>Ia7rRJ7fL>xt- zWi!hXJ<%Yr4DR}xvQW|emd(!8w7UvwlpbVsh{NhE3Usw96li)lFBbg7j#hIf@-S81 zB!@o-@e=2?8Kr|KBONfgHSQZz@AnM00j?@>@0UoS=qR!~`1T7{=a(usdit{k6=T9O zX6a*NHToSU`^mc#MH`s3*`caL`g+(LciXl(6!D9h6c*8{&u(Q?$|jR4F;SPyVB`Ek`IM^b8BEY7bU+0n z8JugWUXuN`<&v|Yh1pHwxKkFT>`NvZ+)|?@LFW|Ikw;)!nu8{X zEH~f%*7MgI3l%nIjKvBm)wUs)VoZm{*N(FaYdR{oW~$SFcqsoc*iViYpvMaP!8WT7 zHDvGh`Ghr9?FPm69U8B*JvZ(z*zxRUw5-|%Zdk_qyz#ws%*T3o^5M+69UiVXy9aS= zf8a!SGz`%JO}lMQdP%zvD>&0OK?alyuOmugz9y2KBw>qHRaAWPi>aCgh{4CJoA38; zXql&|>-QoY8XNm7hLRtq7k(C>(YOPKW)+;Y@T}Y_9m_c<*)DKtE~I!|-6yR)iPZd{ z=I*3_T0zNXlz;cWsXqv}lx$wSpJ>y!iVSLhnEh1Lw2%3^EQ|KrA-uiaQh05ZEmd{J zu>SKQv9(I2qD1g{-)U7*IR3+tUummnFEO$i{IZ~hB+eOvN8>m-JOI=li*<(7e6<>svG`^7N}>5i&IPvBMfk%ka$;Gtk)y6C zz)Gz4J*1Cu-V{}5-RV2Rt7&N^*7jsqv0YW8)bXjxg)z?lntmdU6HBTXlSo0x?H^QT zYD9ZsQ*^11cbmJ@te)-wHFi!_ze8i;eN|nl1^HiFz~2+7T(Je4duSE)=L(yI$GfTp z(c;sKIope;Wra`D6g3yEUcVn>INHwhZSWLe0IxL=T~hk5r{NBJ zvo%g+u%B6<)q(>W7^T{roXWg8-^DOf9|DXegJz3ImHg$sIblgBx=JQh^#UEo?Oyql zmHbkF*yPN*mF8X7yO-qq$2Rx(I|3#y3($@zlBO~K7kvmQ^;<`5?0YG!hlt*`kqCh0 z>Fx*%`tKi&$UvFWkI4*GoXY;UzWsXRx)Kj9I}A=oM796Y$3I47iwtnEI5tGtB>q!h z{~WP2I5=QcT8Z2o`L!4MYaD-H&NGEjGj8#J4*?jy|6fMJ)v1p!Ip=R-1B@{f#$?Ci zLES^MSK43e8@1oP@Z*s*qjQmo#vHW zF3?MRymKCM39&4!`5S3@^Z?_+CDH+UY zc{aH^<$MJvkJGTTQf~SFzWpGeK!b#w3jc=&zR6AUVDczfAhY8B{hR+d`u}hVExQfz zfalWx*G&x3KMt6qAJ_gpY=8R>e;5?*4Ji0>3VUIkZ~x~v%7K|u?mzs0XgS0Sa#GDW zwOW}~kN=(me@=#k7x3kY{4PcRaqquBO0qz-@AW04yv+Rj)PI<}5lUbxQLcjimc;LA z_Up*A?`dg0h+=;Ce||$BxQ9M+D#d?X`R|Xi;w>NRt#djc$o@X{4{dll#{f)trl2d; z|G4+xAJeP>iAS>$BYye&)PIO2O$oS%Qpe_hi23)&XaMpn`kzX?`tU!M_)81_rxJfo zkN>H}|5W0y5%`}<{9ZGD9sP%uI8*f`O>rPp12%EndHF@cz@~z08~|};m4cNEV*qq2 zfdxRHH9w$SRi7UJwTAX4NkNW2MCh1>Na9j#u6=>X2DGd02X%kmBxjju(S(n zJIx!>7<3=s$a8Ao588c-r~J9Da@!*Vzy`I!nByNOMmk>%v7Zhw==OZ8xWAasDXf|! zFKoP+dkPz*f|ZcT4}s@48i=DEZ!mLadUtj=7TXlXc^W_5v8JDBhW`2JCBDnZlA37g z5`YxP0FdWl=F@`(US@B6%`a|Zy}#<_GM^>^b`63qRxPp6HORVQxNKEw^Fw*j4*+;Z zlMKMqM!m$bqNxvOIWo;|_pA(2m&@*4ZAT$YOU$m<{TkxfW))I52lmbLU*1m`A1~4;@!n&P2WE+5tn}9rB z>F_l5(dFiL`4MmT7c8G_3-k?yVS9qTpY~cazQ#HiyyyGC{xvd2&2APZ4fZp>!%xpM z0P@QMKy#8O7;IZ6^+t9M*xeCM06`Y#@fOeLI*R~*kGQ~HdFnkSdC%h-*8|ah`7r19 zaW}riVbiTShoYV;5a7{BuwQZ|EC zB|HV8gN4;g2}@49h0KiIZGOVb{nYi8900g&1fa~K0M=2C2igJ>mA<=Uvw;{RlA%8m^y)&^Dv{ zT!+X&Yj80DqD(i0y0#khvgmnH=SsVuU?t0DtXVF*`*A16n)2O6b#wB~*|tnG`b^BT zl-(xE;Oi$yqCl|H@ftn^pNlgYh!Fr>5DH;5{tn6aAhFot5g;atvl%ly(nnF86v}zC zWz$EN)8S}<0|3c-+FYj7V+Mw2TccJXr!;SB@UGxXC5F&+HiA;Y!;+ZQVSi0vU@D77 zazAXA{0)z{P@?sGRyFfRV9eckR$&JOdM3Lgp3~mgbvQS~@y<9jt%b5pg>pSyU2*vN zSK{PNS=T7`6IfIE86Dq1!cj_M(`xfkRj21}d@PLN9acsRGht~d+%IWIti{%hr?KRAyRTgIs%YaIwOi`m;NAO|8Kr87v1{SBAQX4kl7yn6_5LDc*d?f(N?71r4DMR00r4K@|WVb?>s=h1{5KV2WS{C3yO3`N1oV)0~qR+q_1L>g8IcYPyzGc*&Hbpi?8;qF9MfaEP zkM<2IDu#UDlJmc=T^H3KDECug5Ps9GSJhH}ib5bCB6F8>XptWx2X5l^A>+C2n`k5s zkSO#EzzA8+G&G!zi6CdAelKfozT2QFZ9a58bgop*Qt6*u{p1Dea=zY(o$lr@Nx4<% z26c%5Azv*oXYdf`Ta_Zakj>h_8i5bu4nkLh53qCbyhXajjmKTskzbR8qiajTRI}9k z(YlcPP<2s$rr3-L9pjzqYo&y7-WWnP)(Y2k9mA}HUf7xaZ1F4|k^ll*Gtm92uDH#p zC!2ZT87>N11veNAST{ceZ1j0>;DzF&eeI7LFlmZhE>j;4HtuyXLRiWpwMWFiFkKmwJ>cjWFL`SXwh^(!g zB(fLEe$q1o4TpX)9A{nWMy1f=hkN={QMFxf!3B1#W!u{`^n?QUI^88QWtm9EBn+QxW>nXUKoHY1j?@SU~)+kF;m$+v}E zh8wL{hBU1=EJs15WzBTvRa)uyu=|wOxo>yJjlHLk&ZdDh6>l9}c*Djv#aRq0GV8C_ zcX$*oCxVFc8T?)7DyGP0#9Kc^okP05URwp@#F3cW`4hOt8WpxaurO{C`_*p)`6@x^ zSQRmv2b5=F+4nEu%t+jhyz%)|&~Oav!yh5B4@c3xOTj-`lx062sy5;lezepws@goK zlK=~1qjw2A!;4<`YPltW=F)#Aw@>ozqJ>V@d>mF?Of5+(gf28vwEl)2){v4Posryg z+5C9V>^I=KKZDV|X|J4EG9vA}OuNDQ{VR;l?T$h-R#(MVT4Vnv=Vfh4X2?q_E9PRw zk|>i`q;pm4G#^)O=h90?n!d)6o3=ZWfsgecym#Yh z5eqxeBU0AR8Y7lxAUQr*6M~nSbGn9KGr?{M#232y8m{q(L;(Dpomb2P<*t~;|5}3b zTh`t4QvvdKjlXc0Ehq?{2GwM2*9Zyr;|>^O*u+#I1`p!{O9qRx-IzoXO{A-C(ReOh zM6p7aPS}jvgzQsQ@mhAw zB7r;BvWybS1r?vz);7R()XuDY#y>3%V5ptEoNywf@)N-fGU>Qugn3DR;1`4o(`B7)_$ zJyxE~IeCWk6!axJ+kiuF@;6eA)5*}O)!*V!D|*2Rw^vQMLEjNV28wod`2oQDowjGl zn+bzv%G#k&7mBEcrvAlASUqFMA8BA86WT_Y$*#?+Tl7GoLyk>-*(R5KWxN?J|inF6`N4`GsE;s%Oz zl2&ur$(ncqrt*xoxWtj<7z;1K7qp)VR!E0MUMqPCwH={=OYw$9Dox`*>7;2hR#OG! zX`4fRaTpwY4r)HkUC)Y0qTOujwF+L?W#uurP=mC;7(Oj?2M?7!KHPQ>-dN9v=Q5Dw zf;qhS&6~OrXqEgvM)qmv<@_KA`})g5$PkG7_Z|OG)PK-Rmre@QYI@xgqnT=K7$Xpb zqK%UBxFyIH*b*rW^0G4@5c<;V0&5Pd0P7|4K^@%ADUvHx0z~XLvrBclr^4DfE(CRkG3e(3fOtbSCk;j1G!eI0;k#>(|E-={c*OAO4;^y`Ejs`eELBR}^ z3aG%^JtxJHdMhOOfNfSGHS|il5AS*LwzB*l1u?IG6Y~hpQ7m8tc9`1OHPIJ%&_* zA^Ozx1p$)KXq6<3lzm2R6?E2)twtXX*kWhwD$v(|Ky0T)amTj<7*ZmJa?gsCD zP$y?z;#hJ+H1L!yK2Y1GJZgp%*l!A|4`FgRDtN=uM)Ul)+)-OJa<(?;+*Aw7}jFlTAKN4x=bg zf_J$Q*^44N=d`y2t_?(IdTcxj%5|^hWr!u>24mk=FcLU)$Chwj1{W!`lml0K)|zPb zn%sR2d)fb$QPJ}ug2~-I`pY%(S_?Z_MbsHOYOi}5_v?NuTrjroRAa4O9JU7Mi1yb$ zgCUO7c3)7}d5>*tEbJ96K94Pr_S7&(t;|6zo^O&nI=J_fH>ei9CbR@x-rh!(&T_j> z-*M}6=W|Nwvv8O17sJWREajSdt08fLN%+w~T=7&7_7<36#ul8s!2u@e*Yq8mpDuet zX^4)r)p%Sm{(;M9E2Jqr^gniI(TvxVR{tFzVg2k8I5KAJy$l=tgFTyuVIOttalVG3 zX!(k@RR{K_Z6*eU-rf1Kb7m$xYd>3WVZ6*7Q^jO6w_yBkCS{Gm zjik@Oe;84tK=zn^f?e>RkpdvkqJ8g2?^WHON<3f~w`nuH;%>=ejgOEE@TSK^$( z`}i!c@yAG3U1FX|%J?gvK#jaqwLH=m1OL9QF@dkcuM4GA{}ChE3IxIhYR3gE`hSd# zlYh(A@fuM}Hq~>2oe6XBG%qtbtytSAPUMD1iIOh`8G#~vo%{xxiLwycI%|@ys zGM&mlHNDrt<+o!r(g{R#>Stg`2;qcW|Icp$ zQ+R;glau~X5>M!_ZyBon3>U-;NQ52QOKFneIF}_%QU~_ND)>K153S4FfsHhq8Q^XP zpnssEvS>IKr~_bywL;q|rJPC5PE-=nF94R{EK+ZDUI|#i?AwES8rP%NmqtfFTeYU= zpmB06mYb-S_DZbSE|>u4kD=*JGZz3WSb>JD*1EC)n2K`{h$YJutik>3&02y{wpYhLj1XHpjI3D>sb>)QgXyi43LP(q$KIOv^^66=ujgp)dO&XV)mh$1hUnBNTmM4I>NBulnnyqy<-4q5DVkF41ir{!|o6Cqnq(Mr4~zTEKJ>N_xu=Y z3>F=0A#xsS9O}E%o(CW)H2w_mTseSLPSfoa=YxRSQfg0@6}lCM&!2`du?aq{ z&@-JA*^mr00YcYFMPfhVE*O4@Bl4ub?es8%J^g{T83f^_hc|l(pmFG@I`_n}s&xOa zy=#qza&5!dle||6MXg3=}lk=&WPEMm_(oi`xj0ib~B#bGQQ{^5zvS)~jMV3jAeLU843Ejp32|o4kHo-ETIq64 zf)uR3#WC{MW+(R4t$@3Tn_s5uGeQLFL^FY6O>xFqmt^V;9KX)zK#OqXNmwDlLz4B@ z82hX93@~4$@m+L9>k3y0CwD5VDrkEWVQfApd+pguj_PzcuQCUp;aOoel@S<{<&+xM zV;tpiTsmF{Sca+qFSfhSymuC)D~FSRH_eCy+Jyu9_V(&bq*F0j$bR zRbzbwSIM^(Wq{n&r;vCpeZU!LEK&_zEzfkNrRjU$uqbjQH8E%7CM~H^K&_spKBEq{ zM2qj^n;+?S`;NsF@WC@Bs2-ksHS1i>jH?iq3wY>sVgh;{QFcr3<0+Yzk>;0~=xkE7 z7-{^9e=|{J0BVI^+C^(}tD8lCyU%z}p>X#opG`Z2cx>`*3WB3ZY1qdl#~o^nzb%&Y zKqCvDD?uw61ayaXoUG=?l3UQ>dE}z1Ad2&wREL;)wG%bZ@WzKA;N88LOY`tTr9y=M zls8q>(zu0@KvYF>-*^yj=LO3GEfd%Tm8A+BC&=|&qn42D)8PXZ8^kyUdFsin)K{*~ zVdM9SP$%S5yzYiy25K^IXLOq(l-Hjv8cotd9e+M)EVp5R&3Aa@J!t` zDKrglu-4ECu`%VDr;yaa6Qy@-36^N!Ri1GbAR&uXBFN5kf?wr^1vTaiT9uqLgOxEl zv%X2EO!HLtFH-$Pn+IK|W3Nv|1$Hok^X(N5e_u}5?ax&PHo&UbLdEv^VZ~cYV~0Gm54jA8}gpzSZZO_OzsNdrfmniVC<>H^7PB; z9-w1AY##vsc)-_k!m`mqYum!OQG83S=UxE|Q_FU|xMy3_dyz&c3%XXUs!~p7sj|lf zGL8R;&3HvjMV&-lfx>ct?@}ZY{EfG%Rinkh?P>*822wPi|N{D=mLLG6f* zc~PYg$YVEu)}uc&^tNfo*R`!bLDj?>a@#U|HD%hYKX}<3?DEx6y6hb=MkJ(gTM&Kd z7x6>=uqi?W#+G+cvB^N{X>8m~rS~X9SD5=;7SXfJ1bmq;$E7EdKShm=R$Lno`sO_d zu!-bb%1fe)_hie7A$ef#%jcc?yVwX5Z-7a_8c2P#H;d?IgGO8vBR+lm`{knF47(Pa#gjp;c^bd#ic?kW(K>4rS{VeBe zg(5iQ65y$k689h7OykLvs?CDu>w0;)v-L~8*hKR{{}V6q)1jD`x;+L~-w@L0kuEhd ze=kR~&-~+TB1Z~v-U0?+{$A$O$y62|W|wpJ$%dn^0Z(uogG(TL-wozl!c6g>j(<35 zbLSc3q2vO12=BfO?^^ez(nAg*%HD=%8Z7PEn!-;yOE(N^eQeB6pXq6+D+NYTer~*W zNLME*KkEUXR++HqVi0MYk~#FA>ZA-;erb(EGpA>nXQys6uMNKgTSX8$Q7#QE&|W`@ z$sU<)C1(>zTmz(u3CIr#mx3&Hg~K{Kk7(q#iapiJHt|-UA2?A~Aoz>$$=!PQ=cAJgr`guTXh^u*2ms$V(AFi3-WIVhFV+gv;>F0Fs`J7-Q z)wk}@4Fh$M83%(jAxFp*a&x!C`j}~$UtW41CEg~kV>tC^nIr5bwQNx2${Z9`Tru8u zaWLufJIJa=aHEdvnfIJ%NgZRCP#np7C>T&t%s3?|zeW&=)Lh^QpNCa3*sGNw>l(d{ zqt2gVlLJMP{|u@c71Vo zTSUsQtmt<&$j#ip3NNwV@x{58^Q*<=DZ!LuI4|$2_CaEwB35(Zda39_2)L0^t+ofJ zJ!4ikRIX8(m2~SSq+`^*lP*G>?FoxMEggP|`0f>DpGA@>z9EsnhbMN)7h{zI^ak}s zpmKCoTPt&2yk(B~U=82umlAzi&%KDvKr7M=rhPxs447vp@}fsn(y3FaNBqp}^@o$L z!ZYSI)PKk@6AnT)DH{UI=Jn*i!hwm6Lds{vd1hPKX?W_CqHicc>03hobwlOB@~Q!1tVV*6i&gG2`R* zXLO91C91TB#wKc7uVwKJC1?#`qlo5t&^m8v_TaVR3NVq{c}C7gk=v%fNWTQvkWDzyULg);T%yemLR`!c-l z+97hnBmyK40N~bPh$l+C9`@^0TieK0#`eoNO0SaJmS3;7;G&>vIMV(8l0g~9igKoY z@ww5D@jMHl|Fh{#{Y_uOS(`5)s2;gl45{6OZc;Am8K68&S4!3Z@5#f`LpRKW?)2Km zz*F`CWszu+uM#G+n&sf&GPDx_wa9a=iv*q zIB4WMAhS)XyRJc`Dvs+NyMKZ9%c%DenmCDbC1jg}P;kAh(nIpc)}*=P_i#mBvU1{L zOsB0+nTd-ln?8u48SuMfTH~F4cB!xCagoD%31{3R1d&DuCkYq0>tCb}>&+EtK?|h1 z5$3e(*4GT%HrpZYcL)jW=*t17vpIts-CwL!{AI0VZUIM??2SjrHKCl9vA-g9notLs zj5d9OTqT?#F)2Y#@k#X(IR%-zTVNgC2FP$9Zl>)9ST3C{UaHe4o&f7ZB!DxDG7I(7 zv;U~N&-Yc65OM9wrmmiGq+ALuMAI9Us7HpJLb6IcvL$md58On*}P>Jx)VUB^_=Zb;p&y* z;|T`ps;tVXs4$7snBlf_b9M;=VK+B*s3JeI!PzbCWCJ1VC!>J%=+nV!nb4Wy z3rgGFLWRi9wm1ihfBeoj7VYMyou^zm)rK$bwS}!Mk~HR|k6m z72olgL>NlA+Bkdn$ zCH*;wN44j|*2Z&N7VV>*qmj!Si)bSl_EGS!X>RGXwzEF&uRcFZqyz95#sLdMb2o z`t`iRv`WwT zyN>F|GyYnwbWh}mkCq4L_hBeB@LA5={3Y;TotUdC?+O3B{Kua4b3Fca=&)v|W(5R< zjm&mo2|teauZQ}1`7(#_Gju-}@AnDqTdMk*xR%$*x0w8IiR)*!S&roYJKMbZ Date: Tue, 30 Aug 2022 17:39:15 +0100 Subject: [PATCH 026/418] Adds documentation section filenames into the model. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ .../structurizr/documentation/Section.java | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0a3dcc9c2..d98d7e68e 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.14.1' + version = '1.14.2' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index e6fa45ba8..83178a2c9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.14.2 (unreleased to Maven Central) + +- Adds documentation section filenames into the model. + ## 1.14.1 (15th August 2022) - Enables `structurizr-core` to be used as a transitive dependency by consumers of `structurizr-client`. diff --git a/structurizr-core/src/com/structurizr/documentation/Section.java b/structurizr-core/src/com/structurizr/documentation/Section.java index 3a84e26c3..ea5cc0b42 100644 --- a/structurizr-core/src/com/structurizr/documentation/Section.java +++ b/structurizr-core/src/com/structurizr/documentation/Section.java @@ -5,6 +5,7 @@ */ public final class Section extends DocumentationContent { + private String filename; private int order; public Section() { @@ -16,6 +17,24 @@ public Section(String title, Format format, String content) { setContent(content); } + /** + * Gets the filename of this section. + * + * @return the filename, as a String + */ + public String getFilename() { + return filename; + } + + /** + * Sets the filename of this section (e.g. where this section was imported from). + * + * @param filename the filename, as a String + */ + public void setFilename(String filename) { + this.filename = filename; + } + public int getOrder() { return order; } From cfa3287022bfef6715a5e767dda642b553f9e527 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 31 Aug 2022 14:35:33 +0100 Subject: [PATCH 027/418] Adds support for custom elements on dynamic views. --- build.gradle | 2 +- docs/changelog.md | 3 +- .../src/com/structurizr/view/CustomView.java | 28 ++++++++++++ .../com/structurizr/view/DeploymentView.java | 28 ++++++++++++ .../src/com/structurizr/view/DynamicView.java | 45 +++++++++++++++++++ .../src/com/structurizr/view/StaticView.java | 33 ++++++++++++-- .../src/com/structurizr/view/View.java | 28 ------------ .../structurizr/view/DynamicViewTests.java | 4 +- 8 files changed, 135 insertions(+), 36 deletions(-) diff --git a/build.gradle b/build.gradle index d98d7e68e..d73b5e5f0 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.14.2' + version = '1.15.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 83178a2c9..673bfa458 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,9 @@ # Changelog -## 1.14.2 (unreleased to Maven Central) +## 1.15.0 (unreleased to Maven Central) - Adds documentation section filenames into the model. +- Adds support for custom elements on dynamic views. ## 1.14.1 (15th August 2022) diff --git a/structurizr-core/src/com/structurizr/view/CustomView.java b/structurizr-core/src/com/structurizr/view/CustomView.java index 025a37b25..8a99a6017 100644 --- a/structurizr-core/src/com/structurizr/view/CustomView.java +++ b/structurizr-core/src/com/structurizr/view/CustomView.java @@ -136,6 +136,34 @@ void setAnimations(List animations) { } } + /** + * Adds the given custom element to this view, including relationships to/from that custom element. + * + * @param customElement the CustomElement to add + */ + public void add(@Nonnull CustomElement customElement) { + add(customElement, true); + } + + /** + * Adds the given custom element to this view. + * + * @param customElement the CustomElement to add + * @param addRelationships whether to add relationships to/from the custom element + */ + public void add(@Nonnull CustomElement customElement, boolean addRelationships) { + addElement(customElement, addRelationships); + } + + /** + * Removes the given custom element from this view. + * + * @param customElement the CustomElement to add + */ + public void remove(@Nonnull CustomElement customElement) { + removeElement(customElement); + } + /** * Adds the default set of elements to this view. */ diff --git a/structurizr-core/src/com/structurizr/view/DeploymentView.java b/structurizr-core/src/com/structurizr/view/DeploymentView.java index 425c38829..02580f0ec 100644 --- a/structurizr-core/src/com/structurizr/view/DeploymentView.java +++ b/structurizr-core/src/com/structurizr/view/DeploymentView.java @@ -451,4 +451,32 @@ void setAnimations(List animations) { } } + /** + * Adds the given custom element to this view, including relationships to/from that custom element. + * + * @param customElement the CustomElement to add + */ + public void add(@Nonnull CustomElement customElement) { + add(customElement, true); + } + + /** + * Adds the given custom element to this view. + * + * @param customElement the CustomElement to add + * @param addRelationships whether to add relationships to/from the custom element + */ + public void add(@Nonnull CustomElement customElement, boolean addRelationships) { + addElement(customElement, addRelationships); + } + + /** + * Removes the given custom element from this view. + * + * @param customElement the CustomElement to add + */ + public void remove(@Nonnull CustomElement customElement) { + removeElement(customElement); + } + } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/DynamicView.java b/structurizr-core/src/com/structurizr/view/DynamicView.java index a8ca3eaec..14bec348b 100644 --- a/structurizr-core/src/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/com/structurizr/view/DynamicView.java @@ -100,6 +100,46 @@ public RelationshipView add(@Nonnull StaticStructureElement source, String descr } public RelationshipView add(@Nonnull StaticStructureElement source, String description, String technology, @Nonnull StaticStructureElement destination) { + return addRelationshipViaElements(source, description, technology, destination); + } + + public RelationshipView add(@Nonnull CustomElement source, @Nonnull StaticStructureElement destination) { + return add(source, "", destination); + } + + public RelationshipView add(@Nonnull CustomElement source, String description, @Nonnull StaticStructureElement destination) { + return add(source, description, "", destination); + } + + public RelationshipView add(@Nonnull CustomElement source, String description, String technology, @Nonnull StaticStructureElement destination) { + return addRelationshipViaElements(source, description, technology, destination); + } + + public RelationshipView add(@Nonnull StaticStructureElement source, @Nonnull CustomElement destination) { + return add(source, "", destination); + } + + public RelationshipView add(@Nonnull StaticStructureElement source, String description, @Nonnull CustomElement destination) { + return add(source, description, "", destination); + } + + public RelationshipView add(@Nonnull StaticStructureElement source, String description, String technology, @Nonnull CustomElement destination) { + return addRelationshipViaElements(source, description, technology, destination); + } + + public RelationshipView add(@Nonnull CustomElement source, @Nonnull CustomElement destination) { + return add(source, "", destination); + } + + public RelationshipView add(@Nonnull CustomElement source, String description, @Nonnull CustomElement destination) { + return add(source, description, "", destination); + } + + public RelationshipView add(@Nonnull CustomElement source, String description, String technology, @Nonnull CustomElement destination) { + return addRelationshipViaElements(source, description, technology, destination); + } + + private RelationshipView addRelationshipViaElements(@Nonnull Element source, String description, String technology, @Nonnull Element destination) { if (source == null) { throw new IllegalArgumentException("A source element must be specified."); } @@ -244,6 +284,11 @@ public void endParallelSequence(boolean endAllParallelSequencesAndContinueNumber @Override protected void checkElementCanBeAdded(Element elementToBeAdded) { + if (elementToBeAdded instanceof CustomElement) { + // all good + return; + } + if (!(elementToBeAdded instanceof StaticStructureElement)) { throw new ElementNotPermittedInViewException("Only people, software systems, containers and components can be added to dynamic views."); } diff --git a/structurizr-core/src/com/structurizr/view/StaticView.java b/structurizr-core/src/com/structurizr/view/StaticView.java index dd4947dae..b4199bfa5 100644 --- a/structurizr-core/src/com/structurizr/view/StaticView.java +++ b/structurizr-core/src/com/structurizr/view/StaticView.java @@ -1,9 +1,6 @@ package com.structurizr.view; -import com.structurizr.model.Element; -import com.structurizr.model.Person; -import com.structurizr.model.Relationship; -import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.*; import javax.annotation.Nonnull; import java.util.ArrayList; @@ -240,4 +237,32 @@ void setAnimations(List animations) { } } + /** + * Adds the given custom element to this view, including relationships to/from that custom element. + * + * @param customElement the CustomElement to add + */ + public void add(@Nonnull CustomElement customElement) { + add(customElement, true); + } + + /** + * Adds the given custom element to this view. + * + * @param customElement the CustomElement to add + * @param addRelationships whether to add relationships to/from the custom element + */ + public void add(@Nonnull CustomElement customElement, boolean addRelationships) { + addElement(customElement, addRelationships); + } + + /** + * Removes the given custom element from this view. + * + * @param customElement the CustomElement to add + */ + public void remove(@Nonnull CustomElement customElement) { + removeElement(customElement); + } + } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/View.java b/structurizr-core/src/com/structurizr/view/View.java index 419163a68..f12a6a08b 100644 --- a/structurizr-core/src/com/structurizr/view/View.java +++ b/structurizr-core/src/com/structurizr/view/View.java @@ -510,25 +510,6 @@ final void checkParentAndChildrenHaveNotAlreadyBeenAdded(StaticStructureElement } } - /** - * Adds the given custom element to this view, including relationships to/from that custom element. - * - * @param customElement the CustomElement to add - */ - public void add(@Nonnull CustomElement customElement) { - add(customElement, true); - } - - /** - * Adds the given custom element to this view. - * - * @param customElement the CustomElement to add - * @param addRelationships whether to add relationships to/from the custom element - */ - public void add(@Nonnull CustomElement customElement, boolean addRelationships) { - addElement(customElement, addRelationships); - } - protected void addNearestNeighbours(Element element, Class typeOfElement) { if (element == null) { return; @@ -562,13 +543,4 @@ protected void addNearestNeighbours(Element element, Class Date: Wed, 31 Aug 2022 14:59:53 +0100 Subject: [PATCH 028/418] Make Java builds sequential, to prevent client tests from failing when using the API at the same time. --- .github/workflows/gradle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 5b5e99dbc..b449b38bd 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -16,6 +16,7 @@ jobs: strategy: matrix: java: [ '11', '17' ] + max-parallel: 1 steps: - uses: actions/checkout@v3 From c516d45483e08f6d60fb7886df6e2de8e818681d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 13 Sep 2022 08:43:13 +0100 Subject: [PATCH 029/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 673bfa458..5356a1260 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.15.0 (unreleased to Maven Central) +## 1.15.0 (13th September 2022) - Adds documentation section filenames into the model. - Adds support for custom elements on dynamic views. From ccfe4dedde267585aed2bbe96b54a8ffa147d3a4 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Sep 2022 23:20:32 +0100 Subject: [PATCH 030/418] Allows filenames to be used to specify element style icons (this is really for themes, where the images can be loaded relative to the theme definition). --- .../src/com/structurizr/util/ImageUtils.java | 21 ++++++++++++------- .../com/structurizr/util/ImageUtilsTests.java | 4 ++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/structurizr-core/src/com/structurizr/util/ImageUtils.java b/structurizr-core/src/com/structurizr/util/ImageUtils.java index 7849f7fe6..6b7fdeee2 100644 --- a/structurizr-core/src/com/structurizr/util/ImageUtils.java +++ b/structurizr-core/src/com/structurizr/util/ImageUtils.java @@ -69,25 +69,30 @@ public static String getImageAsDataUri(File file) throws IOException { return "data:" + contentType + ";base64," + base64Content; } - public static void validateImage(String url) { - if (StringUtils.isNullOrEmpty(url)) { + public static void validateImage(String imageDescriptor) { + if (StringUtils.isNullOrEmpty(imageDescriptor)) { return; } - url = url.trim(); + imageDescriptor = imageDescriptor.trim(); - if (Url.isUrl(url)) { + if (Url.isUrl(imageDescriptor)) { // all good return; } - if (url.startsWith("data:image")) { - if (ImageUtils.isSupportedDataUri(url)) { - // all good + if (imageDescriptor.toLowerCase().endsWith(".png") || imageDescriptor.toLowerCase().endsWith(".jpg") || imageDescriptor.toLowerCase().endsWith(".jpeg") || imageDescriptor.toLowerCase().endsWith(".gif")) { + // it's just a filename + return; + } + + if (imageDescriptor.startsWith("data:image")) { + if (ImageUtils.isSupportedDataUri(imageDescriptor)) { + // it's a PNG/JPG data URI return; } else { // it's a data URI, but not supported - throw new IllegalArgumentException("Only PNG and JPG data URIs are supported: " + url); + throw new IllegalArgumentException("Only PNG and JPG data URIs are supported: " + imageDescriptor); } } diff --git a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java index 7d9a25af6..819201ab7 100644 --- a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java @@ -159,6 +159,10 @@ void validateImage() { ImageUtils.validateImage("https://structurizr.com/image.png"); ImageUtils.validateImage("data:image/png;base64,iVBORw0KGg"); ImageUtils.validateImage("data:image/jpeg;base64,iVBORw0KGg"); + ImageUtils.validateImage("image.png"); + ImageUtils.validateImage("image.jpg"); + ImageUtils.validateImage("image.jpeg"); + ImageUtils.validateImage("image.gif"); //disallowed try { From e9a340cc1ffd9b380fe254dacc538a98a0234cb0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Sep 2022 23:21:52 +0100 Subject: [PATCH 031/418] Add Jackson annotations as a dependency. --- structurizr-client/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 2660a56ad..e869bb738 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,7 +2,8 @@ dependencies { api project(':structurizr-core') - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.4' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4' implementation 'org.apache.httpcomponents.client5:httpclient5:5.1.3' From aaaeacdb069c91eb868cced589cbfdf122c68155 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 22 Sep 2022 14:28:11 +0100 Subject: [PATCH 032/418] Some minor changes to support building a diagram legend with the PlantUML exporter. --- build.gradle | 2 +- .../src/com/structurizr/model/ModelItem.java | 16 ++------- .../src/com/structurizr/util/TagUtils.java | 22 ++++++++++++ .../src/com/structurizr/view/Styles.java | 35 +++++++++++++++---- 4 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 structurizr-core/src/com/structurizr/util/TagUtils.java diff --git a/build.gradle b/build.gradle index d73b5e5f0..3d5478eb3 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.15.0' + version = '1.15.1' repositories { mavenCentral() diff --git a/structurizr-core/src/com/structurizr/model/ModelItem.java b/structurizr-core/src/com/structurizr/model/ModelItem.java index 3ebc76107..e95ea9148 100644 --- a/structurizr-core/src/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/com/structurizr/model/ModelItem.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.structurizr.PropertyHolder; import com.structurizr.util.StringUtils; +import com.structurizr.util.TagUtils; import com.structurizr.util.Url; import java.util.*; @@ -45,20 +46,7 @@ void setId(String id) { * or an empty string if there are no tags */ public String getTags() { - Set setOfTags = getTagsAsSet(); - - if (setOfTags.isEmpty()) { - return ""; - } - - StringBuilder buf = new StringBuilder(); - for (String tag : setOfTags) { - buf.append(tag); - buf.append(","); - } - - String tagsAsString = buf.toString(); - return tagsAsString.substring(0, tagsAsString.length()-1); + return TagUtils.toString(getTagsAsSet()); } @JsonIgnore diff --git a/structurizr-core/src/com/structurizr/util/TagUtils.java b/structurizr-core/src/com/structurizr/util/TagUtils.java new file mode 100644 index 000000000..c41056584 --- /dev/null +++ b/structurizr-core/src/com/structurizr/util/TagUtils.java @@ -0,0 +1,22 @@ +package com.structurizr.util; + +import java.util.Collection; + +public class TagUtils { + + public static String toString(Collection tags) { + if (tags.isEmpty()) { + return ""; + } + + StringBuilder buf = new StringBuilder(); + for (String tag : tags) { + buf.append(tag); + buf.append(","); + } + + String tagsAsString = buf.toString(); + return tagsAsString.substring(0, tagsAsString.length()-1); + } + +} diff --git a/structurizr-core/src/com/structurizr/view/Styles.java b/structurizr-core/src/com/structurizr/view/Styles.java index 2081559b0..ddf92ccb4 100644 --- a/structurizr-core/src/com/structurizr/view/Styles.java +++ b/structurizr-core/src/com/structurizr/view/Styles.java @@ -2,6 +2,7 @@ import com.structurizr.model.*; import com.structurizr.util.StringUtils; +import com.structurizr.util.TagUtils; import java.util.*; @@ -88,13 +89,14 @@ public RelationshipStyle addRelationshipStyle(String tag) { * * * @param tag the tag (a String) - * @return an ElementStyle instance + * @return an ElementStyle instance, or null if there is no style for the given tag */ public ElementStyle findElementStyle(String tag) { if (tag == null) { return null; } + boolean elementStyleExists = false; tag = tag.trim(); ElementStyle style = new ElementStyle(tag); @@ -106,11 +108,16 @@ public ElementStyle findElementStyle(String tag) { for (ElementStyle elementStyle : elementStyles) { if (elementStyle != null && elementStyle.getTag().equals(tag)) { + elementStyleExists = true; style.copyFrom(elementStyle); } } - return style; + if (elementStyleExists) { + return style; + } else { + return null; + } } /** @@ -119,13 +126,14 @@ public ElementStyle findElementStyle(String tag) { * * * @param tag the tag (a String) - * @return a RelationshipStyle instance + * @return a RelationshipStyle instance, or null if there is no style for the given tag */ public RelationshipStyle findRelationshipStyle(String tag) { if (tag == null) { return null; } + boolean relationshipStyleExists = false; tag = tag.trim(); RelationshipStyle style = new RelationshipStyle(tag); @@ -138,10 +146,15 @@ public RelationshipStyle findRelationshipStyle(String tag) { for (RelationshipStyle relationshipStyle : relationshipStyles) { if (relationshipStyle != null && relationshipStyle.getTag().equals(tag)) { style.copyFrom(relationshipStyle); + relationshipStyleExists = true; } } - return style; + if (relationshipStyleExists) { + return style; + } else { + return null; + } } /** @@ -155,7 +168,7 @@ public RelationshipStyle findRelationshipStyle(String tag) { * @return an ElementStyle object */ public ElementStyle findElementStyle(Element element) { - ElementStyle style = new ElementStyle("").background("#dddddd").color("#000000").shape(Shape.Box).fontSize(24).border(Border.Solid).opacity(100).metadata(true).description(true); + ElementStyle style = new ElementStyle(Tags.ELEMENT).background("#dddddd").color("#000000").shape(Shape.Box).fontSize(24).border(Border.Solid).opacity(100).metadata(true).description(true); if (element instanceof DeploymentNode) { style.setBackground("#ffffff"); @@ -164,6 +177,8 @@ public ElementStyle findElementStyle(Element element) { } if (element != null) { + Set tagsUsedToComposeStyle = new LinkedHashSet<>(); + tagsUsedToComposeStyle.add(Tags.ELEMENT); String tags = element.getTags(); if (element instanceof SoftwareSystemInstance) { @@ -179,9 +194,12 @@ public ElementStyle findElementStyle(Element element) { ElementStyle elementStyle = findElementStyle(tag); if (elementStyle != null) { style.copyFrom(elementStyle); + tagsUsedToComposeStyle.add(elementStyle.getTag()); } } } + + style.setTag(TagUtils.toString(tagsUsedToComposeStyle)); } if (style.getWidth() == null) { @@ -219,9 +237,11 @@ public ElementStyle findElementStyle(Element element) { * @return a RelationshipStyle object */ public RelationshipStyle findRelationshipStyle(Relationship relationship) { - RelationshipStyle style = new RelationshipStyle("").thickness(2).color("#707070").dashed(true).routing(Routing.Direct).fontSize(24).width(200).position(50).opacity(100); + RelationshipStyle style = new RelationshipStyle(Tags.RELATIONSHIP).thickness(2).color("#707070").dashed(true).routing(Routing.Direct).fontSize(24).width(200).position(50).opacity(100); if (relationship != null) { + Set tagsUsedToComposeStyle = new LinkedHashSet<>(); + tagsUsedToComposeStyle.add(Tags.RELATIONSHIP); String tags = relationship.getTags(); String linkedRelationshipId = relationship.getLinkedRelationshipId(); @@ -239,9 +259,12 @@ public RelationshipStyle findRelationshipStyle(Relationship relationship) { RelationshipStyle relationshipStyle = findRelationshipStyle(tag); if (relationshipStyle != null) { style.copyFrom(relationshipStyle); + tagsUsedToComposeStyle.add(relationshipStyle.getTag()); } } } + + style.setTag(TagUtils.toString(tagsUsedToComposeStyle)); } return style; From 190c6315e8b019a6041fe4e934ccfc6cb0ab4e9f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 23 Sep 2022 15:39:46 +0100 Subject: [PATCH 033/418] Adds some additional functionality for getting and finding element/relationship styles. --- docs/changelog.md | 4 ++ .../src/com/structurizr/view/Styles.java | 28 +++++++++ .../com/structurizr/view/StylesTests.java | 62 +++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 5356a1260..4752fe2a8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.15.1 (23rd September 2022) + +- Adds some additional functionality for getting and finding element/relationship styles. + ## 1.15.0 (13th September 2022) - Adds documentation section filenames into the model. diff --git a/structurizr-core/src/com/structurizr/view/Styles.java b/structurizr-core/src/com/structurizr/view/Styles.java index ddf92ccb4..70e0dadf1 100644 --- a/structurizr-core/src/com/structurizr/view/Styles.java +++ b/structurizr-core/src/com/structurizr/view/Styles.java @@ -83,6 +83,20 @@ public RelationshipStyle addRelationshipStyle(String tag) { return relationshipStyle; } + /** + * Gets the element style that has been defined (in this workspace) for the given tag. + * + * @param tag the tag (a String) + * @return an ElementStyle instance, or null if no element style has been defined in this workspace + */ + public ElementStyle getElementStyle(String tag) { + if (StringUtils.isNullOrEmpty(tag)) { + throw new IllegalArgumentException("A tag must be specified."); + } + + return elements.stream().filter(es -> es.getTag().equals(tag)).findFirst().orElse(null); + } + /** * Finds the element style for the given tag. This method creates an empty style, * and copies properties from any element styles (from the workspace and any themes) for the given tag. @@ -120,6 +134,20 @@ public ElementStyle findElementStyle(String tag) { } } + /** + * Gets the relationship style that has been defined (in this workspace) for the given tag. + * + * @param tag the tag (a String) + * @return an RelationshipStyle instance, or null if no relationship style has been defined in this workspace + */ + public RelationshipStyle getRelationshipStyle(String tag) { + if (StringUtils.isNullOrEmpty(tag)) { + throw new IllegalArgumentException("A tag must be specified."); + } + + return relationships.stream().filter(rs -> rs.getTag().equals(tag)).findFirst().orElse(null); + } + /** * Finds the relationship style for the given tag. This method creates an empty style, * and copies properties from any relationship styles (from the workspace and any themes) for the given tag. diff --git a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java index 0a663da2a..36bccbf94 100644 --- a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java @@ -333,4 +333,66 @@ void clearRelationshipStyles_RemovesAllRelationshipStyles() { assertEquals(0, styles.getRelationships().size()); } + @Test + void getElementStyle_ThrowsAnException_WhenGivenNoTag() { + try { + styles.getElementStyle(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + + try { + styles.getElementStyle(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + } + + @Test + void getElementStyle_ReturnsNull_WhenThereIsNoStyleForTheGivenTag() { + ElementStyle style = styles.getElementStyle("Tag"); + assertNull(style); + } + + @Test + void getElementStyle_ReturnsTheElementStyle_WhenThereIsAStyleForTheGivenTag() { + styles.addElementStyle("Tag").background("#ffffff"); + + ElementStyle style = styles.getElementStyle("Tag"); + assertEquals("#ffffff", style.getBackground()); + } + + @Test + void getRelationshipStyle_ThrowsAnException_WhenGivenNoTag() { + try { + styles.getRelationshipStyle(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + + try { + styles.getRelationshipStyle(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + } + + @Test + void getRelationshipStyle_ReturnsNull_WhenThereIsNoStyleForTheGivenTag() { + RelationshipStyle style = styles.getRelationshipStyle("Tag"); + assertNull(style); + } + + @Test + void getRelationshipStyle_ReturnsTheRelationshipStyle_WhenThereIsAStyleForTheGivenTag() { + styles.addRelationshipStyle("Tag").color("#ffffff"); + + RelationshipStyle style = styles.getRelationshipStyle("Tag"); + assertEquals("#ffffff", style.getColor()); + } + } \ No newline at end of file From eb8112148b7a47766f0544d347d7629fd42c24ed Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 29 Sep 2022 22:50:09 +0200 Subject: [PATCH 034/418] Adds support for element icons being specified as filenames (rather than full URLs) in themes. --- .../src/com/structurizr/view/ThemeUtils.java | 16 ++++++++++++++ .../com/structurizr/view/ThemeUtilsTests.java | 22 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/structurizr-client/src/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/com/structurizr/view/ThemeUtils.java index ce1eef78b..6f7c79c8d 100644 --- a/structurizr-client/src/com/structurizr/view/ThemeUtils.java +++ b/structurizr-client/src/com/structurizr/view/ThemeUtils.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.structurizr.Workspace; import com.structurizr.io.WorkspaceWriterException; +import com.structurizr.util.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; @@ -79,6 +80,21 @@ public static void loadThemes(Workspace workspace) throws Exception { objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Theme theme = objectMapper.readValue(json, Theme.class); + String baseUrl = url.substring(0, url.lastIndexOf('/') + 1); + + for (ElementStyle elementStyle : theme.getElements()) { + String icon = elementStyle.getIcon(); + if (!StringUtils.isNullOrEmpty(icon)) { + if (icon.startsWith("http")) { + // okay, image served over HTTP + } else if (icon.startsWith("data:image")) { + // also okay, data URI + } else { + // convert the relative icon filename into a full URL + elementStyle.setIcon(baseUrl + icon); + } + } + } workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(theme); } diff --git a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java index afe40f9d0..2d390a47c 100644 --- a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java @@ -39,7 +39,7 @@ void loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { assertNotNull(style); assertEquals("#d6242d", style.getStroke()); assertEquals("#d6242d", style.getColor()); - assertNotNull(style.getIcon()); + assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Alexa-For-Business_light-bg@4x.png", style.getIcon()); } @Test @@ -129,4 +129,24 @@ void findRelationshipStyle_WithThemes() { assertEquals(Integer.valueOf(100), style.getOpacity()); } + @Test + void loadThemes_ReplacesRelativeIconReferences() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + softwareSystem.addTags("Amazon Web Services - Alexa For Business"); + workspace.getViews().getConfiguration().setThemes("https://raw.githubusercontent.com/structurizr/themes/master/amazon-web-services-2020.04.30/theme.json"); + + ThemeUtils.loadThemes(workspace); + + // there should still be zero styles in the workspace + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getElements().size()); + + // but we should be able to find a style included in the theme + ElementStyle style = workspace.getViews().getConfiguration().getStyles().findElementStyle(softwareSystem); + assertNotNull(style); + assertEquals("#d6242d", style.getStroke()); + assertEquals("#d6242d", style.getColor()); + assertEquals("https://raw.githubusercontent.com/structurizr/themes/master/amazon-web-services-2020.04.30/Alexa-For-Business_light-bg@4x.png", style.getIcon()); + } + } \ No newline at end of file From 7cb174502432b94d10d680cad7f6a4b175efaa58 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 29 Sep 2022 22:51:41 +0200 Subject: [PATCH 035/418] Update version/changelog. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3d5478eb3..f823f1730 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.15.1' + version = '1.15.2' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 4752fe2a8..86b1ced51 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.15.2 (unreleased to Maven Central) + +- Adds support for element icons being specified as filenames (rather than full URLs) in themes. + ## 1.15.1 (23rd September 2022) - Adds some additional functionality for getting and finding element/relationship styles. From ce6f36704cbef9b9a765a1281adfecbb7d3392ee Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 3 Oct 2022 07:53:42 +0100 Subject: [PATCH 036/418] Added release date. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 86b1ced51..4bf540b6d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.15.2 (unreleased to Maven Central) +## 1.15.2 (3rd October 2022) - Adds support for element icons being specified as filenames (rather than full URLs) in themes. From 0d53848be04d34c11c1efab8ce693fdfb32460d2 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 10 Oct 2022 09:23:52 +0100 Subject: [PATCH 037/418] Fixes some transitive dependencies. --- structurizr-client/build.gradle | 11 +++-------- structurizr-core/build.gradle | 7 +++---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index e869bb738..c47bdc04d 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,14 +2,9 @@ dependencies { api project(':structurizr-core') - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.4' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4' - - implementation 'org.apache.httpcomponents.client5:httpclient5:5.1.3' - - implementation 'javax.xml.bind:jaxb-api:2.3.0' - - implementation 'commons-logging:commons-logging:1.2' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.4' + api 'org.apache.httpcomponents.client5:httpclient5:5.1.3' + api 'javax.xml.bind:jaxb-api:2.3.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 08250abb3..406636d45 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,9 +1,8 @@ dependencies { - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.3' - implementation 'com.google.code.findbugs:jsr305:3.0.2' - - implementation 'commons-logging:commons-logging:1.2' + api 'com.fasterxml.jackson.core:jackson-annotations:2.13.4' + api 'com.google.code.findbugs:jsr305:3.0.2' + api 'commons-logging:commons-logging:1.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' testImplementation 'org.assertj:assertj-core:3.23.1' From f3d80e1919aae773eee8d076c49cb33a63c462bb Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 10 Oct 2022 09:24:19 +0100 Subject: [PATCH 038/418] Bump version number. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f823f1730..6e459fc46 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.15.2' + version = '1.15.3' repositories { mavenCentral() From 4a37a4edfb4ab2b9489db07b5b65df8818bc3a2c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 11 Oct 2022 13:10:20 +0100 Subject: [PATCH 039/418] Updated to reflect release. --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 4bf540b6d..576719449 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.15.3 (11th October 2022) + +- Updates some transitive dependencies. + ## 1.15.2 (3rd October 2022) - Adds support for element icons being specified as filenames (rather than full URLs) in themes. From 4015cf7f13500a9d8fa2245a85d7bd0655905398 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 18 Oct 2022 13:05:03 +0100 Subject: [PATCH 040/418] Adds support for element style stroke widths (re: https://github.com/structurizr/structurizr/issues/124). --- build.gradle | 2 +- docs/changelog.md | 4 +++ .../com/structurizr/view/ElementStyle.java | 31 ++++++++++++++++ .../structurizr/view/ElementStyleTests.java | 36 +++++++++++++++++++ .../com/structurizr/view/StylesTests.java | 6 +++- 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6e459fc46..9a351a4cc 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.15.3' + version = '1.16.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 576719449..efd7157e8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.16.0 (unreleased to Maven Central) + +- Adds support for element style stroke widths. + ## 1.15.3 (11th October 2022) - Updates some transitive dependencies. diff --git a/structurizr-core/src/com/structurizr/view/ElementStyle.java b/structurizr-core/src/com/structurizr/view/ElementStyle.java index 75999d2ea..3302b1fc6 100644 --- a/structurizr-core/src/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/com/structurizr/view/ElementStyle.java @@ -26,6 +26,9 @@ public final class ElementStyle extends AbstractStyle { @JsonInclude(value = JsonInclude.Include.NON_NULL) private String stroke; + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer strokeWidth; + @JsonInclude(value = JsonInclude.Include.NON_NULL) private String color; @@ -164,6 +167,30 @@ public ElementStyle stroke(String color) { return this; } + /** + * Gets the stroke width, in pixels, between 1 and 10. + * + * @return the stroke width + */ + public Integer getStrokeWidth() { + return strokeWidth; + } + + public void setStrokeWidth(Integer strokeWidth) { + if (strokeWidth == null) { + this.strokeWidth = null; + } else if (strokeWidth < 1) { + this.strokeWidth = 1; + } else { + this.strokeWidth = Math.min(10, strokeWidth); + } + } + + public ElementStyle strokeWidth(Integer strokeWidth) { + setStrokeWidth(strokeWidth); + return this; + } + /** * Gets the foreground (text) colour of the element, as a HTML RGB hex string (e.g. #123456). * @@ -352,6 +379,10 @@ void copyFrom(ElementStyle elementStyle) { this.setStroke(elementStyle.getStroke()); } + if (elementStyle.getStrokeWidth() != null) { + this.setStrokeWidth(elementStyle.getStrokeWidth()); + } + if (!StringUtils.isNullOrEmpty(elementStyle.getColor())) { this.setColor(elementStyle.getColor()); } diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java index 779c0b984..83b6768ec 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java @@ -294,4 +294,40 @@ void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { assertEquals("value", style.getProperties().get("name")); } + @Test + void setStrokeWidth_WhenNullIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setStrokeWidth(10); + style.setStrokeWidth(null); + assertNull(style.getStrokeWidth()); + } + + @Test + void setStrokeWidth_WhenANegativeIntegerIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setStrokeWidth(-1); + assertEquals(1, style.getStrokeWidth()); + } + + @Test + void setStrokeWidth_WhenZeroIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setStrokeWidth(0); + assertEquals(1, style.getStrokeWidth()); + } + + @Test + void setStrokeWidth_WhenAPositiveIntegerIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setStrokeWidth(10); + assertEquals(10, style.getStrokeWidth()); + } + + @Test + void setStrokeWidth_WhenAPositiveIntegerOver10IsSpecified() { + ElementStyle style = new ElementStyle(); + style.setStrokeWidth(20); + assertEquals(10, style.getStrokeWidth()); + } + } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java index 36bccbf94..a47b3a213 100644 --- a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java @@ -22,6 +22,7 @@ void findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { assertNull(style.getIcon()); assertEquals(Border.Solid, style.getBorder()); assertEquals("#9a9a9a", style.getStroke()); + assertNull(style.getStrokeWidth()); assertEquals(Integer.valueOf(100), style.getOpacity()); assertEquals(true, style.getMetadata()); assertEquals(true, style.getDescription()); @@ -40,6 +41,7 @@ void findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { assertNull(style.getIcon()); assertEquals(Border.Solid, style.getBorder()); assertEquals("#9a9a9a", style.getStroke()); + assertNull(style.getStrokeWidth()); assertEquals(Integer.valueOf(100), style.getOpacity()); assertEquals(true, style.getMetadata()); assertEquals(true, style.getDescription()); @@ -51,7 +53,7 @@ void findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { element.addTags("Some Tag"); styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#ff0000").color("#ffffff"); - styles.addElementStyle("Some Tag").color("#0000ff").stroke("#00ff00").shape(Shape.RoundedBox).width(123).height(456); + styles.addElementStyle("Some Tag").color("#0000ff").stroke("#00ff00").strokeWidth(2).shape(Shape.RoundedBox).width(123).height(456); ElementStyle style = styles.findElementStyle(element); assertEquals(Integer.valueOf(123), style.getWidth()); @@ -63,6 +65,7 @@ void findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { assertNull(style.getIcon()); assertEquals(Border.Solid, style.getBorder()); assertEquals("#00ff00", style.getStroke()); + assertEquals(2, style.getStrokeWidth()); assertEquals(Integer.valueOf(100), style.getOpacity()); assertEquals(true, style.getMetadata()); assertEquals(true, style.getDescription()); @@ -89,6 +92,7 @@ void findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDe assertNull(style.getIcon()); assertEquals(Border.Solid, style.getBorder()); assertEquals("#00ff00", style.getStroke()); + assertNull(style.getStrokeWidth()); assertEquals(Integer.valueOf(100), style.getOpacity()); assertEquals(true, style.getMetadata()); assertEquals(true, style.getDescription()); From a143f196825eb892acbe848f331d0fb59d7c849c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 24 Oct 2022 13:59:55 +0100 Subject: [PATCH 041/418] Ensures that style properties are included when finding a style for an element/relationship. --- structurizr-core/src/com/structurizr/view/ElementStyle.java | 4 ++++ .../src/com/structurizr/view/RelationshipStyle.java | 4 ++++ .../test/unit/com/structurizr/view/StylesTests.java | 6 ++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/structurizr-core/src/com/structurizr/view/ElementStyle.java b/structurizr-core/src/com/structurizr/view/ElementStyle.java index 3302b1fc6..bbe4b6438 100644 --- a/structurizr-core/src/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/com/structurizr/view/ElementStyle.java @@ -414,6 +414,10 @@ void copyFrom(ElementStyle elementStyle) { if (elementStyle.getDescription() != null) { this.setDescription(elementStyle.getDescription()); } + + for (String name : elementStyle.getProperties().keySet()) { + this.addProperty(name, elementStyle.getProperties().get(name)); + } } } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/com/structurizr/view/RelationshipStyle.java index 92219414d..bea120e0a 100644 --- a/structurizr-core/src/com/structurizr/view/RelationshipStyle.java +++ b/structurizr-core/src/com/structurizr/view/RelationshipStyle.java @@ -240,6 +240,10 @@ void copyFrom(RelationshipStyle relationshipStyle) { if (relationshipStyle.getOpacity() != null) { this.setOpacity(relationshipStyle.getOpacity()); } + + for (String name : relationshipStyle.getProperties().keySet()) { + this.addProperty(name, relationshipStyle.getProperties().get(name)); + } } } diff --git a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java index a47b3a213..238ba77a6 100644 --- a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/StylesTests.java @@ -80,7 +80,7 @@ void findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDe SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#ff0000").color("#ffffff"); - styles.addElementStyle("Some Tag").color("#0000ff").stroke("#00ff00").shape(Shape.RoundedBox).width(123).height(456); + styles.addElementStyle("Some Tag").color("#0000ff").stroke("#00ff00").shape(Shape.RoundedBox).width(123).height(456).addProperty("name", "value"); ElementStyle style = styles.findElementStyle(softwareSystemInstance); assertEquals(Integer.valueOf(123), style.getWidth()); @@ -96,6 +96,7 @@ void findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDe assertEquals(Integer.valueOf(100), style.getOpacity()); assertEquals(true, style.getMetadata()); assertEquals(true, style.getDescription()); + assertEquals("value", style.getProperties().get("name")); } @Test @@ -161,7 +162,7 @@ void findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { relationship.addTags("Some Tag"); styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); - styles.addRelationshipStyle("Some Tag").color("#0000ff"); + styles.addRelationshipStyle("Some Tag").color("#0000ff").addProperty("name", "value"); RelationshipStyle style = styles.findRelationshipStyle(relationship); assertEquals(Integer.valueOf(2), style.getThickness()); @@ -172,6 +173,7 @@ void findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { assertEquals(Integer.valueOf(200), style.getWidth()); assertEquals(Integer.valueOf(50), style.getPosition()); assertEquals(Integer.valueOf(100), style.getOpacity()); + assertEquals("value", style.getProperties().get("name")); } @Test From 5ffc9fc556c802d849a98369527ec1e3a408144a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 24 Oct 2022 14:12:00 +0100 Subject: [PATCH 042/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index efd7157e8..eeded6459 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.16.0 (unreleased to Maven Central) +## 1.16.0 (24th October 2022) - Adds support for element style stroke widths. From c249662fdf245aa5953fd1cd3dd9ae57656b3cb1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 27 Oct 2022 07:49:15 +0200 Subject: [PATCH 043/418] Fixes comment typos. --- structurizr-core/src/com/structurizr/view/Configuration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structurizr-core/src/com/structurizr/view/Configuration.java b/structurizr-core/src/com/structurizr/view/Configuration.java index dc5dfdd75..dc9043da6 100644 --- a/structurizr-core/src/com/structurizr/view/Configuration.java +++ b/structurizr-core/src/com/structurizr/view/Configuration.java @@ -209,7 +209,7 @@ public void setViewSortOrder(ViewSortOrder viewSortOrder) { } /** - * Gets the collection of name-value property pairs associated with this workspace, as a Map. + * Gets the collection of name-value property pairs, as a Map. * * @return a Map (String, String) (empty if there are no properties) */ @@ -218,7 +218,7 @@ public Map getProperties() { } /** - * Adds a name-value pair property to this workspace. + * Adds a name-value pair property. * * @param name the name of the property * @param value the value of the property From a1420029bfb10043f2b920e38afcec4e7c991450 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 27 Oct 2022 07:49:58 +0200 Subject: [PATCH 044/418] Adds name-value properties to views. --- .../src/com/structurizr/view/View.java | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/structurizr-core/src/com/structurizr/view/View.java b/structurizr-core/src/com/structurizr/view/View.java index f12a6a08b..fdfbadfd9 100644 --- a/structurizr-core/src/com/structurizr/view/View.java +++ b/structurizr-core/src/com/structurizr/view/View.java @@ -2,20 +2,18 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; +import com.structurizr.PropertyHolder; import com.structurizr.model.*; import com.structurizr.util.StringUtils; import javax.annotation.Nonnull; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** * The superclass for all views (static views, dynamic views and deployment views). */ -public abstract class View { +public abstract class View implements PropertyHolder { private static final int DEFAULT_RANK_SEPARATION = 300; private static final int DEFAULT_NODE_SEPARATION = 300; @@ -30,6 +28,7 @@ public abstract class View { private AutomaticLayout automaticLayout = null; private boolean mergeFromRemote = true; private String title; + private Map properties = new HashMap<>(); private Set elementViews = new LinkedHashSet<>(); private Set relationshipViews = new LinkedHashSet<>(); @@ -543,4 +542,37 @@ protected void addNearestNeighbours(Element element, Class getProperties() { + return new HashMap<>(properties); + } + + /** + * Adds a name-value pair property to this view. + * + * @param name the name of the property + * @param value the value of the property + */ + public void addProperty(String name, String value) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A property name must be specified."); + } + + if (value == null || value.trim().length() == 0) { + throw new IllegalArgumentException("A property value must be specified."); + } + + properties.put(name, value); + } + + void setProperties(Map properties) { + if (properties != null) { + this.properties = new HashMap<>(properties); + } + } + } \ No newline at end of file From 25001caeadf661af58ded4f66903415f9dc12ece Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 27 Oct 2022 21:25:13 +0200 Subject: [PATCH 045/418] Updated to reflect release. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9a351a4cc..4dda6f156 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.16.0' + version = '1.16.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index eeded6459..2fe93e4cf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.16.1 (27th October 2022) + +- Adds name-value properties to views. + ## 1.16.0 (24th October 2022) - Adds support for element style stroke widths. From d4e895e14ef8e9dcfd4a4b086a7910fe8fdb226a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 22 Dec 2022 18:38:01 +0000 Subject: [PATCH 046/418] Upgrade dependencies. --- structurizr-client/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index c47bdc04d..881ac490f 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,9 +2,9 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.13.4' - api 'org.apache.httpcomponents.client5:httpclient5:5.1.3' - api 'javax.xml.bind:jaxb-api:2.3.0' + api 'com.fasterxml.jackson.core:jackson-databind:2.14.1' + api 'org.apache.httpcomponents.client5:httpclient5:5.2.1' + api 'javax.xml.bind:jaxb-api:2.3.1' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' From 7774dd7027be59ab2cf90c4e97851028f9009a4f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 22 Dec 2022 18:39:28 +0000 Subject: [PATCH 047/418] Updated to reflect release. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4dda6f156..18a8e3c84 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.16.1' + version = '1.16.2' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 2fe93e4cf..47972e38d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.16.2 (22nd December 2022) + +- Upgraded dependencies. + ## 1.16.1 (27th October 2022) - Adds name-value properties to views. From c5605ab2487c09b0bbc4f43663c6f6193c8e11c1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 28 Dec 2022 09:50:25 +0000 Subject: [PATCH 048/418] Fixes #190. --- docs/views.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/views.md b/docs/views.md index 035a6fd8f..b53c9e22f 100644 --- a/docs/views.md +++ b/docs/views.md @@ -2,14 +2,19 @@ Once you've [added elements to a model](model.md), you can create one or more views to visualise parts of the model, which can subsequently be rendered as diagrams by a number of different tools. -Structurizr for Java supports all of the view types described in the [C4 model](https://c4model.com), and the Java classes implementing these views can be found in the [com.structurizr.view](https://github.com/structurizr/java/tree/master/structurizr-core/src/com/structurizr/view) package as follows: - -* [SystemContextView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/SystemContextView.java) -* [ContainerView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ContainerView.java) -* [ComponentView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ComponentView.java) -* [SystemLandscapeView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java) -* [DynamicView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/DynamicView.java) -* [DeploymentView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/DeploymentView.java) +Structurizr for Java supports the following view types described in the [C4 model](https://c4model.com), and the Java classes implementing these views can be found in the [com.structurizr.view](https://github.com/structurizr/java/tree/master/structurizr-core/src/com/structurizr/view) package as follows: + +* Core diagrams + * [SystemContextView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/SystemContextView.java) + * [ContainerView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ContainerView.java) + * [ComponentView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ComponentView.java) + +* Supplementary diagrams + * [SystemLandscapeView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java) + * [DynamicView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/DynamicView.java) + * [DeploymentView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/DeploymentView.java) + +Please note that code diagrams (level 4 of the C4 model) are not supported. ## Creating views From 44df84ca745ecf272bfacb69c60b7f850a239575 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 4 Jan 2023 10:29:49 +0000 Subject: [PATCH 049/418] Fixes case-sensitivity inconsistencies related to element names and relationship descriptions (#183). --- build.gradle | 2 +- docs/changelog.md | 4 +++ .../structurizr/util/WorkspaceUtilsTests.java | 31 +++++++++++++++++++ .../src/com/structurizr/model/Element.java | 2 +- .../src/com/structurizr/model/Model.java | 6 ++-- .../structurizr/model/RelationshipTests.java | 10 ++++++ 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 18a8e3c84..404bebda5 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.16.2' + version = '1.17.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 47972e38d..ca2b973e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.17.0 (unreleased) + +- Fixes case-sensitivity inconsistencies related to element names and relationship descriptions (#183). + ## 1.16.2 (22nd December 2022) - Upgraded dependencies. diff --git a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java index c6e3c0188..02155b905 100644 --- a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java @@ -1,6 +1,8 @@ package com.structurizr.util; import com.structurizr.Workspace; +import com.structurizr.model.Model; +import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; import java.io.File; @@ -121,4 +123,33 @@ void fromJson() throws Exception { assertEquals("Description", workspace.getDescription()); } + @Test + void elementNamesAreCaseSensitive() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Name"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("NAME"); + SoftwareSystem softwareSystem3 = model.addSoftwareSystem("name"); + + assertEquals(3, model.getSoftwareSystems().size()); + + WorkspaceUtils.fromJson(WorkspaceUtils.toJson(workspace, false)); // no exception thrown + } + + @Test + void relationshipDescriptionsAreCaseSensitive() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("1"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("2"); + + softwareSystem1.uses(softwareSystem2, "Uses"); + softwareSystem1.uses(softwareSystem2, "USES"); + softwareSystem1.uses(softwareSystem2, "uses"); + + assertEquals(3, softwareSystem1.getRelationships().size()); + + WorkspaceUtils.fromJson(WorkspaceUtils.toJson(workspace, false)); // no exception thrown + } + } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/Element.java b/structurizr-core/src/com/structurizr/model/Element.java index 7d4c994cd..a22b8f895 100644 --- a/structurizr-core/src/com/structurizr/model/Element.java +++ b/structurizr-core/src/com/structurizr/model/Element.java @@ -192,7 +192,7 @@ public Relationship getEfferentRelationshipWith(Element element, String descript } boolean has(Relationship relationship) { - return relationships.stream().anyMatch(r -> r.getDestination().equals(relationship.getDestination()) && r.getDescription().equalsIgnoreCase(relationship.getDescription())); + return relationships.stream().anyMatch(r -> r.getDestination().equals(relationship.getDestination()) && r.getDescription().equals(relationship.getDescription())); } void addRelationship(Relationship relationship) { diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/com/structurizr/model/Model.java index dcb001a87..7c1e4b0c0 100644 --- a/structurizr-core/src/com/structurizr/model/Model.java +++ b/structurizr-core/src/com/structurizr/model/Model.java @@ -544,14 +544,14 @@ private void hydrateDeploymentNode(DeploymentNode deploymentNode, DeploymentNode } private void checkNameIsUnique(Collection elements, String name, String errorMessage) { - if (elements.stream().filter(e -> e.getName().equalsIgnoreCase(name)).count() != 1) { + if (elements.stream().filter(e -> e.getName().equals(name)).count() != 1) { throw new WorkspaceValidationException( String.format(errorMessage, name)); } } private void checkNameIsUnique(Collection deploymentNodes, String name, String environment, String errorMessage) { - if (deploymentNodes.stream().filter(dn -> dn.getName().equalsIgnoreCase(name) && dn.getEnvironment().equals(environment)).count() != 1) { + if (deploymentNodes.stream().filter(dn -> dn.getName().equals(name) && dn.getEnvironment().equals(environment)).count() != 1) { throw new WorkspaceValidationException( String.format(errorMessage, name)); } @@ -568,7 +568,7 @@ private void checkChildNamesAreUnique(DeploymentNode deploymentNode) { } private void checkDescriptionIsUnique(Collection relationships, Relationship relationship) { - if (relationships.stream().filter(r -> r.getDestination().equals(relationship.getDestination()) && r.getDescription().equalsIgnoreCase(relationship.getDescription())).count() != 1) { + if (relationships.stream().filter(r -> r.getDestination().equals(relationship.getDestination()) && r.getDescription().equals(relationship.getDescription())).count() != 1) { throw new WorkspaceValidationException( String.format( "A relationship with the description \"%s\" already exists between \"%s\" and \"%s\".", diff --git a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java index f8edd18ec..f596d09fb 100644 --- a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java @@ -122,4 +122,14 @@ void interactionStyle_CanBeSetToNull() { assertFalse(relationship.getTagsAsSet().contains(Tags.SYNCHRONOUS)); } + @Test + void relationshipDescriptionsAreCaseSensitive() { + softwareSystem1.uses(softwareSystem2, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + softwareSystem1.uses(softwareSystem2, "USES"); + softwareSystem1.uses(softwareSystem2, "uses"); + + assertEquals(3, softwareSystem1.getRelationships().size()); + } + } \ No newline at end of file From 28c05ba4c13dd13ee35203b475ae4196d3dfc226 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 4 Jan 2023 13:29:40 +0000 Subject: [PATCH 050/418] Adds support for setting deployment node instances to positive integers or a range (e.g. 0..1, 0..N, 0..*, 1..N, 1..*, 5..10, etc). --- docs/changelog.md | 3 +- .../com/structurizr/model/DeploymentNode.java | 30 ++++++- .../model/DeploymentNodeTests.java | 81 +++++++++++++++---- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index ca2b973e1..e1177aae0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,8 @@ ## 1.17.0 (unreleased) -- Fixes case-sensitivity inconsistencies related to element names and relationship descriptions (#183). +- Fixes case-sensitivity inconsistencies related to element names and relationship descriptions (#183). +- Adds support for setting deployment node instances to positive integers or a range (e.g. 0..1, 0..N, 0..*, 1..N, 1..*, 5..10, etc). ## 1.16.2 (22nd December 2022) diff --git a/structurizr-core/src/com/structurizr/model/DeploymentNode.java b/structurizr-core/src/com/structurizr/model/DeploymentNode.java index 020b8fff2..faa9c613a 100644 --- a/structurizr-core/src/com/structurizr/model/DeploymentNode.java +++ b/structurizr-core/src/com/structurizr/model/DeploymentNode.java @@ -1,6 +1,7 @@ package com.structurizr.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; import java.util.*; @@ -22,7 +23,7 @@ public final class DeploymentNode extends DeploymentElement { private String technology; - private int instances = 1; + private String instances = "1"; private Set children = new HashSet<>(); private Set infrastructureNodes = new HashSet<>(); @@ -360,13 +361,34 @@ public void setTechnology(String technology) { this.technology = technology; } - public int getInstances() { + public String getInstances() { return instances; } public void setInstances(int instances) { - if (instances < 1) { - throw new IllegalArgumentException("Number of instances must be a positive integer."); + setInstances("" + instances); + } + + @JsonSetter + public void setInstances(String instances) { + try { + int instancesAsInteger = Integer.parseInt(instances); + if (instancesAsInteger < 1) { + throw new IllegalArgumentException("Number of instances must be a positive integer or a range."); + } + } catch (NumberFormatException nfe) { + if (instances.matches("\\d*\\.\\.\\d*")) { + String[] range = instances.split("\\.\\."); + if (Integer.parseInt(range[0]) > Integer.parseInt(range[1])) { + throw new IllegalArgumentException("Range upper bound must be greater than the lower bound."); + } + } else if (instances.matches("\\d*..N")) { + // okay + } else if (instances.matches("\\d*..\\*")) { + // okay + } else { + throw new IllegalArgumentException("Number of instances must be a positive integer or a range."); + } } this.instances = instances; diff --git a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java b/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java index e7e0abd3f..303d3ded6 100644 --- a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java @@ -107,7 +107,7 @@ void addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { assertEquals("Description", child.getDescription()); assertEquals("Technology", child.getTechnology()); assertEquals("Default", child.getEnvironment()); - assertEquals(1, child.getInstances()); + assertEquals("1", child.getInstances()); assertTrue(child.getProperties().isEmpty()); assertTrue(parent.getChildren().contains(child)); @@ -117,7 +117,7 @@ void addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { assertEquals("Description", child.getDescription()); assertEquals("Technology", child.getTechnology()); assertEquals("Default", child.getEnvironment()); - assertEquals(4, child.getInstances()); + assertEquals("4", child.getInstances()); assertTrue(child.getProperties().isEmpty()); assertTrue(parent.getChildren().contains(child)); @@ -127,7 +127,7 @@ void addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { assertEquals("Description", child.getDescription()); assertEquals("Technology", child.getTechnology()); assertEquals("Default", child.getEnvironment()); - assertEquals(4, child.getInstances()); + assertEquals("4", child.getInstances()); assertEquals(1, child.getProperties().size()); assertEquals("value", child.getProperties().get("name")); assertTrue(parent.getChildren().contains(child)); @@ -195,33 +195,86 @@ void getInfrastructureNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsAInf } @Test - void setInstances() { - DeploymentNode deploymentNode = new DeploymentNode(); - deploymentNode.setInstances(8); - - assertEquals(8, deploymentNode.getInstances()); + void setInstances_ThrowsAnException_WhenAZeroIsSpecified() { + try { + DeploymentNode deploymentNode = new DeploymentNode(); + deploymentNode.setInstances("0"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Number of instances must be a positive integer or a range.", iae.getMessage()); + } } @Test - void setInstances_ThrowsAnException_WhenZeroIsSpecified() { + void setInstances_ThrowsAnException_WhenANegativeNumberIsSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); - deploymentNode.setInstances(0); + deploymentNode.setInstances("-1"); fail(); } catch (IllegalArgumentException iae) { - assertEquals("Number of instances must be a positive integer.", iae.getMessage()); + assertEquals("Number of instances must be a positive integer or a range.", iae.getMessage()); } } @Test - void setInstances_ThrowsAnException_WhenANegativeNumberIsSpecified() { + void setInstancesAsPositiveInteger() { + DeploymentNode deploymentNode = new DeploymentNode(); + + deploymentNode.setInstances("8"); + assertEquals("8", deploymentNode.getInstances()); + + deploymentNode.setInstances(8); + assertEquals("8", deploymentNode.getInstances()); + } + + @Test + void setInstances_ThrowsAnException_WhenAnInvalidRangeIsSpecified() { try { DeploymentNode deploymentNode = new DeploymentNode(); - deploymentNode.setInstances(-1); + deploymentNode.setInstances("x..N"); fail(); } catch (IllegalArgumentException iae) { - assertEquals("Number of instances must be a positive integer.", iae.getMessage()); + assertEquals("Number of instances must be a positive integer or a range.", iae.getMessage()); } + + try { + DeploymentNode deploymentNode = new DeploymentNode(); + deploymentNode.setInstances("2..1"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Range upper bound must be greater than the lower bound.", iae.getMessage()); + } + } + + @Test + void setInstancesAsRangeWithBoundedUpperRange() { + DeploymentNode deploymentNode = new DeploymentNode(); + + deploymentNode.setInstances("0..2"); + assertEquals("0..2", deploymentNode.getInstances()); + + deploymentNode.setInstances("1..2"); + assertEquals("1..2", deploymentNode.getInstances()); + + deploymentNode.setInstances("5..10"); + assertEquals("5..10", deploymentNode.getInstances()); + } + + @Test + void setInstancesAsRangeWithUnboundedUpperRange() { + DeploymentNode deploymentNode = new DeploymentNode(); + + deploymentNode.setInstances("0..N"); + assertEquals("0..N", deploymentNode.getInstances()); + + deploymentNode.setInstances("1..N"); + assertEquals("1..N", deploymentNode.getInstances()); + + deploymentNode.setInstances("0..*"); + assertEquals("0..*", deploymentNode.getInstances()); + + deploymentNode.setInstances("1..*"); + assertEquals("1..*", deploymentNode.getInstances()); } } \ No newline at end of file From 68e728a448d8bc10a3244f45c5a1aaf3e1440b44 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 5 Jan 2023 15:58:23 +0000 Subject: [PATCH 051/418] Adds documentation to containers. --- .../src/com/structurizr/model/Container.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/model/Container.java b/structurizr-core/src/com/structurizr/model/Container.java index a00e5a538..17f34ef00 100644 --- a/structurizr-core/src/com/structurizr/model/Container.java +++ b/structurizr-core/src/com/structurizr/model/Container.java @@ -1,19 +1,24 @@ package com.structurizr.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Documentation; +import javax.annotation.Nonnull; import java.util.*; /** * Represents a "container" in the C4 model. */ -public final class Container extends StaticStructureElement { +public final class Container extends StaticStructureElement implements Documentable { private SoftwareSystem parent; private String technology; private Set components = new LinkedHashSet<>(); + private Documentation documentation = new Documentation(); + Container() { } @@ -193,4 +198,22 @@ public Set getDefaultTags() { return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.CONTAINER)); } + /** + * Gets the documentation associated with this container. + * + * @return a Documentation object + */ + public Documentation getDocumentation() { + return documentation; + } + + /** + * Sets the documentation associated with this container. + * + * @param documentation a Documentation object + */ + void setDocumentation(@Nonnull Documentation documentation) { + this.documentation = documentation; + } + } \ No newline at end of file From 36df560e5af6f46245c30229d43ae8c9e97259f6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 5 Jan 2023 16:00:14 +0000 Subject: [PATCH 052/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index e1177aae0..3a51e47b6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.17.0 (unreleased) +## 1.17.0 (5th January 2023) - Fixes case-sensitivity inconsistencies related to element names and relationship descriptions (#183). - Adds support for setting deployment node instances to positive integers or a range (e.g. 0..1, 0..N, 0..*, 1..N, 1..*, 5..10, etc). From 9ba13f81fba1c5c90d6775fffe49b703807d5314 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 5 Jan 2023 16:29:50 +0000 Subject: [PATCH 053/418] Update Jackson Annotations. --- structurizr-core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 406636d45..9bc594da4 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,6 +1,6 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.13.4' + api 'com.fasterxml.jackson.core:jackson-annotations:2.14.1' api 'com.google.code.findbugs:jsr305:3.0.2' api 'commons-logging:commons-logging:1.2' From 4dc1ac0550201dbdc7f425f778ed1f7e7e198f2d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 11 Jan 2023 07:56:09 -0600 Subject: [PATCH 054/418] Fixes #191 (Layout of relationships is reset when changing the description). --- build.gradle | 2 +- docs/changelog.md | 4 + .../view/DefaultLayoutMergeStrategy.java | 22 ++++ .../view/DefaultLayoutMergeStrategyTests.java | 103 ++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 404bebda5..ed26532ad 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.17.0' + version = '1.17.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 3a51e47b6..2cd623e81 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.17.1 (unreleased) + +- Fixes #191 (Layout of relationships is reset when changing the description). + ## 1.17.0 (5th January 2023) - Fixes case-sensitivity inconsistencies related to element names and relationship descriptions (#183). diff --git a/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java b/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java index 0353269e0..6961e1186 100644 --- a/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java +++ b/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java @@ -130,6 +130,17 @@ private RelationshipView findRelationshipView(View viewWithLayoutInformation, Re } } + // if we got this far, perhaps the relationship description was changed, so try matching on ID instead + for (RelationshipView rv : viewWithLayoutInformation.getRelationships()) { + if ( + rv.getRelationship().getSource().equals(sourceElementWithLayoutInformation) && + rv.getRelationship().getDestination().equals(destinationElementWithLayoutInformation) && + rv.getRelationship().getId().equals(relationshipWithoutLayoutInformation.getId()) + ) { + return rv; + } + } + return null; } @@ -151,6 +162,17 @@ private RelationshipView findRelationshipView(View view, RelationshipView relati } } + // if we got this far, perhaps the relationship description was changed, so try matching on ID instead + for (RelationshipView rv : view.getRelationships()) { + if ( + rv.getRelationship().getSource().equals(sourceElementWithLayoutInformation) && + rv.getRelationship().getDestination().equals(destinationElementWithLayoutInformation) && + rv.getRelationship().getId().equals(relationshipWithoutLayoutInformation.getId()) && + rv.getOrder().equals(relationshipWithoutLayoutInformation.getOrder())) { + return rv; + } + } + return null; } diff --git a/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java b/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java index 18744e7d7..fdb582f80 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java @@ -2,9 +2,12 @@ import com.structurizr.Workspace; import com.structurizr.model.Container; +import com.structurizr.model.Relationship; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; +import java.util.Collections; + import static org.junit.jupiter.api.Assertions.assertEquals; public class DefaultLayoutMergeStrategyTests { @@ -171,4 +174,104 @@ void copyLayoutInformation_DoesNotThrowAnExceptionWhenAddingAnElementToAView() { strategy.copyLayoutInformation(view1, view2); } + @Test + void copyLayoutInformation_FromStaticViewWhenRelationshipDescriptionHasNotChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem a1 = workspace1.getModel().addSoftwareSystem("A"); + SoftwareSystem b1 = workspace1.getModel().addSoftwareSystem("B"); + Relationship relationship1 = a1.uses(b1, "Uses"); + SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("Key", "Description"); + view1.addAllElements(); + view1.getRelationshipView(relationship1).setVertices(Collections.singletonList(new Vertex(123, 456))); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem a2 = workspace2.getModel().addSoftwareSystem("A"); + SoftwareSystem b2 = workspace2.getModel().addSoftwareSystem("B"); + Relationship relationship2 = a2.uses(b2, "Uses"); + SystemLandscapeView view2 = workspace2.getViews().createSystemLandscapeView("Key", "Description"); + view2.addAllElements(); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(1, view2.getRelationshipView(relationship2).getVertices().size()); + assertEquals(123, view2.getRelationshipView(relationship2).getVertices().iterator().next().getX()); + assertEquals(456, view2.getRelationshipView(relationship2).getVertices().iterator().next().getY()); + } + + @Test + void copyLayoutInformation_FromStaticViewWhenRelationshipDescriptionHasChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem a1 = workspace1.getModel().addSoftwareSystem("A"); + SoftwareSystem b1 = workspace1.getModel().addSoftwareSystem("B"); + Relationship relationship1 = a1.uses(b1, "Uses"); + SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("Key", "Description"); + view1.addAllElements(); + view1.getRelationshipView(relationship1).setVertices(Collections.singletonList(new Vertex(123, 456))); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem a2 = workspace2.getModel().addSoftwareSystem("A"); + SoftwareSystem b2 = workspace2.getModel().addSoftwareSystem("B"); + Relationship relationship2 = a2.uses(b2, "Reads from and writes to"); + SystemLandscapeView view2 = workspace2.getViews().createSystemLandscapeView("Key", "Description"); + view2.addAllElements(); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(1, view2.getRelationshipView(relationship2).getVertices().size()); + assertEquals(123, view2.getRelationshipView(relationship2).getVertices().iterator().next().getX()); + assertEquals(456, view2.getRelationshipView(relationship2).getVertices().iterator().next().getY()); + } + + @Test + void copyLayoutInformation_FromDynamicViewWhenRelationshipDescriptionHasNotChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem a1 = workspace1.getModel().addSoftwareSystem("A"); + SoftwareSystem b1 = workspace1.getModel().addSoftwareSystem("B"); + Relationship relationship1 = a1.uses(b1, "Uses"); + DynamicView view1 = workspace1.getViews().createDynamicView("Key", "Description"); + RelationshipView rv1 = view1.add(a1, b1); + rv1.setVertices(Collections.singletonList(new Vertex(123, 456))); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem a2 = workspace2.getModel().addSoftwareSystem("A"); + SoftwareSystem b2 = workspace2.getModel().addSoftwareSystem("B"); + Relationship relationship2 = a2.uses(b2, "Uses"); + DynamicView view2 = workspace2.getViews().createDynamicView("Key", "Description"); + RelationshipView rv2 = view2.add(a2, b2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(1, view2.getRelationshipView(relationship2).getVertices().size()); + assertEquals(123, view2.getRelationshipView(relationship2).getVertices().iterator().next().getX()); + assertEquals(456, view2.getRelationshipView(relationship2).getVertices().iterator().next().getY()); + } + + @Test + void copyLayoutInformation_FromDynamicViewWhenRelationshipDescriptionHasChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem a1 = workspace1.getModel().addSoftwareSystem("A"); + SoftwareSystem b1 = workspace1.getModel().addSoftwareSystem("B"); + Relationship relationship1 = a1.uses(b1, "Uses"); + DynamicView view1 = workspace1.getViews().createDynamicView("Key", "Description"); + RelationshipView rv1 = view1.add(a1, b1); + rv1.setVertices(Collections.singletonList(new Vertex(123, 456))); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem a2 = workspace2.getModel().addSoftwareSystem("A"); + SoftwareSystem b2 = workspace2.getModel().addSoftwareSystem("B"); + Relationship relationship2 = a2.uses(b2, "Uses"); + DynamicView view2 = workspace2.getViews().createDynamicView("Key", "Description"); + RelationshipView rv2 = view2.add(a2, "Reads from and writes to", b2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(1, view2.getRelationshipView(relationship2).getVertices().size()); + assertEquals(123, view2.getRelationshipView(relationship2).getVertices().iterator().next().getX()); + assertEquals(456, view2.getRelationshipView(relationship2).getVertices().iterator().next().getY()); + } + } \ No newline at end of file From c004e6f003bca8aea10fb72941dd61848400a24b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 11 Jan 2023 08:18:39 -0600 Subject: [PATCH 055/418] Remove reference to quick-start project as it's going to be deleted. --- README.md | 1 - docs/getting-started.md | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index df19c8b72..a8b396223 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via t * [Client-side encryption](docs/client-side-encryption.md) * Related projects * [structurizr-dsl](https://github.com/structurizr/dsl): A text-based DSL for authoring Structurizr workspaces. - * [java-quickstart](https://github.com/structurizr/java-quickstart): A simple starting point for using Structurizr for Java * [structurizr-export](https://github.com/structurizr/export): Export model and views to external formats (e.g. PlantUML, Mermaid, etc). * [structurizr-documentation](https://github.com/structurizr/documentation): Import Markdown/AsciiDoc documentation and ADRs into a Structurizr workspace. * [java-extensions](https://github.com/structurizr/java-extensions): A collection of Structurizr for Java extensions; including the ability to extract software architecture information from code. diff --git a/docs/getting-started.md b/docs/getting-started.md index 5e11b5f7f..5a5c3425f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,17 +4,15 @@ Here is a quick overview of how to get started with Structurizr for Java so that You can find the code at [GettingStarted.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/GettingStarted.java) and the live example workspace at [https://structurizr.com/share/25441](https://structurizr.com/share/25441). -> See the [java-quickstart project](https://github.com/structurizr/java-quickstart) for a quick and simple way to get started with Structurizr for Java. - For more examples, please see [structurizr-examples](https://github.com/structurizr/examples/tree/main/java/src/main/java/com/structurizr/example). ## 1. Dependencies The Structurizr for Java binaries are hosted on [Maven Central](https://repo1.maven.org/maven2/com/structurizr/structurizr-client/) and the dependencies for use with Maven, Ivy, Gradle, etc are as follows. -Name | Description ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- -com.structurizr:structurizr-client:1.14.1 | The Structurizr API client library. +Name | Description +---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- +com.structurizr:structurizr-client | The Structurizr API client library. ## 2. Create a Java program From 4a92c8e3c6f2b9b16c3f65ccaa5592dab970da3b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 14 Jan 2023 16:38:51 +0000 Subject: [PATCH 056/418] Remove unused class. --- .../src/com/structurizr/view/ColorPair.java | 43 -------- .../com/structurizr/view/ColorPairTests.java | 97 ------------------- 2 files changed, 140 deletions(-) delete mode 100644 structurizr-core/src/com/structurizr/view/ColorPair.java delete mode 100644 structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java diff --git a/structurizr-core/src/com/structurizr/view/ColorPair.java b/structurizr-core/src/com/structurizr/view/ColorPair.java deleted file mode 100644 index 643501a25..000000000 --- a/structurizr-core/src/com/structurizr/view/ColorPair.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.structurizr.view; - -/** - * Represents a pair of colours: background and foreground. - */ -public final class ColorPair { - - private String background; - private String foreground; - - ColorPair() { - } - - public ColorPair(String background, String foreground) { - setBackground(background); - setForeground(foreground); - } - - public String getBackground() { - return background; - } - - public void setBackground(String background) { - if (Color.isHexColorCode(background)) { - this.background = background.toLowerCase(); - } else { - throw new IllegalArgumentException("'" + background + "' is not a valid hex color code."); - } - } - - public String getForeground() { - return foreground; - } - - public void setForeground(String foreground) { - if (Color.isHexColorCode(foreground)) { - this.foreground = foreground.toLowerCase(); - } else { - throw new IllegalArgumentException("'" + foreground + "' is not a valid hex color code."); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java deleted file mode 100644 index 215f1c36b..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.structurizr.view; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -public class ColorPairTests { - - @Test - void construction() { - ColorPair colorPair = new ColorPair("#ffffff", "#000000"); - assertEquals("#ffffff", colorPair.getBackground()); - assertEquals("#000000", colorPair.getForeground()); - } - - @Test - void setBackground_WithAValidHtmlColorCode() { - ColorPair colorPair = new ColorPair(); - colorPair.setBackground("#ffffff"); - assertEquals("#ffffff", colorPair.getBackground()); - } - - @Test - void setBackground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { - try { - ColorPair colorPair = new ColorPair(); - colorPair.setBackground(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("'null' is not a valid hex color code.", iae.getMessage()); - } - } - - @Test - void setBackground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { - try { - ColorPair colorPair = new ColorPair(); - colorPair.setBackground(""); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("'' is not a valid hex color code.", iae.getMessage()); - } - } - - @Test - void setBackground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { - try { - ColorPair colorPair = new ColorPair(); - colorPair.setBackground("ffffff"); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("'ffffff' is not a valid hex color code.", iae.getMessage()); - } - } - - @Test - void setForeground_WithAValidHtmlColorCode() { - ColorPair colorPair = new ColorPair(); - colorPair.setForeground("#000000"); - assertEquals("#000000", colorPair.getForeground()); - } - - @Test - void setForeground_ThrowsAnException_WhenANullHtmlColorCodeIsSpecified() { - try { - ColorPair colorPair = new ColorPair(); - colorPair.setForeground(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("'null' is not a valid hex color code.", iae.getMessage()); - } - } - - @Test - void setForeground_ThrowsAnException_WhenAnEmptyHtmlColorCodeIsSpecified() { - try { - ColorPair colorPair = new ColorPair(); - colorPair.setForeground(""); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("'' is not a valid hex color code.", iae.getMessage()); - } - } - - @Test - void setForeground_ThrowsAnException_WhenAnInvalidHtmlColorCodeIsSpecified() { - try { - ColorPair colorPair = new ColorPair(); - colorPair.setForeground("000000"); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("'000000' is not a valid hex color code.", iae.getMessage()); - } - } - -} From de09f107d547d06e170aa29a1be7a19013a495ee Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 14 Jan 2023 20:46:16 +0000 Subject: [PATCH 057/418] Adds support for using (CSS/HTML) named colors instead of hex color codes (#192). --- build.gradle | 2 +- docs/changelog.md | 3 +- .../src/com/structurizr/view/Color.java | 160 ++++++++++++++++++ .../com/structurizr/view/ElementStyle.java | 30 +++- .../structurizr/view/RelationshipStyle.java | 8 +- .../unit/com/structurizr/view/ColorTests.java | 13 +- .../structurizr/view/ElementStyleTests.java | 54 +++++- .../view/RelationshipStyleTests.java | 22 ++- 8 files changed, 271 insertions(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index ed26532ad..a5eaf58a8 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.17.1' + version = '1.18.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 2cd623e81..91eecc57c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,9 @@ # Changelog -## 1.17.1 (unreleased) +## 1.18.0 (unreleased) - Fixes #191 (Layout of relationships is reset when changing the description). +- Adds support for using (CSS/HTML) named colors instead of hex color codes (#192). ## 1.17.0 (5th January 2023) diff --git a/structurizr-core/src/com/structurizr/view/Color.java b/structurizr-core/src/com/structurizr/view/Color.java index c224d3f40..414660fd2 100644 --- a/structurizr-core/src/com/structurizr/view/Color.java +++ b/structurizr-core/src/com/structurizr/view/Color.java @@ -1,9 +1,169 @@ package com.structurizr.view; +import java.util.HashMap; +import java.util.Map; + public class Color { + private static final Map NAMED_COLORS = new HashMap<>(); + public static boolean isHexColorCode(String colorAsString) { return colorAsString != null && colorAsString.matches("^#[A-Fa-f0-9]{6}"); } + public static String fromColorNameToHexColorCode(String name) { + return NAMED_COLORS.getOrDefault(name, null); + } + + static { + NAMED_COLORS.put("aliceblue", "#F0F8FF"); + NAMED_COLORS.put("antiquewhite", "#FAEBD7"); + NAMED_COLORS.put("aqua", "#00FFFF"); + NAMED_COLORS.put("aquamarine", "#7FFFD4"); + NAMED_COLORS.put("azure", "#F0FFFF"); + NAMED_COLORS.put("beige", "#F5F5DC"); + NAMED_COLORS.put("bisque", "#FFE4C4"); + NAMED_COLORS.put("black", "#000000"); + NAMED_COLORS.put("blanchedalmond", "#FFEBCD"); + NAMED_COLORS.put("blue", "#0000FF"); + NAMED_COLORS.put("blueviolet", "#8A2BE2"); + NAMED_COLORS.put("brown", "#A52A2A"); + NAMED_COLORS.put("burlywood", "#DEB887"); + NAMED_COLORS.put("cadetblue", "#5F9EA0"); + NAMED_COLORS.put("chartreuse", "#7FFF00"); + NAMED_COLORS.put("chocolate", "#D2691E"); + NAMED_COLORS.put("coral", "#FF7F50"); + NAMED_COLORS.put("cornflowerblue", "#6495ED"); + NAMED_COLORS.put("cornsilk", "#FFF8DC"); + NAMED_COLORS.put("crimson", "#DC143C"); + NAMED_COLORS.put("cyan", "#00FFFF"); + NAMED_COLORS.put("darkblue", "#00008B"); + NAMED_COLORS.put("darkcyan", "#008B8B"); + NAMED_COLORS.put("darkgoldenrod", "#B8860B"); + NAMED_COLORS.put("darkgray", "#A9A9A9"); + NAMED_COLORS.put("darkgreen", "#006400"); + NAMED_COLORS.put("darkgrey", "#A9A9A9"); + NAMED_COLORS.put("darkkhaki", "#BDB76B"); + NAMED_COLORS.put("darkmagenta", "#8B008B"); + NAMED_COLORS.put("darkolivegreen", "#556B2F"); + NAMED_COLORS.put("darkorange", "#FF8C00"); + NAMED_COLORS.put("darkorchid", "#9932CC"); + NAMED_COLORS.put("darkred", "#8B0000"); + NAMED_COLORS.put("darksalmon", "#E9967A"); + NAMED_COLORS.put("darkseagreen", "#8FBC8F"); + NAMED_COLORS.put("darkslateblue", "#483D8B"); + NAMED_COLORS.put("darkslategray", "#2F4F4F"); + NAMED_COLORS.put("darkslategrey", "#2F4F4F"); + NAMED_COLORS.put("darkturquoise", "#00CED1"); + NAMED_COLORS.put("darkviolet", "#9400D3"); + NAMED_COLORS.put("deeppink", "#FF1493"); + NAMED_COLORS.put("deepskyblue", "#00BFFF"); + NAMED_COLORS.put("dimgray", "#696969"); + NAMED_COLORS.put("dimgrey", "#696969"); + NAMED_COLORS.put("dodgerblue", "#1E90FF"); + NAMED_COLORS.put("firebrick", "#B22222"); + NAMED_COLORS.put("floralwhite", "#FFFAF0"); + NAMED_COLORS.put("forestgreen", "#228B22"); + NAMED_COLORS.put("fuchsia", "#FF00FF"); + NAMED_COLORS.put("gainsboro", "#DCDCDC"); + NAMED_COLORS.put("ghostwhite", "#F8F8FF"); + NAMED_COLORS.put("gold", "#FFD700"); + NAMED_COLORS.put("goldenrod", "#DAA520"); + NAMED_COLORS.put("gray", "#808080"); + NAMED_COLORS.put("green", "#008000"); + NAMED_COLORS.put("greenyellow", "#ADFF2F"); + NAMED_COLORS.put("grey", "#808080"); + NAMED_COLORS.put("honeydew", "#F0FFF0"); + NAMED_COLORS.put("hotpink", "#FF69B4"); + NAMED_COLORS.put("indianred", "#CD5C5C"); + NAMED_COLORS.put("indigo", "#4B0082"); + NAMED_COLORS.put("ivory", "#FFFFF0"); + NAMED_COLORS.put("khaki", "#F0E68C"); + NAMED_COLORS.put("lavender", "#E6E6FA"); + NAMED_COLORS.put("lavenderblush", "#FFF0F5"); + NAMED_COLORS.put("lawngreen", "#7CFC00"); + NAMED_COLORS.put("lemonchiffon", "#FFFACD"); + NAMED_COLORS.put("lightblue", "#ADD8E6"); + NAMED_COLORS.put("lightcoral", "#F08080"); + NAMED_COLORS.put("lightcyan", "#E0FFFF"); + NAMED_COLORS.put("lightgoldenrodyellow", "#FAFAD2"); + NAMED_COLORS.put("lightgray", "#D3D3D3"); + NAMED_COLORS.put("lightgreen", "#90EE90"); + NAMED_COLORS.put("lightgrey", "#D3D3D3"); + NAMED_COLORS.put("lightpink", "#FFB6C1"); + NAMED_COLORS.put("lightsalmon", "#FFA07A"); + NAMED_COLORS.put("lightseagreen", "#20B2AA"); + NAMED_COLORS.put("lightskyblue", "#87CEFA"); + NAMED_COLORS.put("lightslategray", "#778899"); + NAMED_COLORS.put("lightslategrey", "#778899"); + NAMED_COLORS.put("lightsteelblue", "#B0C4DE"); + NAMED_COLORS.put("lightyellow", "#FFFFE0"); + NAMED_COLORS.put("lime", "#00FF00"); + NAMED_COLORS.put("limegreen", "#32CD32"); + NAMED_COLORS.put("linen", "#FAF0E6"); + NAMED_COLORS.put("magenta", "#FF00FF"); + NAMED_COLORS.put("maroon", "#800000"); + NAMED_COLORS.put("mediumaquamarine", "#66CDAA"); + NAMED_COLORS.put("mediumblue", "#0000CD"); + NAMED_COLORS.put("mediumorchid", "#BA55D3"); + NAMED_COLORS.put("mediumpurple", "#9370DB"); + NAMED_COLORS.put("mediumseagreen", "#3CB371"); + NAMED_COLORS.put("mediumslateblue", "#7B68EE"); + NAMED_COLORS.put("mediumspringgreen", "#00FA9A"); + NAMED_COLORS.put("mediumturquoise", "#48D1CC"); + NAMED_COLORS.put("mediumvioletred", "#C71585"); + NAMED_COLORS.put("midnightblue", "#191970"); + NAMED_COLORS.put("mintcream", "#F5FFFA"); + NAMED_COLORS.put("mistyrose", "#FFE4E1"); + NAMED_COLORS.put("moccasin", "#FFE4B5"); + NAMED_COLORS.put("navajowhite", "#FFDEAD"); + NAMED_COLORS.put("navy", "#000080"); + NAMED_COLORS.put("oldlace", "#FDF5E6"); + NAMED_COLORS.put("olive", "#808000"); + NAMED_COLORS.put("olivedrab", "#6B8E23"); + NAMED_COLORS.put("orange", "#FFA500"); + NAMED_COLORS.put("orangered", "#FF4500"); + NAMED_COLORS.put("orchid", "#DA70D6"); + NAMED_COLORS.put("palegoldenrod", "#EEE8AA"); + NAMED_COLORS.put("palegreen", "#98FB98"); + NAMED_COLORS.put("paleturquoise", "#AFEEEE"); + NAMED_COLORS.put("palevioletred", "#DB7093"); + NAMED_COLORS.put("papayawhip", "#FFEFD5"); + NAMED_COLORS.put("peachpuff", "#FFDAB9"); + NAMED_COLORS.put("peru", "#CD853F"); + NAMED_COLORS.put("pink", "#FFC0CB"); + NAMED_COLORS.put("plum", "#DDA0DD"); + NAMED_COLORS.put("powderblue", "#B0E0E6"); + NAMED_COLORS.put("purple", "#800080"); + NAMED_COLORS.put("rebeccapurple", "#663399"); + NAMED_COLORS.put("red", "#FF0000"); + NAMED_COLORS.put("rosybrown", "#BC8F8F"); + NAMED_COLORS.put("royalblue", "#4169E1"); + NAMED_COLORS.put("saddlebrown", "#8B4513"); + NAMED_COLORS.put("salmon", "#FA8072"); + NAMED_COLORS.put("sandybrown", "#F4A460"); + NAMED_COLORS.put("seagreen", "#2E8B57"); + NAMED_COLORS.put("seashell", "#FFF5EE"); + NAMED_COLORS.put("sienna", "#A0522D"); + NAMED_COLORS.put("silver", "#C0C0C0"); + NAMED_COLORS.put("skyblue", "#87CEEB"); + NAMED_COLORS.put("slateblue", "#6A5ACD"); + NAMED_COLORS.put("slategray", "#708090"); + NAMED_COLORS.put("slategrey", "#708090"); + NAMED_COLORS.put("snow", "#FFFAFA"); + NAMED_COLORS.put("springgreen", "#00FF7F"); + NAMED_COLORS.put("steelblue", "#4682B4"); + NAMED_COLORS.put("tan", "#D2B48C"); + NAMED_COLORS.put("teal", "#008080"); + NAMED_COLORS.put("thistle", "#D8BFD8"); + NAMED_COLORS.put("tomato", "#FF6347"); + NAMED_COLORS.put("turquoise", "#40E0D0"); + NAMED_COLORS.put("violet", "#EE82EE"); + NAMED_COLORS.put("wheat", "#F5DEB3"); + NAMED_COLORS.put("white", "#FFFFFF"); + NAMED_COLORS.put("whitesmoke", "#F5F5F5"); + NAMED_COLORS.put("yellow", "#FFFF00"); + NAMED_COLORS.put("yellowgreen", "#9ACD32"); + } + } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/ElementStyle.java b/structurizr-core/src/com/structurizr/view/ElementStyle.java index bbe4b6438..70299a5fe 100644 --- a/structurizr-core/src/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/com/structurizr/view/ElementStyle.java @@ -132,11 +132,17 @@ public String getBackground() { return background; } - public void setBackground(String background) { - if (Color.isHexColorCode(background)) { - this.background = background.toLowerCase(); + public void setBackground(String color) { + if (Color.isHexColorCode(color)) { + this.background = color.toLowerCase(); } else { - throw new IllegalArgumentException(background + " is not a valid hex colour code."); + String hexColorCode = Color.fromColorNameToHexColorCode(color); + + if (hexColorCode != null) { + this.background = hexColorCode.toLowerCase(); + } else { + throw new IllegalArgumentException(color + " is not a valid hex colour code or HTML colour name."); + } } } @@ -158,7 +164,13 @@ public void setStroke(String color) { if (Color.isHexColorCode(color)) { this.stroke = color.toLowerCase(); } else { - throw new IllegalArgumentException(color + " is not a valid hex colour code."); + String hexColorCode = Color.fromColorNameToHexColorCode(color); + + if (hexColorCode != null) { + this.stroke = hexColorCode.toLowerCase(); + } else { + throw new IllegalArgumentException(color + " is not a valid hex colour code or HTML colour name."); + } } } @@ -204,7 +216,13 @@ public void setColor(String color) { if (Color.isHexColorCode(color)) { this.color = color.toLowerCase(); } else { - throw new IllegalArgumentException(color + " is not a valid hex colour code."); + String hexColorCode = Color.fromColorNameToHexColorCode(color); + + if (hexColorCode != null) { + this.color = hexColorCode.toLowerCase(); + } else { + throw new IllegalArgumentException(color + " is not a valid hex colour code or HTML colour name."); + } } } diff --git a/structurizr-core/src/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/com/structurizr/view/RelationshipStyle.java index bea120e0a..d32e6ee30 100644 --- a/structurizr-core/src/com/structurizr/view/RelationshipStyle.java +++ b/structurizr-core/src/com/structurizr/view/RelationshipStyle.java @@ -83,7 +83,13 @@ public void setColor(String color) { if (Color.isHexColorCode(color)) { this.color = color.toLowerCase(); } else { - throw new IllegalArgumentException(color + " is not a valid hex colour code."); + String hexColorCode = Color.fromColorNameToHexColorCode(color); + + if (hexColorCode != null) { + this.color = hexColorCode.toLowerCase(); + } else { + throw new IllegalArgumentException(color + " is not a valid hex colour code or HTML colour name."); + } } } diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java index ad295c6db..4811d9643 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java @@ -2,8 +2,7 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class ColorTests { @@ -31,4 +30,14 @@ void isHexColorCode_ReturnsTrue_WhenPassedAnValidString() { assertTrue(Color.isHexColorCode("#123456")); } + @Test + void fromColorNameToHexColorCode_ReturnsNull_WhenPassedAnInvalidName() { + assertNull(Color.fromColorNameToHexColorCode("hello world")); + } + + @Test + void fromColorNameToHexColorCode_ReturnsAHexColorCode_WhenPassedAnValidName() { + assertEquals("#FFFF00", Color.fromColorNameToHexColorCode("yellow")); + } + } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java index 83b6768ec..c26c44509 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java @@ -77,11 +77,25 @@ void color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { assertEquals("#123456", style.getColor()); } + @Test + void setColor_SetsTheColorProperty_WhenAValidColorNameIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setColor("yellow"); + assertEquals("#ffff00", style.getColor()); + } + + @Test + void color_SetsTheColorProperty_WhenAValidColorNameIsSpecified() { + ElementStyle style = new ElementStyle(); + style.color("yellow"); + assertEquals("#ffff00", style.getColor()); + } + @Test void setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); - style.setColor("white"); + style.setColor("hello"); }); } @@ -89,7 +103,7 @@ void setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { void color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); - style.color("white"); + style.color("hello"); }); } @@ -119,11 +133,25 @@ void background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { assertEquals("#123456", style.getBackground()); } + @Test + void setBackground_SetsTheBackgroundProperty_WhenAValidColorNameIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setBackground("yellow"); + assertEquals("#ffff00", style.getBackground()); + } + + @Test + void background_SetsTheBackgroundProperty_WhenAValidColorNameIsSpecified() { + ElementStyle style = new ElementStyle(); + style.background("yellow"); + assertEquals("#ffff00", style.getBackground()); + } + @Test void setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); - style.setBackground("white"); + style.setBackground("hello"); }); } @@ -131,7 +159,7 @@ void setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { void background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); - style.background("white"); + style.background("hello"); }); } @@ -204,11 +232,25 @@ void Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { assertEquals("#123456", style.getStroke()); } + @Test + void setStroke_SetsTheStrokeProperty_WhenAValidColorNameIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setStroke("yellow"); + assertEquals("#ffff00", style.getStroke()); + } + + @Test + void Stroke_SetsTheStrokeProperty_WhenAValidColorNameIsSpecified() { + ElementStyle style = new ElementStyle(); + style.stroke("yellow"); + assertEquals("#ffff00", style.getStroke()); + } + @Test void setStroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); - style.setStroke("white"); + style.setStroke("hello"); }); } @@ -216,7 +258,7 @@ void setStroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { void Stroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { ElementStyle style = new ElementStyle(); - style.stroke("white"); + style.stroke("hello"); }); } diff --git a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java index 46e559ad3..e34cff717 100644 --- a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java @@ -117,18 +117,32 @@ void color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { } @Test - void setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void setColor_SetsTheColorProperty_WhenAValidColorNameIsSpecified() { + RelationshipStyle style = new RelationshipStyle(); + style.setColor("yellow"); + assertEquals("#ffff00", style.getColor()); + } + + @Test + void color_SetsTheColorProperty_WhenAValidColorNameIsSpecified() { + RelationshipStyle style = new RelationshipStyle(); + style.color("yellow"); + assertEquals("#ffff00", style.getColor()); + } + + @Test + void setColor_ThrowsAnException_WhenAnInvalidColorIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { RelationshipStyle style = new RelationshipStyle(); - style.setColor("white"); + style.setColor("hello"); }); } @Test - void color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + void color_ThrowsAnException_WhenAnInvalidColorIsSpecified() { assertThrows(IllegalArgumentException.class, () -> { RelationshipStyle style = new RelationshipStyle(); - style.color("white"); + style.color("hello"); }); } From b259f166743e2282f0f7d044f53696d5e4d6982c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 15 Jan 2023 12:32:43 +0000 Subject: [PATCH 058/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 91eecc57c..87fbd39b0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.18.0 (unreleased) +## 1.18.0 (15th January 2023) - Fixes #191 (Layout of relationships is reset when changing the description). - Adds support for using (CSS/HTML) named colors instead of hex color codes (#192). From d466ec3e615787cac7d3385b2080b9c944456a8d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 22 Jan 2023 11:57:40 +0000 Subject: [PATCH 059/418] Fixes #192 (Named colours are case-sensitive). --- docs/changelog.md | 4 ++++ structurizr-core/src/com/structurizr/view/Color.java | 6 +++++- .../test/unit/com/structurizr/view/ColorTests.java | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 87fbd39b0..90e0d4913 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.19.0 (unreleased + +- Fixes #192 (Named colours are case-sensitive). + ## 1.18.0 (15th January 2023) - Fixes #191 (Layout of relationships is reset when changing the description). diff --git a/structurizr-core/src/com/structurizr/view/Color.java b/structurizr-core/src/com/structurizr/view/Color.java index 414660fd2..c876ca294 100644 --- a/structurizr-core/src/com/structurizr/view/Color.java +++ b/structurizr-core/src/com/structurizr/view/Color.java @@ -12,7 +12,11 @@ public static boolean isHexColorCode(String colorAsString) { } public static String fromColorNameToHexColorCode(String name) { - return NAMED_COLORS.getOrDefault(name, null); + if (name != null) { + return NAMED_COLORS.getOrDefault(name.toLowerCase(), null); + } else { + return null; + } } static { diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java index 4811d9643..660cfd40e 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java @@ -36,8 +36,13 @@ void fromColorNameToHexColorCode_ReturnsNull_WhenPassedAnInvalidName() { } @Test - void fromColorNameToHexColorCode_ReturnsAHexColorCode_WhenPassedAnValidName() { + void fromColorNameToHexColorCode_ReturnsAHexColorCode_WhenPassedAValidName() { assertEquals("#FFFF00", Color.fromColorNameToHexColorCode("yellow")); } + @Test + void fromColorNameToHexColorCode_ReturnsAHexColorCode_WhenPassedAValidNameInMixedCase() { + assertEquals("#FFFF00", Color.fromColorNameToHexColorCode("Yellow")); + } + } \ No newline at end of file From 77b1101a808ade3abd3a444ce539f0cbbd0f1005 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 22 Jan 2023 12:01:32 +0000 Subject: [PATCH 060/418] Fixes #196 (Named colours are case-sensitive). --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 90e0d4913..0d4a97247 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,7 @@ ## 1.19.0 (unreleased -- Fixes #192 (Named colours are case-sensitive). +- Fixes #196 (Named colours are case-sensitive). ## 1.18.0 (15th January 2023) From ab8606d44c739a07aec1eb11d52eb7a83309d6a3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 26 Jan 2023 16:02:31 +0000 Subject: [PATCH 061/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 0d4a97247..9969299c0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.19.0 (unreleased +## 1.19.0 (26th January 2023) - Fixes #196 (Named colours are case-sensitive). From 3e6123959996fe2f17f3eae6012e60ffcd939791 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 26 Jan 2023 16:03:15 +0000 Subject: [PATCH 062/418] Updated to reflect release. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a5eaf58a8..8323b965f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.18.0' + version = '1.19.0' repositories { mavenCentral() From 1804c56425922d82c621078141a53296d4fad754 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 27 Jan 2023 11:22:20 +0000 Subject: [PATCH 063/418] Fixes #186. --- structurizr-core/src/com/structurizr/model/Location.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/model/Location.java b/structurizr-core/src/com/structurizr/model/Location.java index 524bb405c..e216c7f14 100644 --- a/structurizr-core/src/com/structurizr/model/Location.java +++ b/structurizr-core/src/com/structurizr/model/Location.java @@ -2,7 +2,11 @@ /** * Represents the location of an element with regards to a specific viewpoint. - * For example, "our customers are external to our enterprise". + * + * Diagram renderers may use this information in a different way, but generally it will be used to mark + * an element as being outside of the enterprise boundary. For example, "our customers are external to our enterprise": + * - https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/BigBankPlc.java#L36 + * - https://structurizr.com/share/28201/diagrams#SystemLandscape */ public enum Location { From 527275ed4b3b80f7fe0c1318aaf09c1814fa9cff Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 27 Jan 2023 11:25:08 +0000 Subject: [PATCH 064/418] Fixes #185. --- structurizr-core/src/com/structurizr/model/Model.java | 1 + 1 file changed, 1 insertion(+) diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/com/structurizr/model/Model.java index 7c1e4b0c0..d5c06d5ce 100644 --- a/structurizr-core/src/com/structurizr/model/Model.java +++ b/structurizr-core/src/com/structurizr/model/Model.java @@ -41,6 +41,7 @@ public Enterprise getEnterprise() { /** * Sets the enterprise associated with this model. + * This is typically used in conjunction with {@link Location}. * * @param enterprise an Enterprise instance */ From fd56d92e6aec5c5c98461987d3818d2c6e6b6547 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 9 Feb 2023 10:18:26 +0000 Subject: [PATCH 065/418] `ThemeUtils.toJson()` now includes the workspace branding logo and font in the resulting theme. --- docs/changelog.md | 4 +++ .../src/com/structurizr/view/ThemeUtils.java | 17 +++++---- .../com/structurizr/view/ThemeUtilsTests.java | 9 ++++- .../src/com/structurizr/view/Theme.java | 35 +++++++++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 9969299c0..cd0fd9349 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.20.0 (unreleased) + +- `ThemeUtils.toJson()` now includes the workspace branding logo and font in the resulting theme. + ## 1.19.0 (26th January 2023) - Fixes #196 (Named colours are case-sensitive). diff --git a/structurizr-client/src/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/com/structurizr/view/ThemeUtils.java index 6f7c79c8d..22c177e62 100644 --- a/structurizr-client/src/com/structurizr/view/ThemeUtils.java +++ b/structurizr-client/src/com/structurizr/view/ThemeUtils.java @@ -111,13 +111,16 @@ private static void write(Workspace workspace, Writer writer) throws Exception { objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); - writer.write(objectMapper.writeValueAsString( - new Theme( - workspace.getName(), - workspace.getDescription(), - workspace.getViews().getConfiguration().getStyles().getElements(), - workspace.getViews().getConfiguration().getStyles().getRelationships() - ))); + Theme theme = new Theme( + workspace.getName(), + workspace.getDescription(), + workspace.getViews().getConfiguration().getStyles().getElements(), + workspace.getViews().getConfiguration().getStyles().getRelationships() + ); + theme.setFont(workspace.getViews().getConfiguration().getBranding().getFont()); + theme.setLogo(workspace.getViews().getConfiguration().getBranding().getLogo()); + + writer.write(objectMapper.writeValueAsString(theme)); } catch (IOException ioe) { throw new WorkspaceWriterException("Could not write the theme as JSON", ioe); } diff --git a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java index 2d390a47c..ae1f66800 100644 --- a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java @@ -52,6 +52,8 @@ void toJson() throws Exception { workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.ELEMENT).background("#ff0000"); workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); + workspace.getViews().getConfiguration().getBranding().setLogo("https://structurizr.com/static/img/structurizr-logo.png"); + workspace.getViews().getConfiguration().getBranding().setFont(new Font("Open Sans", "https://fonts.googleapis.com/css?family=Open+Sans:400,700")); assertEquals("{\n" + " \"name\" : \"Name\",\n" + " \"description\" : \"Description\",\n" + @@ -62,7 +64,12 @@ void toJson() throws Exception { " \"relationships\" : [ {\n" + " \"tag\" : \"Relationship\",\n" + " \"color\" : \"#ff0000\"\n" + - " } ]\n" + + " } ],\n" + + " \"logo\" : \"https://structurizr.com/static/img/structurizr-logo.png\",\n" + + " \"font\" : {\n" + + " \"name\" : \"Open Sans\",\n" + + " \"url\" : \"https://fonts.googleapis.com/css?family=Open+Sans:400,700\"\n" + + " }\n" + "}", ThemeUtils.toJson(workspace)); } diff --git a/structurizr-core/src/com/structurizr/view/Theme.java b/structurizr-core/src/com/structurizr/view/Theme.java index 07484b986..1d30657f6 100644 --- a/structurizr-core/src/com/structurizr/view/Theme.java +++ b/structurizr-core/src/com/structurizr/view/Theme.java @@ -1,6 +1,8 @@ package com.structurizr.view; import com.fasterxml.jackson.annotation.JsonGetter; +import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; import java.util.Collection; import java.util.LinkedList; @@ -11,6 +13,8 @@ final class Theme { private String description; private Collection elements = new LinkedList<>(); private Collection relationships = new LinkedList<>(); + private String logo; + private Font font; Theme() { } @@ -61,4 +65,35 @@ void setRelationships(Collection relationships) { this.relationships = relationships; } + public String getLogo() { + return logo; + } + + /** + * Sets the URL of an image representing a logo. + * + * @param logo a URL or data URI as a String + */ + public void setLogo(String logo) { + if (StringUtils.isNullOrEmpty(logo)) { + this.logo = null; + } else { + ImageUtils.validateImage(logo); + this.logo = logo.trim(); + } + } + + public Font getFont() { + return font; + } + + /** + * Sets the font to use. + * + * @param font a Font object + */ + public void setFont(Font font) { + this.font = font; + } + } \ No newline at end of file From 66b5ccd835b2d1d0200f269b174dbacc1c2d75eb Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 9 Feb 2023 13:17:28 +0000 Subject: [PATCH 066/418] Adds a "Window" shape. --- structurizr-core/src/com/structurizr/view/Shape.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/view/Shape.java b/structurizr-core/src/com/structurizr/view/Shape.java index b53c35446..33232c99f 100644 --- a/structurizr-core/src/com/structurizr/view/Shape.java +++ b/structurizr-core/src/com/structurizr/view/Shape.java @@ -14,8 +14,9 @@ public enum Shape { Robot, Folder, WebBrowser, + Window, MobileDevicePortrait, MobileDeviceLandscape, Component -} +} \ No newline at end of file From 5cb02d09013c0f561e0282848ae776dbbe319118 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 12 Feb 2023 16:31:31 +0000 Subject: [PATCH 067/418] Adds support for "image views". --- docs/changelog.md | 2 + .../src/com/structurizr/view/ImageView.java | 176 ++++++++++++++++++ .../src/com/structurizr/view/ViewSet.java | 85 ++++++++- 3 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 structurizr-core/src/com/structurizr/view/ImageView.java diff --git a/docs/changelog.md b/docs/changelog.md index cd0fd9349..d7d0c8b42 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,8 @@ ## 1.20.0 (unreleased) - `ThemeUtils.toJson()` now includes the workspace branding logo and font in the resulting theme. +- Adds a `Window` shape. +- Adds support for "image views". ## 1.19.0 (26th January 2023) diff --git a/structurizr-core/src/com/structurizr/view/ImageView.java b/structurizr-core/src/com/structurizr/view/ImageView.java new file mode 100644 index 000000000..39da20628 --- /dev/null +++ b/structurizr-core/src/com/structurizr/view/ImageView.java @@ -0,0 +1,176 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.model.Element; +import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; + +/** + * A view that has been rendered elsewhere (e.g. PlantUML, Mermaid, Kroki, etc) as a image (e.g. PNG). + */ +public final class ImageView { + + private String key; + private int order; + + private Element element; + private String elementId; + private String content; + private String contentType; + + private String title; + private String description; + + ImageView() { + } + + ImageView(String key) { + setKey(key); + } + + ImageView(Element element, String key) { + this(key); + setElement(element); + } + + /** + * Gets the ID of the element associated with this view. + * + * @return the ID, as a String, or null if not set + */ + public String getElementId() { + if (this.element != null) { + return element.getId(); + } else { + return this.elementId; + } + } + + void setElementId(String elementId) { + this.elementId = elementId; + } + + @JsonIgnore + public Element getElement() { + return element; + } + + void setElement(Element element) { + this.element = element; + } + + /** + * Gets the content of this view (a URL or a data URI). + * + * @return the content, as a String + */ + public String getContent() { + return content; + } + + /** + * Sets the content of this image view, which needs to be a URL or a data URI. + * + * @param content the content of this view + */ + public void setContent(String content) { + if (StringUtils.isNullOrEmpty(content)) { + this.content = null; + } else { + ImageUtils.validateImage(content); + this.content = content.trim(); + } + } + + /** + * Gets the the content type of this view (e.g. "image/png"). + * + * @return the content type, as a String + */ + public String getContentType() { + return contentType; + } + + /** + * Sets the content type of this view (e.g. "image/png"). + * + * @param contentType the content type, as a String + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Gets the order of this view. + * + * @return a positive integer + */ + public int getOrder() { + return order; + } + + void setOrder(int order) { + this.order = Math.max(1, order); + } + + /** + * Gets the title of this view, if one has been set. + * + * @return the title, as a String + */ + public String getTitle() { + return title; + } + + /** + * Sets the title for this view. + * + * @param title the title, as a String + */ + public void setTitle(String title) { + if (StringUtils.isNullOrEmpty(title)) { + throw new IllegalArgumentException("A title must be specified"); + } + this.title = title; + } + + /** + * Gets the description of this view. + * + * @return the description, as a String + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of this view. + * + * @param description the description, as a string + */ + public void setDescription(String description) { + if (description == null) { + this.description = ""; + } else { + this.description = description; + } + } + + /** + * Gets the identifier for this view. + * + * @return the identifier, as a String + */ + public String getKey() { + return key; + } + + void setKey(String key) { + if (key != null) { + key = key.replaceAll("/", "_"); + } + + this.key = key; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/com/structurizr/view/ViewSet.java index d68273004..b24878268 100644 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/com/structurizr/view/ViewSet.java @@ -28,6 +28,7 @@ public final class ViewSet { private Collection componentViews = new HashSet<>(); private Collection dynamicViews = new HashSet<>(); private Collection deploymentViews = new HashSet<>(); + private Collection imageViews = new HashSet<>(); private Collection filteredViews = new HashSet<>(); @@ -268,12 +269,48 @@ public FilteredView createFilteredView(StaticView view, String key, String descr return filteredView; } + /** + * Creates an image view. + * + * @param key the key for the view (must be unique) + * @return an ImageView object + * @throws IllegalArgumentException if the key is not unique + */ + public ImageView createImageView(String key) { + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + ImageView view = new ImageView(key); + view.setOrder(getNextOrder()); + imageViews.add(view); + return view; + } + + /** + * Creates an image view, where the scope is the specified element. + * + * @param element the Element object representing the scope of the view + * @param key the key for the view (must be unique) + * @return an ImageView object + * @throws IllegalArgumentException if the element is null or the key is not unique + */ + public ImageView createImageView(Element element, String key) { + if (element == null) { + throw new IllegalArgumentException("An element must be specified."); + } + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + ImageView view = new ImageView(element, key); + view.setOrder(getNextOrder()); + imageViews.add(view); + return view; + } + private void assertThatTheViewKeyIsSpecifiedAndUnique(String key) { if (StringUtils.isNullOrEmpty(key)) { throw new IllegalArgumentException("A key must be specified."); } - if (getViewWithKey(key) != null || getFilteredViewWithKey(key) != null) { + if (getViewWithKey(key) != null || getFilteredViewWithKey(key) != null || getImageViewWithKey(key) != null) { throw new IllegalArgumentException("A view with the key " + key + " already exists."); } } @@ -333,6 +370,20 @@ FilteredView getFilteredViewWithKey(String key) { return filteredViews.stream().filter(v -> key.equals(v.getKey())).findFirst().orElse(null); } + /** + * Finds the image view with the specified key, or null if the view does not exist. + * + * @param key the key + * @return a ImageView object, or null if a view with the specified key could not be found + */ + ImageView getImageViewWithKey(String key) { + if (key == null) { + throw new IllegalArgumentException("A key must be specified."); + } + + return imageViews.stream().filter(v -> key.equals(v.getKey())).findFirst().orElse(null); + } + /** * Gets the set of custom views. * @@ -459,7 +510,22 @@ void setDeploymentViews(Set deploymentViews) { } /** - * Gets the set of all views (except filtered views). + * Gets the set of image views. + * + * @return a Collection of ImageView objects + */ + public Collection getImageViews() { + return new HashSet<>(imageViews); + } + + void setImageView(Set imageViews) { + if (imageViews != null) { + this.imageViews = new HashSet<>(imageViews); + } + } + + /** + * Gets the set of all views (except filtered and image views). * * @return a Collection of View objects */ @@ -577,6 +643,20 @@ void hydrate(Model model) { filteredView.setView(view); } + + for (ImageView view : imageViews) { + if (!isNullOrEmpty(view.getElementId())) { + Element element = model.getElement(view.getElementId()); + if (element == null) { + throw new WorkspaceValidationException( + String.format("The image view with key \"%s\" is associated with an element (id=%s), but that element does not exist in the model.", + view.getKey(), view.getElementId()) + ); + } + + view.setElement(element); + } + } } private void hydrateView(View view) { @@ -646,6 +726,7 @@ private synchronized int getNextOrder() { order = Math.max(order, dynamicViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); order = Math.max(order, deploymentViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); order = Math.max(order, filteredViews.stream().max(Comparator.comparingInt(FilteredView::getOrder)).map(FilteredView::getOrder).orElse(0)); + order = Math.max(order, imageViews.stream().max(Comparator.comparingInt(ImageView::getOrder)).map(ImageView::getOrder).orElse(0)); return order + 1; } From 0ffd2f9083a319931399eef2a7fcc1c4ef998c14 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 12 Feb 2023 16:31:40 +0000 Subject: [PATCH 068/418] Typo. --- structurizr-core/src/com/structurizr/view/ViewSet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/com/structurizr/view/ViewSet.java index b24878268..4234e7afa 100644 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/com/structurizr/view/ViewSet.java @@ -495,9 +495,9 @@ void setFilteredViews(Set filteredViews) { } /** - * Gets the set of dynamic views. + * Gets the set of deployment views. * - * @return a Collection of DynamicView objects + * @return a Collection of DeploymentView objects */ public Collection getDeploymentViews() { return new HashSet<>(deploymentViews); From 44e15aaceb328fb19363253dbdcb6bd888dd4016 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 13 Feb 2023 08:40:49 +0000 Subject: [PATCH 069/418] Refactoring so that all views extend the View class. --- docs/changelog.md | 7 +- .../src/com/structurizr/view/CustomView.java | 2 +- .../view/DefaultLayoutMergeStrategy.java | 12 +- .../com/structurizr/view/DeploymentView.java | 2 +- .../src/com/structurizr/view/DynamicView.java | 2 +- .../com/structurizr/view/FilteredView.java | 48 +- .../src/com/structurizr/view/ImageView.java | 82 +-- .../structurizr/view/LayoutMergeStrategy.java | 2 +- .../src/com/structurizr/view/ModelView.java | 475 ++++++++++++++++++ .../src/com/structurizr/view/StaticView.java | 2 +- .../src/com/structurizr/view/View.java | 440 +--------------- .../src/com/structurizr/view/ViewSet.java | 49 +- 12 files changed, 531 insertions(+), 592 deletions(-) create mode 100644 structurizr-core/src/com/structurizr/view/ModelView.java diff --git a/docs/changelog.md b/docs/changelog.md index d7d0c8b42..6b8866b18 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,9 +2,10 @@ ## 1.20.0 (unreleased) -- `ThemeUtils.toJson()` now includes the workspace branding logo and font in the resulting theme. -- Adds a `Window` shape. -- Adds support for "image views". +- __Breaking change__: Renamed `View` to `ModelView`. +- Added support for "image views". +- Added a `Window` shape. +- `ThemeUtils.toJson()` now includes the workspace branding logo and font in the resulting theme. ## 1.19.0 (26th January 2023) diff --git a/structurizr-core/src/com/structurizr/view/CustomView.java b/structurizr-core/src/com/structurizr/view/CustomView.java index 8a99a6017..ebe20cfa2 100644 --- a/structurizr-core/src/com/structurizr/view/CustomView.java +++ b/structurizr-core/src/com/structurizr/view/CustomView.java @@ -15,7 +15,7 @@ /** * Represents a custom view, containing custom elements. */ -public final class CustomView extends View { +public final class CustomView extends ModelView { private List animations = new ArrayList<>(); diff --git a/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java b/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java index 6961e1186..e4faeb92c 100644 --- a/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java +++ b/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java @@ -33,7 +33,7 @@ public class DefaultLayoutMergeStrategy implements LayoutMergeStrategy { * @param viewWithLayoutInformation the source view (e.g. the version stored by the Structurizr service) * @param viewWithoutLayoutInformation the destination View (e.g. the new version, created locally with code) */ - public void copyLayoutInformation(@Nonnull View viewWithLayoutInformation, @Nonnull View viewWithoutLayoutInformation) { + public void copyLayoutInformation(@Nonnull ModelView viewWithLayoutInformation, @Nonnull ModelView viewWithoutLayoutInformation) { setPaperSizeIfNotSpecified(viewWithLayoutInformation, viewWithoutLayoutInformation); setDimensionsIfNotSpecified(viewWithLayoutInformation, viewWithoutLayoutInformation); @@ -69,13 +69,13 @@ public void copyLayoutInformation(@Nonnull View viewWithLayoutInformation, @Nonn } } - private void setPaperSizeIfNotSpecified(@Nonnull View remoteView, @Nonnull View localView) { + private void setPaperSizeIfNotSpecified(@Nonnull ModelView remoteView, @Nonnull ModelView localView) { if (localView.getPaperSize() == null) { localView.setPaperSize(remoteView.getPaperSize()); } } - private void setDimensionsIfNotSpecified(@Nonnull View remoteView, @Nonnull View localView) { + private void setDimensionsIfNotSpecified(@Nonnull ModelView remoteView, @Nonnull ModelView localView) { if (localView.getDimensions() == null) { localView.setDimensions(remoteView.getDimensions()); } @@ -88,7 +88,7 @@ private void setDimensionsIfNotSpecified(@Nonnull View remoteView, @Nonnull View * @param elementWithoutLayoutInformation the Element to find * @return an ElementView */ - protected ElementView findElementView(View viewWithLayoutInformation, Element elementWithoutLayoutInformation) { + protected ElementView findElementView(ModelView viewWithLayoutInformation, Element elementWithoutLayoutInformation) { // see if we can find an element with the same canonical name in the source view ElementView elementView = viewWithLayoutInformation.getElements().stream().filter(ev -> ev.getElement().getCanonicalName().equals(elementWithoutLayoutInformation.getCanonicalName())).findFirst().orElse(null); @@ -112,7 +112,7 @@ protected ElementView findElementView(View viewWithLayoutInformation, Element el return elementView; } - private RelationshipView findRelationshipView(View viewWithLayoutInformation, Relationship relationshipWithoutLayoutInformation, Map elementMap) { + private RelationshipView findRelationshipView(ModelView viewWithLayoutInformation, Relationship relationshipWithoutLayoutInformation, Map elementMap) { if (!elementMap.containsKey(relationshipWithoutLayoutInformation.getSource()) || !elementMap.containsKey(relationshipWithoutLayoutInformation.getDestination())) { return null; } @@ -144,7 +144,7 @@ private RelationshipView findRelationshipView(View viewWithLayoutInformation, Re return null; } - private RelationshipView findRelationshipView(View view, RelationshipView relationshipWithoutLayoutInformation, Map elementMap) { + private RelationshipView findRelationshipView(ModelView view, RelationshipView relationshipWithoutLayoutInformation, Map elementMap) { if (!elementMap.containsKey(relationshipWithoutLayoutInformation.getRelationship().getSource()) || !elementMap.containsKey(relationshipWithoutLayoutInformation.getRelationship().getDestination())) { return null; } diff --git a/structurizr-core/src/com/structurizr/view/DeploymentView.java b/structurizr-core/src/com/structurizr/view/DeploymentView.java index 02580f0ec..f58ae73cb 100644 --- a/structurizr-core/src/com/structurizr/view/DeploymentView.java +++ b/structurizr-core/src/com/structurizr/view/DeploymentView.java @@ -11,7 +11,7 @@ /** * A deployment view, used to show the mapping of container instances to deployment nodes. */ -public final class DeploymentView extends View { +public final class DeploymentView extends ModelView { private Model model; private String environment = DeploymentElement.DEFAULT_DEPLOYMENT_ENVIRONMENT; diff --git a/structurizr-core/src/com/structurizr/view/DynamicView.java b/structurizr-core/src/com/structurizr/view/DynamicView.java index 14bec348b..6dbb48908 100644 --- a/structurizr-core/src/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/com/structurizr/view/DynamicView.java @@ -10,7 +10,7 @@ /** * A dynamic view, used to describe behaviour between static elements at runtime. */ -public final class DynamicView extends View { +public final class DynamicView extends ModelView { private Model model; diff --git a/structurizr-core/src/com/structurizr/view/FilteredView.java b/structurizr-core/src/com/structurizr/view/FilteredView.java index 2a9b9a50c..64b70a507 100644 --- a/structurizr-core/src/com/structurizr/view/FilteredView.java +++ b/structurizr-core/src/com/structurizr/view/FilteredView.java @@ -9,15 +9,11 @@ /** * Represents a view on top of a view, which can be used to include or exclude specific elements. */ -public final class FilteredView { +public final class FilteredView extends View { - private View view; + private StaticView view; private String baseViewKey; - private String key; - private int order; - private String description = ""; - private FilterMode mode = FilterMode.Exclude; private Set tags = new HashSet<>(); @@ -26,8 +22,8 @@ public final class FilteredView { FilteredView(StaticView view, String key, String description, FilterMode mode, String... tags) { this.view = view; - this.key = key; - this.description = description; + setKey(key); + setDescription(description); this.mode = mode; this.tags.addAll(Arrays.asList(tags)); } @@ -37,7 +33,7 @@ public View getView() { return view; } - void setView(View view) { + void setView(StaticView view) { this.view = view; } @@ -53,35 +49,6 @@ void setBaseViewKey(String baseViewKey) { this.baseViewKey = baseViewKey; } - public String getKey() { - return key; - } - - void setKey(String key) { - this.key = key; - } - - /** - * Gets the order of this view. - * - * @return a positive integer - */ - public int getOrder() { - return order; - } - - void setOrder(int order) { - this.order = Math.max(1, order); - } - - public String getDescription() { - return description; - } - - void setDescription(String description) { - this.description = description; - } - public FilterMode getMode() { return mode; } @@ -94,4 +61,9 @@ public Set getTags() { return new HashSet<>(tags); } + @Override + public String getName() { + return "Filtered: " + view.getName(); + } + } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/ImageView.java b/structurizr-core/src/com/structurizr/view/ImageView.java index 39da20628..861ce76c9 100644 --- a/structurizr-core/src/com/structurizr/view/ImageView.java +++ b/structurizr-core/src/com/structurizr/view/ImageView.java @@ -8,19 +8,13 @@ /** * A view that has been rendered elsewhere (e.g. PlantUML, Mermaid, Kroki, etc) as a image (e.g. PNG). */ -public final class ImageView { - - private String key; - private int order; +public final class ImageView extends View { private Element element; private String elementId; private String content; private String contentType; - private String title; - private String description; - ImageView() { } @@ -100,77 +94,9 @@ public void setContentType(String contentType) { this.contentType = contentType; } - /** - * Gets the order of this view. - * - * @return a positive integer - */ - public int getOrder() { - return order; - } - - void setOrder(int order) { - this.order = Math.max(1, order); - } - - /** - * Gets the title of this view, if one has been set. - * - * @return the title, as a String - */ - public String getTitle() { - return title; - } - - /** - * Sets the title for this view. - * - * @param title the title, as a String - */ - public void setTitle(String title) { - if (StringUtils.isNullOrEmpty(title)) { - throw new IllegalArgumentException("A title must be specified"); - } - this.title = title; - } - - /** - * Gets the description of this view. - * - * @return the description, as a String - */ - public String getDescription() { - return description; - } - - /** - * Sets the description of this view. - * - * @param description the description, as a string - */ - public void setDescription(String description) { - if (description == null) { - this.description = ""; - } else { - this.description = description; - } - } - - /** - * Gets the identifier for this view. - * - * @return the identifier, as a String - */ - public String getKey() { - return key; - } - - void setKey(String key) { - if (key != null) { - key = key.replaceAll("/", "_"); - } - - this.key = key; + @Override + public String getName() { + return getTitle(); } } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/LayoutMergeStrategy.java b/structurizr-core/src/com/structurizr/view/LayoutMergeStrategy.java index 063f7bf84..ec17d9634 100644 --- a/structurizr-core/src/com/structurizr/view/LayoutMergeStrategy.java +++ b/structurizr-core/src/com/structurizr/view/LayoutMergeStrategy.java @@ -14,6 +14,6 @@ public interface LayoutMergeStrategy { * @param sourceView the source view (e.g. the version stored by the Structurizr service) * @param destinationView the destination View (e.g. the new version, created locally with code) */ - void copyLayoutInformation(@Nonnull View sourceView, @Nonnull View destinationView); + void copyLayoutInformation(@Nonnull ModelView sourceView, @Nonnull ModelView destinationView); } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/ModelView.java b/structurizr-core/src/com/structurizr/view/ModelView.java new file mode 100644 index 000000000..b10a556f7 --- /dev/null +++ b/structurizr-core/src/com/structurizr/view/ModelView.java @@ -0,0 +1,475 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; + +import javax.annotation.Nonnull; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * The superclass for all views that show elements/relationships from the model, namely: + * + * - System landscape views + * - System context views + * - Container views + * - Dynamic views + * - Deployment views + */ +public abstract class ModelView extends View { + + private static final int DEFAULT_RANK_SEPARATION = 300; + private static final int DEFAULT_NODE_SEPARATION = 300; + + private SoftwareSystem softwareSystem; + private String softwareSystemId; + private PaperSize paperSize = null; + private Dimensions dimensions = null; + private AutomaticLayout automaticLayout = null; + private boolean mergeFromRemote = true; + + private Set elementViews = new LinkedHashSet<>(); + private Set relationshipViews = new LinkedHashSet<>(); + + private LayoutMergeStrategy layoutMergeStrategy = new DefaultLayoutMergeStrategy(); + + private ViewSet viewSet; + + ModelView() { + } + + ModelView(SoftwareSystem softwareSystem, String key, String description) { + this.softwareSystem = softwareSystem; + if (!StringUtils.isNullOrEmpty(key)) { + setKey(key); + } else { + throw new IllegalArgumentException("A key must be specified."); + } + setDescription(description); + } + + /** + * Gets the model that this view belongs to. + * + * @return a Model object + */ + @JsonIgnore + public Model getModel() { + return softwareSystem.getModel(); + } + + /** + * Gets the software system that this view is associated with. + * + * @return a SoftwareSystem object, or null if this view is not associated with a software system (e.g. it's a system landscape view) + */ + @JsonIgnore + public SoftwareSystem getSoftwareSystem() { + return softwareSystem; + } + + void setSoftwareSystem(SoftwareSystem softwareSystem) { + this.softwareSystem = softwareSystem; + } + + /** + * Gets the ID of the software system this view is associated with. + * + * @return the ID, as a String, or null if this view is not associated with a software system (e.g. it's a system landscape view) + */ + public String getSoftwareSystemId() { + if (this.softwareSystem != null) { + return this.softwareSystem.getId(); + } else { + return this.softwareSystemId; + } + } + + void setSoftwareSystemId(String softwareSystemId) { + this.softwareSystemId = softwareSystemId; + } + + /** + * Gets the paper size that should be used to render this view. + * + * @return a PaperSize + */ + public PaperSize getPaperSize() { + return paperSize; + } + + public void setPaperSize(PaperSize paperSize) { + this.paperSize = paperSize; + } + + public Dimensions getDimensions() { + return dimensions; + } + + public void setDimensions(Dimensions dimensions) { + this.dimensions = dimensions; + } + + /** + * Gets the automatic layout settings for this view. + * + * @return an AutomaticLayout object, or null if not enabled + */ + public AutomaticLayout getAutomaticLayout() { + return automaticLayout; + } + + @JsonSetter + void setAutomaticLayout(AutomaticLayout automaticLayout) { + this.automaticLayout = automaticLayout; + } + + /** + * Enables automatic layout for this view, with some default settings. + */ + public void enableAutomaticLayout() { + enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 600, 200, false); + } + + /** + * Enables automatic layout for this view, with the specified settings, using the Dagre implementation. + * + * @param rankDirection the rank direction + * @param rankSeparation the separation between ranks (in pixels, a positive integer) + * @param nodeSeparation the separation between nodes within the same rank (in pixels, a positive integer) + * @param edgeSeparation the separation between edges (in pixels, a positive integer) + * @param vertices whether vertices should be created during automatic layout + */ + public void enableAutomaticLayout(AutomaticLayout.RankDirection rankDirection, int rankSeparation, int nodeSeparation, int edgeSeparation, boolean vertices) { + this.automaticLayout = new AutomaticLayout(AutomaticLayout.Implementation.Dagre, rankDirection, rankSeparation, nodeSeparation, edgeSeparation, vertices); + } + + /** + * Enables automatic layout for this view, with the specified direction, using the Graphviz implementation. + * + * @param rankDirection the rank direction + */ + public void enableAutomaticLayout(AutomaticLayout.RankDirection rankDirection) { + enableAutomaticLayout(rankDirection, DEFAULT_RANK_SEPARATION, DEFAULT_NODE_SEPARATION); + } + + /** + * Enables automatic layout for this view, with the specified settings, using the Graphviz implementation. + * + * @param rankDirection the rank direction + * @param rankSeparation the separation between ranks (in pixels, a positive integer) + * @param nodeSeparation the separation between nodes within the same rank (in pixels, a positive integer) + */ + public void enableAutomaticLayout(AutomaticLayout.RankDirection rankDirection, int rankSeparation, int nodeSeparation) { + this.automaticLayout = new AutomaticLayout(AutomaticLayout.Implementation.Graphviz, rankDirection, rankSeparation, nodeSeparation, 0, false); + } + + /** + * Disables automatic layout for this view. + */ + public void disableAutomaticLayout() { + this.automaticLayout = null; + } + + /** + * Gets whether layout information for this view should be merged from a remote version of the workspace. + * + * @return true if layout information should be merged from the remote workspace, false otherwise + */ + public boolean getMergeFromRemote() { + return mergeFromRemote; + } + + /** + * Specifies whether layout information for this view should be merged from a remote version of the workspace. + * + * @param mergeFromRemote true if layout information should be merged from the remote workspace, false otherwise + */ + @JsonIgnore + public void setMergeFromRemote(boolean mergeFromRemote) { + this.mergeFromRemote = mergeFromRemote; + } + + protected final void addElement(Element element, boolean addRelationships) { + if (element == null) { + throw new IllegalArgumentException("An element must be specified."); + } + + if (getModel().contains(element)) { + checkElementCanBeAdded(element); + elementViews.add(new ElementView(element)); + + if (addRelationships) { + addRelationships(element); + } + } else { + throw new IllegalArgumentException("The element named " + element.getName() + " does not exist in the model associated with this view."); + } + } + + protected abstract void checkElementCanBeAdded(Element element); + + private void addRelationships(Element element) { + Set elements = getElements().stream() + .map(ElementView::getElement) + .collect(Collectors.toSet()); + + // add relationships where the destination exists in the view already + for (Relationship relationship : element.getRelationships()) { + if (elements.contains(relationship.getDestination())) { + this.relationshipViews.add(new RelationshipView(relationship)); + } + } + + // add relationships where the source exists in the view already + for (Element e : elements) { + for (Relationship r : e.getRelationships()) { + if (r.getDestination().equals(element)) { + this.relationshipViews.add(new RelationshipView(r)); + } + } + } + } + + protected void removeElement(Element element) { + if (element == null) { + throw new IllegalArgumentException("An element must be specified."); + } + + if (!canBeRemoved(element)) { + throw new IllegalArgumentException("The element named '" + element.getName() + "' cannot be removed from this view."); + } + + ElementView elementView = new ElementView(element); + elementViews.remove(elementView); + + for (RelationshipView relationshipView : getRelationships()) { + if (relationshipView.getRelationship().getSource().equals(element) || + relationshipView.getRelationship().getDestination().equals(element)) { + remove(relationshipView.getRelationship()); + } + } + } + + protected RelationshipView addRelationship(Relationship relationship) { + if (relationship == null) { + throw new IllegalArgumentException("A relationship must be specified."); + } + + if (isElementInView(relationship.getSource()) && isElementInView(relationship.getDestination())) { + RelationshipView relationshipView = new RelationshipView(relationship); + relationshipViews.add(relationshipView); + + return relationshipView; + } + + return null; + } + + public boolean isElementInView(Element element) { + return this.elementViews.stream().anyMatch(ev -> ev.getElement().equals(element)); + } + + /** + * Removes a relationship from this view. + * + * @param relationship the Relationship to remove + */ + public void remove(Relationship relationship) { + if (relationship != null) { + RelationshipView relationshipView = new RelationshipView(relationship); + relationshipViews.remove(relationshipView); + } + } + + /** + * Removes relationships that are not connected to the specified element. + * + * @param element the Element to test against + */ + public void removeRelationshipsNotConnectedToElement(Element element) { + if (element != null) { + getRelationships().stream() + .map(RelationshipView::getRelationship) + .filter(r -> !r.getSource().equals(element) && !r.getDestination().equals(element)) + .forEach(this::remove); + } + } + + /** + * Gets the set of elements in this view. + * + * @return a Set of ElementView objects + */ + public Set getElements() { + return new HashSet<>(elementViews); + } + + void setElements(Set elementViews) { + if (elementViews != null) { + this.elementViews = new HashSet<>(elementViews); + } + } + + /** + * Gets the set of relationships in this view. + * + * @return a Set of RelationshipView objects + */ + public Set getRelationships() { + return new HashSet<>(this.relationshipViews); + } + + void setRelationships(Set relationshipViews) { + if (relationshipViews != null) { + this.relationshipViews = new HashSet<>(relationshipViews); + } + } + + /** + * Removes all elements that have no relationships to other elements in this view. + */ + public void removeElementsWithNoRelationships() { + Set relationships = getRelationships(); + + Set elementIds = new HashSet<>(); + relationships.forEach(rv -> elementIds.add(rv.getRelationship().getSourceId())); + relationships.forEach(rv -> elementIds.add(rv.getRelationship().getDestinationId())); + + for (ElementView elementView : getElements()) { + if (!elementIds.contains(elementView.getId())) { + removeElement(elementView.getElement()); + } + } + } + + /** + * Sets the strategy used for merging layout information (paper size, x/y positioning, etc) + * from one version of this view to another. + * + * @param layoutMergeStrategy an instance of LayoutMergeStrategy + */ + public void setLayoutMergeStrategy(LayoutMergeStrategy layoutMergeStrategy) { + if (layoutMergeStrategy == null) { + throw new IllegalArgumentException("A LayoutMergeStrategy object must be provided."); + } + + this.layoutMergeStrategy = layoutMergeStrategy; + } + + /** + * Attempts to copy the visual layout information (e.g. x,y coordinates) of elements and relationships + * from the specified source view into this view. + * + * @param source the source View + */ + void copyLayoutInformationFrom(@Nonnull ModelView source) { + layoutMergeStrategy.copyLayoutInformation(source, this); + } + + /** + * Gets the element view for the given element. + * + * @param element the Element to find the ElementView for + * @return an ElementView object, or null if the element doesn't exist in the view + */ + public ElementView getElementView(@Nonnull Element element) { + Optional elementView = this.elementViews.stream().filter(ev -> ev.getId().equals(element.getId())).findFirst(); + return elementView.orElse(null); + } + + /** + * Gets the relationship view for the given relationship. + * + * @param relationship the Relationship to find the RelationshipView for + * @return a RelationshipView object, or null if the relationship doesn't exist in the view + */ + public RelationshipView getRelationshipView(@Nonnull Relationship relationship) { + Optional relationshipView = this.relationshipViews.stream().filter(rv -> rv.getId().equals(relationship.getId())).findFirst(); + return relationshipView.orElse(null); + } + + void setViewSet(@Nonnull ViewSet viewSet) { + this.viewSet = viewSet; + } + + /** + * Gets the view set that this view belongs to. + * + * @return a ViewSet object + */ + @JsonIgnore + public ViewSet getViewSet() { + return viewSet; + } + + protected abstract boolean canBeRemoved(Element element); + + final void checkParentAndChildrenHaveNotAlreadyBeenAdded(StaticStructureElement elementToBeAdded) { + // check a parent hasn't been added already + Set idsOfElementsInView = getElements().stream().map(ElementView::getElement).map(Element::getId).collect(Collectors.toSet()); + + Element parent = elementToBeAdded.getParent(); + while (parent != null) { + if (idsOfElementsInView.contains(parent.getId())) { + throw new ElementNotPermittedInViewException("A parent of " + elementToBeAdded.getName() + " is already in this view."); + } + + parent = parent.getParent(); + } + + // and now check a child hasn't been added already + Set elementParentIds = new HashSet<>(); + for (ElementView elementView : getElements()) { + Element element = elementView.getElement(); + parent = element.getParent(); + while (parent != null) { + elementParentIds.add(parent.getId()); + parent = parent.getParent(); + } + } + + if (elementParentIds.contains(elementToBeAdded.getId())) { + throw new ElementNotPermittedInViewException("A child of " + elementToBeAdded.getName() + " is already in this view."); + } + } + + protected void addNearestNeighbours(Element element, Class typeOfElement) { + if (element == null) { + return; + } + + try { + addElement(element, true); + + Set relationships = getModel().getRelationships(); + relationships.stream().filter(r -> r.getSource().equals(element) && typeOfElement.isInstance(r.getDestination())) + .map(Relationship::getDestination) + .forEach(d -> { + try { + addElement(d, true); + } catch (ElementNotPermittedInViewException e) { + System.out.println(e.getMessage() + " (ignoring " + d.getName() + ")"); + } + }); + + relationships.stream().filter(r -> r.getDestination().equals(element) && typeOfElement.isInstance(r.getSource())) + .map(Relationship::getSource) + .forEach(s -> { + try { + addElement(s, true); + } catch (ElementNotPermittedInViewException e) { + System.out.println(e.getMessage() + " (ignoring " + s.getName() + ")"); + } + }); + } catch (ElementNotPermittedInViewException e) { + System.out.println(e.getMessage() + " (ignoring " + element.getName() + ")"); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/StaticView.java b/structurizr-core/src/com/structurizr/view/StaticView.java index b4199bfa5..da0dfb569 100644 --- a/structurizr-core/src/com/structurizr/view/StaticView.java +++ b/structurizr-core/src/com/structurizr/view/StaticView.java @@ -11,7 +11,7 @@ /** * The superclass for all static views (system landscape, system context, container and component views). */ -public abstract class StaticView extends View { +public abstract class StaticView extends ModelView { private List animations = new ArrayList<>(); diff --git a/structurizr-core/src/com/structurizr/view/View.java b/structurizr-core/src/com/structurizr/view/View.java index fdfbadfd9..0d18cdc7b 100644 --- a/structurizr-core/src/com/structurizr/view/View.java +++ b/structurizr-core/src/com/structurizr/view/View.java @@ -1,96 +1,28 @@ package com.structurizr.view; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonSetter; import com.structurizr.PropertyHolder; -import com.structurizr.model.*; -import com.structurizr.util.StringUtils; import javax.annotation.Nonnull; -import java.util.*; -import java.util.stream.Collectors; +import java.util.HashMap; +import java.util.Map; /** - * The superclass for all views (static views, dynamic views and deployment views). + * The superclass for all views. */ public abstract class View implements PropertyHolder { - private static final int DEFAULT_RANK_SEPARATION = 300; - private static final int DEFAULT_NODE_SEPARATION = 300; - - private SoftwareSystem softwareSystem; - private String softwareSystemId; - private String description = ""; private String key; private int order; - private PaperSize paperSize = null; - private Dimensions dimensions = null; - private AutomaticLayout automaticLayout = null; - private boolean mergeFromRemote = true; private String title; + private String description; private Map properties = new HashMap<>(); - private Set elementViews = new LinkedHashSet<>(); - private Set relationshipViews = new LinkedHashSet<>(); - - private LayoutMergeStrategy layoutMergeStrategy = new DefaultLayoutMergeStrategy(); - private ViewSet viewSet; View() { } - View(SoftwareSystem softwareSystem, String key, String description) { - this.softwareSystem = softwareSystem; - if (!StringUtils.isNullOrEmpty(key)) { - setKey(key); - } else { - throw new IllegalArgumentException("A key must be specified."); - } - setDescription(description); - } - - /** - * Gets the model that this view belongs to. - * - * @return a Model object - */ - @JsonIgnore - public Model getModel() { - return softwareSystem.getModel(); - } - - /** - * Gets the software system that this view is associated with. - * - * @return a SoftwareSystem object, or null if this view is not associated with a software system (e.g. it's a system landscape view) - */ - @JsonIgnore - public SoftwareSystem getSoftwareSystem() { - return softwareSystem; - } - - void setSoftwareSystem(SoftwareSystem softwareSystem) { - this.softwareSystem = softwareSystem; - } - - /** - * Gets the ID of the software system this view is associated with. - * - * @return the ID, as a String, or null if this view is not associated with a software system (e.g. it's a system landscape view) - */ - public String getSoftwareSystemId() { - if (this.softwareSystem != null) { - return this.softwareSystem.getId(); - } else { - return this.softwareSystemId; - } - } - - void setSoftwareSystemId(String softwareSystemId) { - this.softwareSystemId = softwareSystemId; - } - /** * Gets the description of this view. * @@ -138,107 +70,6 @@ void setOrder(int order) { this.order = Math.max(1, order); } - /** - * Gets the paper size that should be used to render this view. - * - * @return a PaperSize - */ - public PaperSize getPaperSize() { - return paperSize; - } - - public void setPaperSize(PaperSize paperSize) { - this.paperSize = paperSize; - } - - public Dimensions getDimensions() { - return dimensions; - } - - public void setDimensions(Dimensions dimensions) { - this.dimensions = dimensions; - } - - /** - * Gets the automatic layout settings for this view. - * - * @return an AutomaticLayout object, or null if not enabled - */ - public AutomaticLayout getAutomaticLayout() { - return automaticLayout; - } - - @JsonSetter - void setAutomaticLayout(AutomaticLayout automaticLayout) { - this.automaticLayout = automaticLayout; - } - - /** - * Enables automatic layout for this view, with some default settings. - */ - public void enableAutomaticLayout() { - enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 600, 200, false); - } - - /** - * Enables automatic layout for this view, with the specified settings, using the Dagre implementation. - * - * @param rankDirection the rank direction - * @param rankSeparation the separation between ranks (in pixels, a positive integer) - * @param nodeSeparation the separation between nodes within the same rank (in pixels, a positive integer) - * @param edgeSeparation the separation between edges (in pixels, a positive integer) - * @param vertices whether vertices should be created during automatic layout - */ - public void enableAutomaticLayout(AutomaticLayout.RankDirection rankDirection, int rankSeparation, int nodeSeparation, int edgeSeparation, boolean vertices) { - this.automaticLayout = new AutomaticLayout(AutomaticLayout.Implementation.Dagre, rankDirection, rankSeparation, nodeSeparation, edgeSeparation, vertices); - } - - /** - * Enables automatic layout for this view, with the specified direction, using the Graphviz implementation. - * - * @param rankDirection the rank direction - */ - public void enableAutomaticLayout(AutomaticLayout.RankDirection rankDirection) { - enableAutomaticLayout(rankDirection, DEFAULT_RANK_SEPARATION, DEFAULT_NODE_SEPARATION); - } - - /** - * Enables automatic layout for this view, with the specified settings, using the Graphviz implementation. - * - * @param rankDirection the rank direction - * @param rankSeparation the separation between ranks (in pixels, a positive integer) - * @param nodeSeparation the separation between nodes within the same rank (in pixels, a positive integer) - */ - public void enableAutomaticLayout(AutomaticLayout.RankDirection rankDirection, int rankSeparation, int nodeSeparation) { - this.automaticLayout = new AutomaticLayout(AutomaticLayout.Implementation.Graphviz, rankDirection, rankSeparation, nodeSeparation, 0, false); - } - - /** - * Disables automatic layout for this view. - */ - public void disableAutomaticLayout() { - this.automaticLayout = null; - } - - /** - * Gets whether layout information for this view should be merged from a remote version of the workspace. - * - * @return true if layout information should be merged from the remote workspace, false otherwise - */ - public boolean getMergeFromRemote() { - return mergeFromRemote; - } - - /** - * Specifies whether layout information for this view should be merged from a remote version of the workspace. - * - * @param mergeFromRemote true if layout information should be merged from the remote workspace, false otherwise - */ - @JsonIgnore - public void setMergeFromRemote(boolean mergeFromRemote) { - this.mergeFromRemote = mergeFromRemote; - } - /** * Gets the title of this view, if one has been set. * @@ -265,205 +96,6 @@ public void setTitle(String title) { @JsonIgnore public abstract String getName(); - protected final void addElement(Element element, boolean addRelationships) { - if (element == null) { - throw new IllegalArgumentException("An element must be specified."); - } - - if (getModel().contains(element)) { - checkElementCanBeAdded(element); - elementViews.add(new ElementView(element)); - - if (addRelationships) { - addRelationships(element); - } - } else { - throw new IllegalArgumentException("The element named " + element.getName() + " does not exist in the model associated with this view."); - } - } - - protected abstract void checkElementCanBeAdded(Element element); - - private void addRelationships(Element element) { - Set elements = getElements().stream() - .map(ElementView::getElement) - .collect(Collectors.toSet()); - - // add relationships where the destination exists in the view already - for (Relationship relationship : element.getRelationships()) { - if (elements.contains(relationship.getDestination())) { - this.relationshipViews.add(new RelationshipView(relationship)); - } - } - - // add relationships where the source exists in the view already - for (Element e : elements) { - for (Relationship r : e.getRelationships()) { - if (r.getDestination().equals(element)) { - this.relationshipViews.add(new RelationshipView(r)); - } - } - } - } - - protected void removeElement(Element element) { - if (element == null) { - throw new IllegalArgumentException("An element must be specified."); - } - - if (!canBeRemoved(element)) { - throw new IllegalArgumentException("The element named '" + element.getName() + "' cannot be removed from this view."); - } - - ElementView elementView = new ElementView(element); - elementViews.remove(elementView); - - for (RelationshipView relationshipView : getRelationships()) { - if (relationshipView.getRelationship().getSource().equals(element) || - relationshipView.getRelationship().getDestination().equals(element)) { - remove(relationshipView.getRelationship()); - } - } - } - - protected RelationshipView addRelationship(Relationship relationship) { - if (relationship == null) { - throw new IllegalArgumentException("A relationship must be specified."); - } - - if (isElementInView(relationship.getSource()) && isElementInView(relationship.getDestination())) { - RelationshipView relationshipView = new RelationshipView(relationship); - relationshipViews.add(relationshipView); - - return relationshipView; - } - - return null; - } - - public boolean isElementInView(Element element) { - return this.elementViews.stream().anyMatch(ev -> ev.getElement().equals(element)); - } - - /** - * Removes a relationship from this view. - * - * @param relationship the Relationship to remove - */ - public void remove(Relationship relationship) { - if (relationship != null) { - RelationshipView relationshipView = new RelationshipView(relationship); - relationshipViews.remove(relationshipView); - } - } - - /** - * Removes relationships that are not connected to the specified element. - * - * @param element the Element to test against - */ - public void removeRelationshipsNotConnectedToElement(Element element) { - if (element != null) { - getRelationships().stream() - .map(RelationshipView::getRelationship) - .filter(r -> !r.getSource().equals(element) && !r.getDestination().equals(element)) - .forEach(this::remove); - } - } - - /** - * Gets the set of elements in this view. - * - * @return a Set of ElementView objects - */ - public Set getElements() { - return new HashSet<>(elementViews); - } - - void setElements(Set elementViews) { - if (elementViews != null) { - this.elementViews = new HashSet<>(elementViews); - } - } - - /** - * Gets the set of relationships in this view. - * - * @return a Set of RelationshipView objects - */ - public Set getRelationships() { - return new HashSet<>(this.relationshipViews); - } - - void setRelationships(Set relationshipViews) { - if (relationshipViews != null) { - this.relationshipViews = new HashSet<>(relationshipViews); - } - } - - /** - * Removes all elements that have no relationships to other elements in this view. - */ - public void removeElementsWithNoRelationships() { - Set relationships = getRelationships(); - - Set elementIds = new HashSet<>(); - relationships.forEach(rv -> elementIds.add(rv.getRelationship().getSourceId())); - relationships.forEach(rv -> elementIds.add(rv.getRelationship().getDestinationId())); - - for (ElementView elementView : getElements()) { - if (!elementIds.contains(elementView.getId())) { - removeElement(elementView.getElement()); - } - } - } - - /** - * Sets the strategy used for merging layout information (paper size, x/y positioning, etc) - * from one version of this view to another. - * - * @param layoutMergeStrategy an instance of LayoutMergeStrategy - */ - public void setLayoutMergeStrategy(LayoutMergeStrategy layoutMergeStrategy) { - if (layoutMergeStrategy == null) { - throw new IllegalArgumentException("A LayoutMergeStrategy object must be provided."); - } - - this.layoutMergeStrategy = layoutMergeStrategy; - } - - /** - * Attempts to copy the visual layout information (e.g. x,y coordinates) of elements and relationships - * from the specified source view into this view. - * - * @param source the source View - */ - void copyLayoutInformationFrom(@Nonnull View source) { - layoutMergeStrategy.copyLayoutInformation(source, this); - } - - /** - * Gets the element view for the given element. - * - * @param element the Element to find the ElementView for - * @return an ElementView object, or null if the element doesn't exist in the view - */ - public ElementView getElementView(@Nonnull Element element) { - Optional elementView = this.elementViews.stream().filter(ev -> ev.getId().equals(element.getId())).findFirst(); - return elementView.orElse(null); - } - - /** - * Gets the relationship view for the given relationship. - * - * @param relationship the Relationship to find the RelationshipView for - * @return a RelationshipView object, or null if the relationship doesn't exist in the view - */ - public RelationshipView getRelationshipView(@Nonnull Relationship relationship) { - Optional relationshipView = this.relationshipViews.stream().filter(rv -> rv.getId().equals(relationship.getId())).findFirst(); - return relationshipView.orElse(null); - } - void setViewSet(@Nonnull ViewSet viewSet) { this.viewSet = viewSet; } @@ -478,70 +110,6 @@ public ViewSet getViewSet() { return viewSet; } - protected abstract boolean canBeRemoved(Element element); - - final void checkParentAndChildrenHaveNotAlreadyBeenAdded(StaticStructureElement elementToBeAdded) { - // check a parent hasn't been added already - Set idsOfElementsInView = getElements().stream().map(ElementView::getElement).map(Element::getId).collect(Collectors.toSet()); - - Element parent = elementToBeAdded.getParent(); - while (parent != null) { - if (idsOfElementsInView.contains(parent.getId())) { - throw new ElementNotPermittedInViewException("A parent of " + elementToBeAdded.getName() + " is already in this view."); - } - - parent = parent.getParent(); - } - - // and now check a child hasn't been added already - Set elementParentIds = new HashSet<>(); - for (ElementView elementView : getElements()) { - Element element = elementView.getElement(); - parent = element.getParent(); - while (parent != null) { - elementParentIds.add(parent.getId()); - parent = parent.getParent(); - } - } - - if (elementParentIds.contains(elementToBeAdded.getId())) { - throw new ElementNotPermittedInViewException("A child of " + elementToBeAdded.getName() + " is already in this view."); - } - } - - protected void addNearestNeighbours(Element element, Class typeOfElement) { - if (element == null) { - return; - } - - try { - addElement(element, true); - - Set relationships = getModel().getRelationships(); - relationships.stream().filter(r -> r.getSource().equals(element) && typeOfElement.isInstance(r.getDestination())) - .map(Relationship::getDestination) - .forEach(d -> { - try { - addElement(d, true); - } catch (ElementNotPermittedInViewException e) { - System.out.println(e.getMessage() + " (ignoring " + d.getName() + ")"); - } - }); - - relationships.stream().filter(r -> r.getDestination().equals(element) && typeOfElement.isInstance(r.getSource())) - .map(Relationship::getSource) - .forEach(s -> { - try { - addElement(s, true); - } catch (ElementNotPermittedInViewException e) { - System.out.println(e.getMessage() + " (ignoring " + s.getName() + ")"); - } - }); - } catch (ElementNotPermittedInViewException e) { - System.out.println(e.getMessage() + " (ignoring " + element.getName() + ")"); - } - } - /** * Gets the collection of name-value property pairs associated with this view, as a Map. * diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/com/structurizr/view/ViewSet.java index 4234e7afa..8644db9cf 100644 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/com/structurizr/view/ViewSet.java @@ -265,6 +265,7 @@ public FilteredView createFilteredView(StaticView view, String key, String descr FilteredView filteredView = new FilteredView(view, key, description, mode, tags); filteredView.setOrder(getNextOrder()); + view.setViewSet(this); filteredViews.add(filteredView); return filteredView; } @@ -281,6 +282,7 @@ public ImageView createImageView(String key) { ImageView view = new ImageView(key); view.setOrder(getNextOrder()); + view.setViewSet(this); imageViews.add(view); return view; } @@ -301,6 +303,7 @@ public ImageView createImageView(Element element, String key) { ImageView view = new ImageView(element, key); view.setOrder(getNextOrder()); + view.setViewSet(this); imageViews.add(view); return view; } @@ -641,7 +644,14 @@ void hydrate(Model model) { ); } - filteredView.setView(view); + if (view instanceof StaticView) { + filteredView.setView((StaticView)view); + } else { + throw new WorkspaceValidationException( + String.format("The filtered view with key \"%s\" is based upon a view (key=%s), but that view is not a static view.", + filteredView.getKey(), filteredView.getBaseViewKey()) + ); + } } for (ImageView view : imageViews) { @@ -659,7 +669,7 @@ void hydrate(Model model) { } } - private void hydrateView(View view) { + private void hydrateView(ModelView view) { view.setViewSet(this); for (ElementView elementView : view.getElements()) { @@ -687,8 +697,7 @@ private void hydrateView(View view) { } } - private void checkViewKeysAreUnique() { - Set keys = new HashSet<>(); + private Collection getAllViews() { Collection views = new ArrayList<>(); views.addAll(customViews); views.addAll(systemLandscapeViews); @@ -697,38 +706,26 @@ private void checkViewKeysAreUnique() { views.addAll(componentViews); views.addAll(dynamicViews); views.addAll(deploymentViews); + views.addAll(filteredViews); + views.addAll(imageViews); - for (View view : views) { + return views; + } + + private void checkViewKeysAreUnique() { + Set keys = new HashSet<>(); + + for (View view : getAllViews()) { if (keys.contains(view.getKey())) { throw new WorkspaceValidationException("A view with the key " + view.getKey() + " already exists."); } else { keys.add(view.getKey()); } } - - for (FilteredView filteredView : filteredViews) { - if (keys.contains(filteredView.getKey())) { - throw new WorkspaceValidationException("A view with the key " + filteredView.getKey() + " already exists."); - } else { - keys.add(filteredView.getKey()); - } - } } private synchronized int getNextOrder() { - int order = 0; - - order = Math.max(order, customViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); - order = Math.max(order, systemLandscapeViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); - order = Math.max(order, systemContextViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); - order = Math.max(order, containerViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); - order = Math.max(order, componentViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); - order = Math.max(order, dynamicViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); - order = Math.max(order, deploymentViews.stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0)); - order = Math.max(order, filteredViews.stream().max(Comparator.comparingInt(FilteredView::getOrder)).map(FilteredView::getOrder).orElse(0)); - order = Math.max(order, imageViews.stream().max(Comparator.comparingInt(ImageView::getOrder)).map(ImageView::getOrder).orElse(0)); - - return order + 1; + return getAllViews().stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0) + 1; } /** From a7204923685483c42329fda0dd01e98a633d795d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 13 Feb 2023 14:51:40 +0000 Subject: [PATCH 070/418] Added some basic construction tests. --- .../com/structurizr/view/ImageViewTests.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 structurizr-core/test/unit/com/structurizr/view/ImageViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ImageViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ImageViewTests.java new file mode 100644 index 000000000..b7b921099 --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/view/ImageViewTests.java @@ -0,0 +1,30 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ImageViewTests extends AbstractWorkspaceTestBase { + + @Test + void construction_WhenNoElementIsSpecified() { + ImageView view = views.createImageView("key"); + + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + } + + @Test + void construction_WhenAnElementIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ImageView view = views.createImageView(softwareSystem, "key"); + + assertEquals("key", view.getKey()); + assertSame(softwareSystem, view.getElement()); + assertEquals(softwareSystem.getId(), view.getElementId()); + } + +} \ No newline at end of file From 94826f9dac58629a49b1e873652c93efedff0340 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 16 Feb 2023 11:06:49 +0000 Subject: [PATCH 071/418] Updated for build. --- build.gradle | 2 +- docs/changelog.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 8323b965f..a9925b673 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.19.0' + version = '1.20.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 6b8866b18..0878eb24f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,8 @@ # Changelog -## 1.20.0 (unreleased) +## 1.20.0 (16th February 2023) -- __Breaking change__: Renamed `View` to `ModelView`. +- __Breaking change__: Renamed `com.structurizr.view.View` to `com.structurizr.view.ModelView`. - Added support for "image views". - Added a `Window` shape. - `ThemeUtils.toJson()` now includes the workspace branding logo and font in the resulting theme. From 84a5664f2588cac464a8c3cbbe5b2f9fabb97759 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 16 Feb 2023 14:36:07 +0000 Subject: [PATCH 072/418] `ViewSet.getViews()` now includes all views. --- build.gradle | 2 +- docs/changelog.md | 3 ++ .../src/com/structurizr/view/ViewSet.java | 35 ++++--------------- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/build.gradle b/build.gradle index a9925b673..bed9016fc 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.20.0' + version = '1.20.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 0878eb24f..f221f2dc4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,8 @@ # Changelog +## 1.20.1 (unreleased) + +- `ViewSet.getViews()` now includes all views. ## 1.20.0 (16th February 2023) - __Breaking change__: Renamed `com.structurizr.view.View` to `com.structurizr.view.ModelView`. diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/com/structurizr/view/ViewSet.java index 8644db9cf..88e2a6f67 100644 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/com/structurizr/view/ViewSet.java @@ -347,16 +347,7 @@ View getViewWithKey(String key) { throw new IllegalArgumentException("A key must be specified."); } - Set views = new HashSet<>(); - views.addAll(customViews); - views.addAll(systemLandscapeViews); - views.addAll(systemContextViews); - views.addAll(containerViews); - views.addAll(componentViews); - views.addAll(dynamicViews); - views.addAll(deploymentViews); - - return views.stream().filter(v -> key.equals(v.getKey())).findFirst().orElse(null); + return getViews().stream().filter(v -> key.equals(v.getKey())).findFirst().orElse(null); } /** @@ -528,7 +519,7 @@ void setImageView(Set imageViews) { } /** - * Gets the set of all views (except filtered and image views). + * Gets the set of all views. * * @return a Collection of View objects */ @@ -536,12 +527,15 @@ void setImageView(Set imageViews) { public Collection getViews() { HashSet views = new HashSet<>(); + views.addAll(getCustomViews()); views.addAll(getSystemLandscapeViews()); views.addAll(getSystemContextViews()); views.addAll(getContainerViews()); views.addAll(getComponentViews()); views.addAll(getDynamicViews()); views.addAll(getDeploymentViews()); + views.addAll(getFilteredViews()); + views.addAll(getImageViews()); return views; } @@ -697,25 +691,10 @@ private void hydrateView(ModelView view) { } } - private Collection getAllViews() { - Collection views = new ArrayList<>(); - views.addAll(customViews); - views.addAll(systemLandscapeViews); - views.addAll(systemContextViews); - views.addAll(containerViews); - views.addAll(componentViews); - views.addAll(dynamicViews); - views.addAll(deploymentViews); - views.addAll(filteredViews); - views.addAll(imageViews); - - return views; - } - private void checkViewKeysAreUnique() { Set keys = new HashSet<>(); - for (View view : getAllViews()) { + for (View view : getViews()) { if (keys.contains(view.getKey())) { throw new WorkspaceValidationException("A view with the key " + view.getKey() + " already exists."); } else { @@ -725,7 +704,7 @@ private void checkViewKeysAreUnique() { } private synchronized int getNextOrder() { - return getAllViews().stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0) + 1; + return getViews().stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0) + 1; } /** From 35f968f0a1d993b2857b23ebc0264ee366424f5c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 16 Feb 2023 14:36:35 +0000 Subject: [PATCH 073/418] `ViewSet.getViewWithKey()` is now public. --- docs/changelog.md | 2 ++ structurizr-core/src/com/structurizr/view/ViewSet.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index f221f2dc4..67924b149 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,8 @@ ## 1.20.1 (unreleased) - `ViewSet.getViews()` now includes all views. +- `ViewSet.getViewWithKey()` is now public. + ## 1.20.0 (16th February 2023) - __Breaking change__: Renamed `com.structurizr.view.View` to `com.structurizr.view.ModelView`. diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/com/structurizr/view/ViewSet.java index 88e2a6f67..2763aa03e 100644 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/com/structurizr/view/ViewSet.java @@ -342,7 +342,7 @@ private void assertThatTheViewIsNotNull(View view) { * @param key the key * @return a View object, or null if a view with the specified key could not be found */ - View getViewWithKey(String key) { + public View getViewWithKey(String key) { if (key == null) { throw new IllegalArgumentException("A key must be specified."); } From 8b86aa33baac409afef19ee49d396c5c0ceea742 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 16 Feb 2023 14:44:31 +0000 Subject: [PATCH 074/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 67924b149..495158303 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.20.1 (unreleased) +## 1.20.1 (16th February 2023) - `ViewSet.getViews()` now includes all views. - `ViewSet.getViewWithKey()` is now public. From 8bf1c385c22e49fd239d0b707f9d5c6f1fd483e6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 18 Feb 2023 15:49:33 +0000 Subject: [PATCH 075/418] Sets the viewset on new filtered views. --- structurizr-core/src/com/structurizr/view/ViewSet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/com/structurizr/view/ViewSet.java index 2763aa03e..02e799b3f 100644 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/com/structurizr/view/ViewSet.java @@ -265,7 +265,7 @@ public FilteredView createFilteredView(StaticView view, String key, String descr FilteredView filteredView = new FilteredView(view, key, description, mode, tags); filteredView.setOrder(getNextOrder()); - view.setViewSet(this); + filteredView.setViewSet(this); filteredViews.add(filteredView); return filteredView; } From d30d24890c3e55c5de26198b000267c7ce3f22a8 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 24 Feb 2023 16:00:20 +0000 Subject: [PATCH 076/418] Removes the concept of "code elements" from `Component`. --- build.gradle | 2 +- docs/changelog.md | 4 + .../com/structurizr/model/CodeElement.java | 236 ------------------ .../structurizr/model/CodeElementRole.java | 13 - .../src/com/structurizr/model/Component.java | 80 +----- .../src/com/structurizr/model/Container.java | 46 +--- .../src/com/structurizr/model/Model.java | 6 +- .../structurizr/model/CodeElementTests.java | 143 ----------- .../com/structurizr/model/ComponentTests.java | 96 ------- .../com/structurizr/model/ContainerTests.java | 62 ----- 10 files changed, 10 insertions(+), 678 deletions(-) delete mode 100644 structurizr-core/src/com/structurizr/model/CodeElement.java delete mode 100644 structurizr-core/src/com/structurizr/model/CodeElementRole.java delete mode 100644 structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java diff --git a/build.gradle b/build.gradle index bed9016fc..5a9142c6b 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.20.1' + version = '1.21.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 495158303..59fc17e5a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.21.0 (unreleased) + +- __Breaking change__: Removes the concept of "code elements" from `Component`. + ## 1.20.1 (16th February 2023) - `ViewSet.getViews()` now includes all views. diff --git a/structurizr-core/src/com/structurizr/model/CodeElement.java b/structurizr-core/src/com/structurizr/model/CodeElement.java deleted file mode 100644 index a6935494a..000000000 --- a/structurizr-core/src/com/structurizr/model/CodeElement.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.util.Url; - -/** - * Represents a code element, such as a Java class or interface, - * that is part of the implementation of a component. - */ -public final class CodeElement { - - /** the role of the code element ... Primary or Supporting */ - private CodeElementRole role = CodeElementRole.Supporting; - - /** the name of the code element ... typically the simple class/interface name */ - private String name; - - /** the fully qualified type of the code element **/ - private String type; - - /** a short description of the code element */ - private String description; - - /** a URL; e.g. a reference to the code element in source code control */ - private String url; - - /** the programming language used to create the code element */ - private String language = "Java"; - - /** the category of code element; e.g. class, interface, etc */ - private String category; - - /** the visibility of the code element; e.g. public, package, private */ - private String visibility; - - /** the size of the code element; e.g. the number of lines */ - private long size; - - CodeElement() { - } - - CodeElement(String fullyQualifiedTypeName) { - if (fullyQualifiedTypeName == null || fullyQualifiedTypeName.trim().isEmpty()) { - throw new IllegalArgumentException("A fully qualified name must be provided."); - } - - int dot = fullyQualifiedTypeName.lastIndexOf('.'); - if (dot > -1) { - setName(fullyQualifiedTypeName.substring(dot+1, fullyQualifiedTypeName.length())); - setType(fullyQualifiedTypeName); - } else { - setName(fullyQualifiedTypeName); - setType(fullyQualifiedTypeName); - } - } - - /** - * Gets the role of this code element; Primary or Supporting. - * - * @return a CodeElementRole enum - */ - public CodeElementRole getRole() { - return role; - } - - void setRole(CodeElementRole role) { - this.role = role; - } - - /** - * Gets the name of this code element. - * - * @return the name, as a String - */ - public String getName() { - return name; - } - - void setName(String name) { - this.name = name; - } - - /** - * Gets the type (fully qualified type name) of this code element. - * - * @return the type, as a String - */ - public String getType() { - return type; - } - - void setType(String type) { - this.type = type; - } - - /** - * Gets the Java package of this component (i.e. the package of the primary code element). - * - * @return the package name, as a String - */ - @JsonIgnore - public String getPackage() { - return type.substring(0, type.lastIndexOf('.')); - } - - /** - * Gets the description of this code element. - * - * @return the description, as a String - */ - public String getDescription() { - return description; - } - - /** - * Sets the description of this code element. - * - * @param description the description, as a String - */ - public void setDescription(String description) { - this.description = description; - } - - /** - * Gets the URL where more information about this code element can be found. - * - * @return the URL as a String, or null if not set - */ - public String getUrl() { - return url; - } - - /** - * Sets the URL where more information about this code element can be found. - * - * @param url the URL as a String - * @throws IllegalArgumentException if the URL is not a well-formed URL - */ - public void setUrl(String url) { - if (url != null && url.trim().length() > 0) { - if (Url.isUrl(url)) { - this.url = url; - } else { - throw new IllegalArgumentException(url + " is not a valid URL."); - } - } - } - - /** - * Gets the programming language of this code element. - * - * @return the programming language, as a String - */ - public String getLanguage() { - return language; - } - - /** - * Sets the programming language of this code element. - * - * @param language the programming language, as a String - */ - public void setLanguage(String language) { - this.language = language; - } - - /** - * Gets the category of this code element (interface, class, etc). - * - * @return the category, as a String - */ - public String getCategory() { - return category; - } - - /** - * Sets the category of this code element. - * - * @param category the category, as a String - */ - public void setCategory(String category) { - this.category = category; - } - - /** - * Gets the visibility of this code element (public, package, etc). - * - * @return the visibility, as a String - */ - public String getVisibility() { - return visibility; - } - - /** - * Sets the visibility of this code element. - * - * @param visibility the visibility, as a String - */ - public void setVisibility(String visibility) { - this.visibility = visibility; - } - - /** - * Gets the size of this code element (e.g. the number of lines of code). - * - * @return the size, as a long - */ - public long getSize() { - return size; - } - - /** - * Sets the size of this code element. - * - * @param size the size, as a long - */ - public void setSize(long size) { - this.size = size; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - CodeElement that = (CodeElement) o; - - return type.equals(that.type); - } - - @Override - public int hashCode() { - return type.hashCode(); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/CodeElementRole.java b/structurizr-core/src/com/structurizr/model/CodeElementRole.java deleted file mode 100644 index 0cc30c106..000000000 --- a/structurizr-core/src/com/structurizr/model/CodeElementRole.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.structurizr.model; - -/** - * Used to represent the role of a code element. A component can have - * one primary code element, and zero or more supporting code elements - * associated with it. - */ -public enum CodeElementRole { - - Primary, - Supporting - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/Component.java b/structurizr-core/src/com/structurizr/model/Component.java index 64c4127d7..06e3e7975 100644 --- a/structurizr-core/src/com/structurizr/model/Component.java +++ b/structurizr-core/src/com/structurizr/model/Component.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore; -import java.util.*; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; /** * Represents a "component" in the C4 model. @@ -12,8 +14,6 @@ public final class Component extends StaticStructureElement { private Container parent; private String technology; - private Set codeElements = new HashSet<>(); - private long size; Component() { } @@ -52,80 +52,6 @@ public void setTechnology(String technology) { this.technology = technology; } - /** - * Gets the type of this component (e.g. a fully qualified Java interface/class name). - * - * @return the type, as a String - */ - @JsonIgnore - public CodeElement getType() { - return codeElements.stream().filter(ce -> ce.getRole() == CodeElementRole.Primary).findFirst().orElse(null); - } - - /** - * Sets the type of this component (e.g. a fully qualified Java interface/class name). - * - * @param type the fully qualified type name - * @return the CodeElement that was created - * @throws IllegalArgumentException if the specified type is null - */ - public CodeElement setType(String type) { - Optional optional = codeElements.stream().filter(ce -> ce.getRole() == CodeElementRole.Primary).findFirst(); - optional.ifPresent(codeElement -> codeElements.remove(codeElement)); - - CodeElement codeElement = new CodeElement(type); - codeElement.setRole(CodeElementRole.Primary); - this.codeElements.add(codeElement); - - return codeElement; - } - - /** - * Gets the set of CodeElement objects. - * - * @return a Set, which could be empty - */ - public Set getCode() { - return new HashSet<>(codeElements); - } - - void setCode(Set codeElements) { - this.codeElements = codeElements; - } - - /** - * Adds a supporting type to this Component. - * - * @param type the fully qualified type name - * @return a CodeElement representing the supporting type - * @throws IllegalArgumentException if the specified type is null - */ - public CodeElement addSupportingType(String type) { - CodeElement codeElement = new CodeElement(type); - codeElement.setRole(CodeElementRole.Supporting); - this.codeElements.add(codeElement); - - return codeElement; - } - - /** - * Gets the size of this Component (e.g. number of lines). - * - * @return the size of this component, as a long - */ - public long getSize() { - return size; - } - - /** - * Sets the size of this component (e.g. number of lines). - * - * @param size the size - */ - public void setSize(long size) { - this.size = size; - } - /** * Gets the canonical name of this component, in the form "/Software System/Container/Component". * diff --git a/structurizr-core/src/com/structurizr/model/Container.java b/structurizr-core/src/com/structurizr/model/Container.java index 17f34ef00..a4c9fe29a 100644 --- a/structurizr-core/src/com/structurizr/model/Container.java +++ b/structurizr-core/src/com/structurizr/model/Container.java @@ -99,35 +99,7 @@ public Component addComponent(String name, String description) { * @throws IllegalArgumentException if the component name is null or empty, or a component with the same name already exists */ public Component addComponent(String name, String description, String technology) { - return this.addComponent(name, (String)null, description, technology); - } - - /** - * Adds a component to this container. - * - * @param name the name of the component - * @param type a Class instance representing the primary type of the component - * @param description a description of the component - * @param technology the technology of the component - * @return the resulting Component instance - * @throws IllegalArgumentException if the component name is null or empty, or a component with the same name already exists - */ - public Component addComponent(String name, Class type, String description, String technology) { - return this.addComponent(name, type.getCanonicalName(), description, technology); - } - - /** - * Adds a component to this container. - * - * @param name the name of the component - * @param type a String describing the fully qualified name of the primary type of the component - * @param description a description of the component - * @param technology the technology of the component - * @return the resulting Component instance - * @throws IllegalArgumentException if the component name is null or empty, or a component with the same name already exists - */ - public Component addComponent(String name, String type, String description, String technology) { - return getModel().addComponentOfType(this, name, type, description, technology); + return getModel().addComponent(this, name, description, technology); } void add(Component component) { @@ -167,22 +139,6 @@ public Component getComponentWithName(String name) { return component.orElse(null); } - /** - * Gets the component of the specified type. - * - * @param type the fully qualified type of the component - * @return the Component instance, or null if a component with the specified type does not exist - * @throws IllegalArgumentException if the type is null or empty - */ - public Component getComponentOfType(String type) { - if (type == null || type.trim().length() == 0) { - throw new IllegalArgumentException("A component type must be provided."); - } - - Optional component = components.stream().filter(c -> type.equals(c.getType().getType())).findFirst(); - return component.orElse(null); - } - /** * Gets the canonical name of this container, in the form "/Software System/Container". * diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/com/structurizr/model/Model.java index d5c06d5ce..b97c0d23c 100644 --- a/structurizr-core/src/com/structurizr/model/Model.java +++ b/structurizr-core/src/com/structurizr/model/Model.java @@ -215,17 +215,13 @@ Container addContainer(SoftwareSystem parent, @Nonnull String name, @Nullable St } } - Component addComponentOfType(Container parent, String name, String type, String description, String technology) { + Component addComponent(Container parent, String name, String description, String technology) { if (parent.getComponentWithName(name) == null) { Component component = new Component(); component.setName(name); component.setDescription(description); component.setTechnology(technology); - if (type != null && type.trim().length() > 0) { - component.setType(type); - } - component.setParent(parent); parent.add(component); diff --git a/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java b/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java deleted file mode 100644 index 79cb47c86..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.structurizr.model; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -public class CodeElementTests { - - @Test - void construction_WhenAFullyQualifiedNameIsSpecified() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertEquals("SomeComponent", codeElement.getName()); - assertEquals("com.structurizr.component.SomeComponent", codeElement.getType()); - } - - @Test - void construction_WhenAFullyQualifiedNameIsSpecifiedInTheDefaultPackage() { - CodeElement codeElement = new CodeElement("SomeComponent"); - assertEquals("SomeComponent", codeElement.getName()); - assertEquals("SomeComponent", codeElement.getType()); - } - - @Test - void descriptionProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertNull(codeElement.getDescription()); - - codeElement.setDescription("Description"); - assertEquals("Description", codeElement.getDescription()); - } - - @Test - void sizeProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertEquals(0, codeElement.getSize()); - - codeElement.setSize(123456); - assertEquals(123456, codeElement.getSize()); - } - - @Test - void languageProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertEquals("Java", codeElement.getLanguage()); - - codeElement.setLanguage("Scala"); - assertEquals("Scala", codeElement.getLanguage()); - } - - @Test - void categoryProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertNull(codeElement.getCategory()); - - codeElement.setCategory("class"); - assertEquals("class", codeElement.getCategory()); - } - - @Test - void visibilityProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertNull(codeElement.getVisibility()); - - codeElement.setVisibility("package"); - assertEquals("package", codeElement.getVisibility()); - } - - @Test - void setUrl() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - codeElement.setUrl("https://structurizr.com"); - assertEquals("https://structurizr.com", codeElement.getUrl()); - } - - @Test - void setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { - assertThrows(IllegalArgumentException.class, () -> { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - codeElement.setUrl("htt://blah"); - }); - } - - @Test - void setUrl_DoesNothing_WhenANullUrlIsSpecified() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - codeElement.setUrl(null); - assertNull(codeElement.getUrl()); - } - - @Test - void setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - codeElement.setUrl(" "); - assertNull(codeElement.getUrl()); - } - - @Test - void construction_ThrowsAnIllegalArgumentException_WhenANullFullyQualifiedNameIsSpecified() { - assertThrows(IllegalArgumentException.class, () -> { - new CodeElement(null); - }); - } - - @Test - void construction_ThrowsAnIllegalArgumentException_WhenAnEmptyFullyQualifiedNameIsSpecified() { - assertThrows(IllegalArgumentException.class, () -> { - new CodeElement(" "); - }); - } - - @Test - void equals_ReturnsFalse_WhenComparedToNull() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertNotEquals(codeElement, null); - } - - @Test - void equals_ReturnsFalse_WhenComparedToDifferentTypeOfObject() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertNotEquals(codeElement, "hello"); - } - - @Test - void equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithADifferentType() { - CodeElement codeElement1 = new CodeElement("com.structurizr.component.SomeComponent1"); - CodeElement codeElement2 = new CodeElement("com.structurizr.component.SomeComponent2"); - assertNotEquals(codeElement1, codeElement2); - } - - @Test - void equals_ReturnsFalse_WhenComparedToAnotherCodeElementWithTheSameType() { - CodeElement codeElement1 = new CodeElement("com.structurizr.component.SomeComponent1"); - CodeElement codeElement2 = new CodeElement("com.structurizr.component.SomeComponent1"); - assertEquals(codeElement1, codeElement2); - } - - @Test - void getPackage_ReturnsThePackageName() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertEquals("com.structurizr.component", codeElement.getPackage()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java index 82f72d2a4..fa19b0dee 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java @@ -3,8 +3,6 @@ import com.structurizr.AbstractWorkspaceTestBase; import org.junit.jupiter.api.Test; -import java.util.Set; - import static org.junit.jupiter.api.Assertions.*; public class ComponentTests extends AbstractWorkspaceTestBase { @@ -65,98 +63,4 @@ void technologyProperty() { assertEquals("Spring Bean", component.getTechnology()); } - @Test - void sizeProperty() { - Component component = new Component(); - assertEquals(0, component.getSize()); - - component.setSize(123456); - assertEquals(123456, component.getSize()); - } - - @Test - void setType_ThrowsAnExceptionWhenPassedNull() { - Component component = new Component(); - try { - component.setType(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A fully qualified name must be provided.", iae.getMessage()); - } - } - - @Test - void setType_AddsAPrimaryCodeElement_WhenPassedAFullyQualifiedTypeName() { - Component component = new Component(); - component.setType("com.structurizr.web.HomePageController"); - - Set codeElements = component.getCode(); - assertEquals(1, codeElements.size()); - CodeElement codeElement = codeElements.iterator().next(); - assertEquals("HomePageController", codeElement.getName()); - assertEquals("com.structurizr.web.HomePageController", codeElement.getType()); - assertEquals(CodeElementRole.Primary, codeElement.getRole()); - } - - @Test - void setType_OverwritesThePrimaryCodeElement_WhenCalledMoreThanOnce() { - Component component = new Component(); - component.setType("com.structurizr.web.HomePageController"); - component.setType("com.structurizr.web.SomeOtherController"); - - Set codeElements = component.getCode(); - assertEquals(1, codeElements.size()); - CodeElement codeElement = codeElements.iterator().next(); - assertEquals("SomeOtherController", codeElement.getName()); - assertEquals("com.structurizr.web.SomeOtherController", codeElement.getType()); - assertEquals(CodeElementRole.Primary, codeElement.getRole()); - - } - - @Test - void addSupportingType_ThrowsAnExceptionWhenPassedNull() { - Component component = new Component(); - try { - component.addSupportingType(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A fully qualified name must be provided.", iae.getMessage()); - } - } - - @Test - void addSupportingType_AddsASupportingCodeElement_WhenPassedAFullyQualifiedTypeName() { - Component component = new Component(); - component.addSupportingType("com.structurizr.web.HomePageViewModel"); - - Set codeElements = component.getCode(); - assertEquals(1, codeElements.size()); - CodeElement codeElement = codeElements.iterator().next(); - assertEquals("HomePageViewModel", codeElement.getName()); - assertEquals("com.structurizr.web.HomePageViewModel", codeElement.getType()); - assertEquals(CodeElementRole.Supporting, codeElement.getRole()); - } - - @Test - void getType_ReturnsNull_WhenThereAreNoCodeElements() { - Component component = new Component(); - assertNull(component.getType()); - } - - @Test - void getType_ReturnsNull_WhenThereAreNoPrimaryCodeElements() { - Component component = new Component(); - component.addSupportingType("com.structurizr.SomeType"); - assertNull(component.getType()); - } - - @Test - void getType_ReturnsThePrimaryCodeElement_WhenThereIsAPrimaryCodeElement() { - Component component = new Component(); - component.setType("com.structurizr.SomeType"); - CodeElement codeElement = component.getType(); - assertEquals(CodeElementRole.Primary, codeElement.getRole()); - assertEquals("com.structurizr.SomeType", codeElement.getType()); - } - } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java index 3665f5194..159f40805 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java @@ -84,8 +84,6 @@ void addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() { assertEquals("Name", component.getName()); assertEquals("Description", component.getDescription()); assertNull(component.getTechnology()); - assertNull(component.getType()); - assertEquals(0, component.getCode().size()); assertSame(container, component.getParent()); } @@ -96,32 +94,6 @@ void addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnology( assertEquals("Name", component.getName()); assertEquals("Description", component.getDescription()); assertEquals("Technology", component.getTechnology()); - assertNull(component.getType()); - assertEquals(0, component.getCode().size()); - assertSame(container, component.getParent()); - } - - @Test - void addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndStringType() { - Component component = container.addComponent("Name", "SomeType", "Description", "Technology"); - assertTrue(container.getComponents().contains(component)); - assertEquals("Name", component.getName()); - assertEquals("Description", component.getDescription()); - assertEquals("Technology", component.getTechnology()); - assertEquals("SomeType", component.getType().getType()); - assertEquals(1, component.getCode().size()); - assertSame(container, component.getParent()); - } - - @Test - void addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnologyAndClassType() { - Component component = container.addComponent("Name", this.getClass(), "Description", "Technology"); - assertTrue(container.getComponents().contains(component)); - assertEquals("Name", component.getName()); - assertEquals("Description", component.getDescription()); - assertEquals("Technology", component.getTechnology()); - assertEquals("com.structurizr.model.ContainerTests", component.getType().getType()); - assertEquals(1, component.getCode().size()); assertSame(container, component.getParent()); } @@ -145,38 +117,4 @@ void getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { } } - @Test - void getComponentOfType_ThrowsAnException_WhenANullTypeIsSpecified() { - try { - container.getComponentOfType(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A component type must be provided.", iae.getMessage()); - } - } - - @Test - void getComponentOfType_ThrowsAnException_WhenAnEmptyTypeIsSpecified() { - try { - container.getComponentOfType(" "); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A component type must be provided.", iae.getMessage()); - } - } - - @Test - void getComponentOfType_ReturnsNull_WhenNoComponentWithTheSpecifiedTypeExists() { - assertNull(container.getComponentOfType("SomeType")); - } - - @Test - void getComponentOfType_ReturnsAComponent_WhenAComponentWithTheSpecifiedTypeExists() { - container.addComponent("Name", "SomeType", "Description", "Technology"); - Component component = container.getComponentOfType("SomeType"); - - assertNotNull(component); - assertEquals("SomeType", component.getType().getType()); - } - } \ No newline at end of file From a16624d758ae0e19590688b1034e964cc155b53d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 24 Feb 2023 16:26:51 +0000 Subject: [PATCH 077/418] Docs update. --- README.md | 10 ++-------- docs/client-side-encryption.md | 4 ++-- docs/faq.md | 12 +++++++++--- docs/model.md | 18 +++++++----------- docs/usage-patterns.md | 18 ------------------ 5 files changed, 20 insertions(+), 42 deletions(-) delete mode 100644 docs/usage-patterns.md diff --git a/README.md b/README.md index a8b396223..7f5d88274 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via t ## Table of contents +* [changelog](docs/changelog.md) * Introduction * [Getting started](docs/getting-started.md) * [Basic concepts](https://structurizr.com/help/concepts) (workspaces, models, views and documentation) @@ -38,7 +39,6 @@ or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via t * [Binaries](docs/binaries.md) * [Building from source](docs/building.md) * [API client](docs/api-client.md) - * [Usage patterns](docs/usage-patterns.md) * [FAQ](docs/faq.md) * Model * [Creating your model](docs/model.md) @@ -54,16 +54,10 @@ or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via t * [Styling elements](docs/styling-elements.md) * [Styling relationships](docs/styling-relationships.md) * [Filtered views](docs/filtered-views.md) - * [Graphviz automatic layout](https://github.com/structurizr/java-extensions/blob/master/structurizr-graphviz) * Other * [Client-side encryption](docs/client-side-encryption.md) * Related projects * [structurizr-dsl](https://github.com/structurizr/dsl): A text-based DSL for authoring Structurizr workspaces. * [structurizr-export](https://github.com/structurizr/export): Export model and views to external formats (e.g. PlantUML, Mermaid, etc). - * [structurizr-documentation](https://github.com/structurizr/documentation): Import Markdown/AsciiDoc documentation and ADRs into a Structurizr workspace. - * [java-extensions](https://github.com/structurizr/java-extensions): A collection of Structurizr for Java extensions; including the ability to extract software architecture information from code. - * [structurizr-kotlin](https://github.com/Catalysts/structurizr-extensions/tree/master/structurizr-kotlin): An extension for Structurizr that lets you create your models in a fluent way. - * [structurizr-spring-boot](https://github.com/Catalysts/structurizr-extensions/tree/master/structurizr-spring-boot): A way to apply dependency management to help modularise Structurizr code. - * [structurizr-groovy](https://github.com/tidyjava/structurizr-groovy): An initial version of a Groovy wrapper around Structurizr for Java. -* [changelog](docs/changelog.md) + * [structurizr-import](https://github.com/structurizr/import): Import Markdown/AsciiDoc documentation/ADRs into a Structurizr workspace. diff --git a/docs/client-side-encryption.md b/docs/client-side-encryption.md index cce9397e6..5115c7db1 100644 --- a/docs/client-side-encryption.md +++ b/docs/client-side-encryption.md @@ -1,8 +1,8 @@ # Client-side encryption -> Note: this page describes a feature that is not available to use with Structurizr's Free Plan. +> This feature is not available with a free Structurizr cloud service account. -The JSON representation of your workspace is stored on the Structurizr servers using AES encryption with a 128-bit key, a random salt and a server-side passphrase. For additional peace of mind, you can choose to encrypt your workspace with your own passphrase on the client before uploading it to Structurizr. In order to view a client-side encrypted workspace, you will be asked to enter your passphrase when you open the workspace in your web browser. The passphrase is then used to decrypt the workspace in your web browser - at no point does the passphrase leave your computer. +The JSON representation of your workspace is stored on the Structurizr cloud service using AES encryption with a 128-bit key, a random salt and a server-side passphrase. For additional peace of mind, you can choose to encrypt your workspace with your own passphrase on the client before uploading it to Structurizr. In order to view a client-side encrypted workspace, you will be asked to enter your passphrase when you open the workspace in your web browser. The passphrase is then used to decrypt the workspace in your web browser - at no point does the passphrase leave your computer. To use client-side encryption, simply create an instance of ```AesEncryptionStrategy``` and associate it with your ```StructurizrClient``` instance. For example: diff --git a/docs/faq.md b/docs/faq.md index cae05f899..29f78a937 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,11 +2,17 @@ ## Why are many classes final with package-protected members, and not open to extension? -First and foremost, this repo is a client library for the [Structurizr cloud service and on-premises installation](https://structurizr.com). It allows you to write Java code to create an in-memory object graph representing a software architecture model and views (a "workspace"), serialize that to JSON, and upload it via a web API. The workspace has an [OpenAPI definition](https://github.com/structurizr/json/blob/master/structurizr.yaml), but this library also implements a number of rules (think of them as the "business logic") to ensure that the workspace is valid. These rules include, for example, ensuring that all containers with a software system have unique names, and that you can't add components to a system context view. +First and foremost, this repo is a client library for the [Structurizr cloud service and on-premises installation](https://structurizr.com). +It allows you to write Java code to create an in-memory object graph representing a software architecture model and views (a "workspace"), serialize that to JSON, and upload it via a web API. +The workspace has an [OpenAPI definition](https://github.com/structurizr/json/blob/master/structurizr.yaml), but this library also implements a number of rules (think of them as the "business logic") to ensure that the workspace is valid. +These rules include, for example, ensuring that all containers with a software system have unique names, and that you can't add components to a system context view. -Removing the `final` modifier from the classes and leaving the them open for extension allows you to bypass/break these rules, which will likely lead to the serialized workspace definitions being incompatible with the Structurizr cloud service and on-premises installation. The output of this library also needs to be compatible with all of the other client libraries. +Removing the `final` modifier from the classes and leaving them open for extension allows you to bypass/break these rules, which will likely lead to the serialized workspace definitions being incompatible with the various diagram rendering tools +(i.e. the Structurizr cloud service/on-premises installation/Lite, plus the PlantUML/Mermaid exporters). +The output of this library also needs to be compatible with client libraries written in other languages. -You are welcome to fork this library for your own purposes. Alternatively, you can build a thin wrapper around the library, to provide your own custom functionality, or perhaps a more fluent API ... many teams have done this. +You are welcome to fork this library for your own purposes. +Alternatively, you can build a thin wrapper around the library, to provide your own custom functionality, or perhaps a more fluent API ... many teams have done this. ## Can I submit a pull request? diff --git a/docs/model.md b/docs/model.md index 7c5999e19..969755d6d 100644 --- a/docs/model.md +++ b/docs/model.md @@ -1,24 +1,20 @@ # Model -This is the definition of the software architecture model, consisting of people, software systems, containers, components, code elements and deployment nodes, plus the relationships between them. +This is the definition of the software architecture model, consisting of people, software systems, containers, components, deployment nodes, etc plus the relationships between them. All of the Java classes representing people, software systems, containers, components, etc, and the functionality related to creating a software architecture model can be found in the [com.structurizr.model](https://github.com/structurizr/java/tree/master/structurizr-core/src/com/structurizr/model) package. An empty model is created for you when you create a workspace. ```java -Workspace workspace = new Workspace("Getting Started", "This is a model of my software system."); +Workspace workspace = new Workspace("Name", "Description"); Model model = workspace.getModel(); ``` -Once you have a reference to a ```Model``` instance, you can add elements to it manually or automatically, using static analysis and reflection techniques. +Once you have a reference to a ```Model``` instance, you can add elements to it via the various public `add*` methods that you'll find on ```Model```, ```SoftwareSystem```, ```Container```, etc. -## 1. Manual model creation +## Automatic model generation -Manually adding elements to the model is the simplest way to use the Structurizr for Java client library. This can be done using the various public ```add*``` methods that you'll find on ```Model```, ```SoftwareSystem```, ```Container```, ```Component```, etc. - -## 2. Automatic extraction - -You can also extract components (and add them to a ```Container``` instance) automatically from a given codebase, using a number of different component finder strategies. See [Component finder](https://github.com/structurizr/java-extensions/blob/master/docs/component-finder.md) for more details. - -Although there is nothing included in the Structurizr for Java library to support this, you could also choose to parse an external definition of your software architecture (e.g. an AWS infrastructure topology, another Architecture Description Language, etc) and create model elements accordingly. \ No newline at end of file +Although there is nothing included in the Structurizr for Java library to support automatic model generation, +you could choose to parse an external definition of your software architecture (e.g. an AWS infrastructure topology, Terraform definition, another Architecture Description Language, your source code, etc) +and create model elements accordingly. \ No newline at end of file diff --git a/docs/usage-patterns.md b/docs/usage-patterns.md deleted file mode 100644 index a2958b185..000000000 --- a/docs/usage-patterns.md +++ /dev/null @@ -1,18 +0,0 @@ -# Usage patterns - -## Single program - -The simplest way to create a software architecture model is to write a single Java program that first creates the model elements (people, software systems, containers and components) before subsequently creating the required views and uploading the workspace to Structurizr. If you have a particularly large model, or you'd like to share common elements between models, then one approach is to modularise your program appropriately, perhaps using something like [Structurizr Extensions](https://github.com/Catalysts/structurizr-extensions). - -## Multiple programs - -Another approach is to write a collection of Java programs, that are each responsible for creating a different part of the software architecture model. This is especially useful if you need to use the component finder with different classpaths. You can then write a script, or use your build script, to run these Java programs in sequence. Intermediate versions of the workspace can be saved to and loaded from disk using the [WorkspaceUtils](https://github.com/structurizr/java/blob/master/structurizr-client/src/com/structurizr/util/WorkspaceUtils.java) class. For example: - -1. Program 1: Create the basic model elements (people, software systems and containers) and the relationships between them. -2. Program 2: Add components for container 1 (e.g. run the component finder). -3. Program 3: Add components for container 2 (e.g. run the component finder with a different classpath). -4. Program 4: Create views. -5. Program 5: Add styling. -6. Program 6: Upload to Structurizr. - -In this example, the first program would write the initial version of the workspace to a local file on disk, which subsequent programs then load and add to, before writing the workspace back to disk. From 4a648a90f68d64afea3318bd2c35a30dc6a42f7d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 24 Feb 2023 16:28:59 +0000 Subject: [PATCH 078/418] Reorganise TOC. --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7f5d88274..b6069bca3 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via t ## Table of contents -* [changelog](docs/changelog.md) +* [Changelog](docs/changelog.md) * Introduction * [Getting started](docs/getting-started.md) * [Basic concepts](https://structurizr.com/help/concepts) (workspaces, models, views and documentation) @@ -38,24 +38,24 @@ or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via t * [Examples](https://github.com/structurizr/examples) * [Binaries](docs/binaries.md) * [Building from source](docs/building.md) - * [API client](docs/api-client.md) * [FAQ](docs/faq.md) * Model * [Creating your model](docs/model.md) * [Implied relationships](docs/implied-relationships.md) * Views - * [Creating views](docs/views.md) - * [System Context diagram](docs/system-context-diagram.md) - * [Container diagram](docs/container-diagram.md) - * [Component diagram](docs/component-diagram.md) - * [Dynamic diagram](docs/dynamic-diagram.md) - * [Deployment diagram](docs/deployment-diagram.md) - * [System Landscape diagram](docs/system-landscape-diagram.md) - * [Styling elements](docs/styling-elements.md) - * [Styling relationships](docs/styling-relationships.md) - * [Filtered views](docs/filtered-views.md) -* Other - * [Client-side encryption](docs/client-side-encryption.md) + * [Creating views](docs/views.md) + * [System Context diagram](docs/system-context-diagram.md) + * [Container diagram](docs/container-diagram.md) + * [Component diagram](docs/component-diagram.md) + * [Dynamic diagram](docs/dynamic-diagram.md) + * [Deployment diagram](docs/deployment-diagram.md) + * [System Landscape diagram](docs/system-landscape-diagram.md) + * [Styling elements](docs/styling-elements.md) + * [Styling relationships](docs/styling-relationships.md) + * [Filtered views](docs/filtered-views.md) +* Cloud service/on-premises installation + * [API client](docs/api-client.md) + * [Client-side encryption](docs/client-side-encryption.md) * Related projects * [structurizr-dsl](https://github.com/structurizr/dsl): A text-based DSL for authoring Structurizr workspaces. * [structurizr-export](https://github.com/structurizr/export): Export model and views to external formats (e.g. PlantUML, Mermaid, etc). From 2c8c0b09549345c4f7d7445440b59b1943d391e1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 25 Feb 2023 13:19:19 +0000 Subject: [PATCH 079/418] Adds a utility method to find the content type of a URL. --- .../src/com/structurizr/util/ImageUtils.java | 18 ++++++++++++++++++ .../com/structurizr/util/ImageUtilsTests.java | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/util/ImageUtils.java b/structurizr-core/src/com/structurizr/util/ImageUtils.java index 6b7fdeee2..18115ed19 100644 --- a/structurizr-core/src/com/structurizr/util/ImageUtils.java +++ b/structurizr-core/src/com/structurizr/util/ImageUtils.java @@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.net.URL; import java.net.URLConnection; import java.util.Base64; @@ -38,6 +39,23 @@ public static String getContentType(@Nonnull File file) throws IOException { return contentType; } + /** + * Gets the content type of the specified URL representing an image. + * + * @param url a URL pointing to an image + * @return a content type (e.g. "image/png") + * @throws IOException if there is an error reading the file + */ + public static String getContentType(String url) throws IOException { + if (StringUtils.isNullOrEmpty(url)) { + throw new IllegalArgumentException("A URL must be specified."); + } + + URLConnection connection = new URL(url).openConnection(); + connection.setConnectTimeout(1000 * 30); + return connection.getContentType(); + } + /** * Gets the content of an image as a Base64 encoded string. * diff --git a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java index 819201ab7..a30598e1c 100644 --- a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java @@ -11,7 +11,7 @@ public class ImageUtilsTests { @Test void getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { - ImageUtils.getContentType(null); + ImageUtils.getContentType((File)null); fail(); } catch (IllegalArgumentException iae) { assertEquals("A file must be specified.", iae.getMessage()); @@ -56,6 +56,22 @@ void getContentType_ReturnsTheContentType_WhenAFileIsSpecified() throws Exceptio assertEquals("image/png", contentType); } + @Test + void getContentType_ThrowsAnException_WhenANullUrlIsSpecified() throws Exception { + try { + ImageUtils.getContentType((String)null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A URL must be specified.", iae.getMessage()); + } + } + + @Test + void getContentType_ReturnsTheContentType_WhenAUrlIsSpecified() throws Exception { + String contentType = ImageUtils.getContentType(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png").toURI().toURL().toExternalForm()); + assertEquals("image/png", contentType); + } + @Test void getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { From 9b31d5d3fd33075b565ed7c8a36b127dd400797c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 25 Feb 2023 15:30:10 +0000 Subject: [PATCH 080/418] Adds some support for getting the content types from data URIs. Also adds support for allow SVG. --- .../src/com/structurizr/util/ImageUtils.java | 39 +++++++++++++++++-- .../com/structurizr/util/ImageUtilsTests.java | 25 ++++++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/structurizr-core/src/com/structurizr/util/ImageUtils.java b/structurizr-core/src/com/structurizr/util/ImageUtils.java index 18115ed19..d656c143a 100644 --- a/structurizr-core/src/com/structurizr/util/ImageUtils.java +++ b/structurizr-core/src/com/structurizr/util/ImageUtils.java @@ -15,6 +15,15 @@ */ public class ImageUtils { + public static final String DATA_URI_PREFIX = "data:"; + public static final String DATA_URI_IMAGE_PNG = "data:image/png;base64,"; + public static final String DATA_URI_IMAGE_JPG = "data:image/jpeg;base64,"; + public static final String DATA_URI_IMAGE_SVG = "data:image/svg+xml;"; + + public static final String CONTENT_TYPE_IMAGE_PNG = "image/png"; + public static final String CONTENT_TYPE_IMAGE_JPG = "image/jpeg"; + public static final String CONTENT_TYPE_IMAGE_SVG = "image/svg+xml"; + /** * Gets the content type of the specified file representing an image. * @@ -56,6 +65,28 @@ public static String getContentType(String url) throws IOException { return connection.getContentType(); } + /** + * Gets the content type of the specified data URI representing an image. + * + * @param dataUri a data URI representing an image + * @return a content type (e.g. "image/png") + */ + public static String getContentTypeFromDataUri(String dataUri) { + if (StringUtils.isNullOrEmpty(dataUri)) { + throw new IllegalArgumentException("A data URI must be specified."); + } + + if (dataUri.startsWith(DATA_URI_IMAGE_PNG)) { + return CONTENT_TYPE_IMAGE_PNG; + } else if (dataUri.startsWith(DATA_URI_IMAGE_JPG)) { + return CONTENT_TYPE_IMAGE_JPG; + } else if (dataUri.startsWith(DATA_URI_IMAGE_SVG)) { + return CONTENT_TYPE_IMAGE_SVG; + } + + return null; + } + /** * Gets the content of an image as a Base64 encoded string. * @@ -84,7 +115,7 @@ public static String getImageAsDataUri(File file) throws IOException { String contentType = getContentType(file); String base64Content = getImageAsBase64(file); - return "data:" + contentType + ";base64," + base64Content; + return DATA_URI_PREFIX + contentType + ";base64," + base64Content; } public static void validateImage(String imageDescriptor) { @@ -104,7 +135,7 @@ public static void validateImage(String imageDescriptor) { return; } - if (imageDescriptor.startsWith("data:image")) { + if (imageDescriptor.startsWith(DATA_URI_PREFIX)) { if (ImageUtils.isSupportedDataUri(imageDescriptor)) { // it's a PNG/JPG data URI return; @@ -118,7 +149,9 @@ public static void validateImage(String imageDescriptor) { } public static boolean isSupportedDataUri(String uri) { - return uri.startsWith("data:image/png;base64,") || uri.startsWith("data:image/jpeg;base64,"); + return uri.startsWith(DATA_URI_IMAGE_PNG) || + uri.startsWith(DATA_URI_IMAGE_JPG) || + uri.startsWith(DATA_URI_IMAGE_SVG); } } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java index a30598e1c..81892cd78 100644 --- a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java @@ -72,6 +72,24 @@ void getContentType_ReturnsTheContentType_WhenAUrlIsSpecified() throws Exception assertEquals("image/png", contentType); } + @Test + void getContentTypeFromDataUri_ThrowsAnException_WhenANullDataUriIsSpecified() throws Exception { + try { + ImageUtils.getContentTypeFromDataUri(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A data URI must be specified.", iae.getMessage()); + } + } + + @Test + void getContentTypeFromDataUri_ReturnsTheContentType_WhenAUrlIsSpecified() throws Exception { + assertEquals("image/png", ImageUtils.getContentTypeFromDataUri("data:image/png;base64,...")); + assertEquals("image/jpeg", ImageUtils.getContentTypeFromDataUri("data:image/jpeg;base64,...")); + assertEquals("image/svg+xml", ImageUtils.getContentTypeFromDataUri("data:image/svg+xml;utf8,...")); + assertNull(ImageUtils.getContentTypeFromDataUri("data:...")); + } + @Test void getImageAsBase64_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { try { @@ -179,13 +197,14 @@ void validateImage() { ImageUtils.validateImage("image.jpg"); ImageUtils.validateImage("image.jpeg"); ImageUtils.validateImage("image.gif"); + ImageUtils.validateImage("data:image/svg+xml;utf8,iVBORw0KGg"); //disallowed try { - ImageUtils.validateImage("data:image/svg+xml;base64,iVBORw0KGg"); + ImageUtils.validateImage("data:image/other"); fail(); } catch (Exception e) { - assertEquals("Only PNG and JPG data URIs are supported: data:image/svg+xml;base64,iVBORw0KGg", e.getMessage()); + assertEquals("Only PNG and JPG data URIs are supported: data:image/other", e.getMessage()); } } @@ -193,7 +212,7 @@ void validateImage() { void isSupportedDataUri() { assertTrue(ImageUtils.isSupportedDataUri("data:image/png;base64,iVBORw0KGg")); assertTrue(ImageUtils.isSupportedDataUri("data:image/jpeg;base64,iVBORw0KGg")); - assertFalse(ImageUtils.isSupportedDataUri("data:image/svg+xml;base64,iVBORw0KGg")); + assertTrue(ImageUtils.isSupportedDataUri("data:image/svg+xml;utf8, Date: Sat, 25 Feb 2023 15:30:38 +0000 Subject: [PATCH 081/418] Refactor. --- structurizr-core/src/com/structurizr/util/Url.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/util/Url.java b/structurizr-core/src/com/structurizr/util/Url.java index d77ef9f95..de4d7555e 100644 --- a/structurizr-core/src/com/structurizr/util/Url.java +++ b/structurizr-core/src/com/structurizr/util/Url.java @@ -15,7 +15,7 @@ public class Url { * @return true if the URL is valid, false otherwise */ public static boolean isUrl(String urlAsString) { - if (urlAsString != null && urlAsString.trim().length() > 0) { + if (!StringUtils.isNullOrEmpty(urlAsString)) { try { new URL(urlAsString); return true; From 97019b974dcb333d3ff1fa625c362b6e11d0ffff Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 26 Feb 2023 08:17:33 +0000 Subject: [PATCH 082/418] Adds support for element/relationship URLs of the form `{workspace}/...` for linking to diagrams/documentation/decisions in the same workspace. --- docs/changelog.md | 1 + .../src/com/structurizr/model/ModelItem.java | 10 +++++++--- structurizr-core/src/com/structurizr/util/Url.java | 2 ++ .../unit/com/structurizr/model/ModelItemTests.java | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 59fc17e5a..1d947df19 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,7 @@ ## 1.21.0 (unreleased) - __Breaking change__: Removes the concept of "code elements" from `Component`. +- Adds support for element/relationship URLs of the form `{workspace}/...` for linking to diagrams/documentation/decisions in the same workspace. ## 1.20.1 (16th February 2023) diff --git a/structurizr-core/src/com/structurizr/model/ModelItem.java b/structurizr-core/src/com/structurizr/model/ModelItem.java index e95ea9148..4cdf2d304 100644 --- a/structurizr-core/src/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/com/structurizr/model/ModelItem.java @@ -122,10 +122,14 @@ public String getUrl() { public void setUrl(String url) { if (StringUtils.isNullOrEmpty(url)) { this.url = null; - } else if (Url.isUrl(url)) { - this.url = url; } else { - throw new IllegalArgumentException(url + " is not a valid URL."); + if (url.startsWith(Url.WORKSPACE_URL_PREFIX)) { + this.url = url; + } else if (Url.isUrl(url)) { + this.url = url; + } else { + throw new IllegalArgumentException(url + " is not a valid URL."); + } } } diff --git a/structurizr-core/src/com/structurizr/util/Url.java b/structurizr-core/src/com/structurizr/util/Url.java index de4d7555e..49e69ab87 100644 --- a/structurizr-core/src/com/structurizr/util/Url.java +++ b/structurizr-core/src/com/structurizr/util/Url.java @@ -8,6 +8,8 @@ */ public class Url { + public static final String WORKSPACE_URL_PREFIX = "{workspace}"; + /** * Determines whether the supplied string is a valid URL. * diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java index ae9c33852..7013a0227 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java @@ -218,4 +218,18 @@ void addPerspective_ThrowsAnException_WhenTheNamedPerspectiveAlreadyExists() { } } + @Test + void setUrl_AcceptsAUrl() { + Element element = model.addSoftwareSystem("Name"); + element.setUrl("https://structurizr.com"); + assertEquals("https://structurizr.com", element.getUrl()); + } + + @Test + void setUrl_AcceptsAWorkspaceUrl() { + Element element = model.addSoftwareSystem("Name"); + element.setUrl("{workspace}/diagrams#key"); + assertEquals("{workspace}/diagrams#key", element.getUrl()); + } + } \ No newline at end of file From af6268978ca06ac27732cae2eea2edab1392bfbc Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 26 Feb 2023 08:18:25 +0000 Subject: [PATCH 083/418] Adds release date. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 1d947df19..5bb997bc7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.21.0 (unreleased) +## 1.21.0 (26th February 2023) - __Breaking change__: Removes the concept of "code elements" from `Component`. - Adds support for element/relationship URLs of the form `{workspace}/...` for linking to diagrams/documentation/decisions in the same workspace. From ae91d82b80beb3202eb55e851a74e33f05498c08 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 5 Mar 2023 11:53:52 +0000 Subject: [PATCH 084/418] Adds documentation to components. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ .../src/com/structurizr/model/Component.java | 24 ++++++++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5a9142c6b..242af024b 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.21.0' + version = '1.22.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 5bb997bc7..83af10b9e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.22.0 (5th March 2023) + +- Adds documentation to components. + ## 1.21.0 (26th February 2023) - __Breaking change__: Removes the concept of "code elements" from `Component`. diff --git a/structurizr-core/src/com/structurizr/model/Component.java b/structurizr-core/src/com/structurizr/model/Component.java index 06e3e7975..a1b63db61 100644 --- a/structurizr-core/src/com/structurizr/model/Component.java +++ b/structurizr-core/src/com/structurizr/model/Component.java @@ -1,6 +1,8 @@ package com.structurizr.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Documentation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -9,12 +11,14 @@ /** * Represents a "component" in the C4 model. */ -public final class Component extends StaticStructureElement { +public final class Component extends StaticStructureElement implements Documentable { private Container parent; private String technology; + private Documentation documentation = new Documentation(); + Component() { } @@ -67,4 +71,22 @@ public Set getDefaultTags() { return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.COMPONENT)); } + /** + * Gets the documentation associated with this component. + * + * @return a Documentation object + */ + public Documentation getDocumentation() { + return documentation; + } + + /** + * Sets the documentation associated with this component. + * + * @param documentation a Documentation object + */ + void setDocumentation(Documentation documentation) { + this.documentation = documentation; + } + } \ No newline at end of file From 054e58f0480a5036e504daf5ccd638e643927335 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 5 Mar 2023 12:10:43 +0000 Subject: [PATCH 085/418] Removes unused documentation section title property. --- build.gradle | 2 +- docs/changelog.md | 6 ++- .../structurizr/documentation/Decision.java | 14 +++++++ .../documentation/Documentation.java | 10 ----- .../documentation/DocumentationContent.java | 14 ------- .../structurizr/documentation/Section.java | 9 ++--- .../documentation/DocumentationTests.java | 38 ++----------------- .../documentation/SectionTests.java | 3 +- 8 files changed, 28 insertions(+), 68 deletions(-) diff --git a/build.gradle b/build.gradle index 242af024b..9a4737aac 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.22.0' + version = '1.22.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 83af10b9e..5e26f191a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,12 @@ # Changelog +## 1.22.1 (5th March 2023) + +- Removes unused documentation section title property. + ## 1.22.0 (5th March 2023) -- Adds documentation to components. +- Adds documentation to components. ## 1.21.0 (26th February 2023) diff --git a/structurizr-core/src/com/structurizr/documentation/Decision.java b/structurizr-core/src/com/structurizr/documentation/Decision.java index bb0ee574b..96e1fd2ac 100644 --- a/structurizr-core/src/com/structurizr/documentation/Decision.java +++ b/structurizr-core/src/com/structurizr/documentation/Decision.java @@ -12,6 +12,7 @@ public final class Decision extends DocumentationContent { private String id; + private String title; private Date date; private String status; @@ -37,6 +38,19 @@ void setId(String id) { this.id = id; } + /** + * Gets the title. + * + * @return the title, as a String + */ + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + /** * Gets the date of this decision. * diff --git a/structurizr-core/src/com/structurizr/documentation/Documentation.java b/structurizr-core/src/com/structurizr/documentation/Documentation.java index 0155113f5..27a85bcee 100644 --- a/structurizr-core/src/com/structurizr/documentation/Documentation.java +++ b/structurizr-core/src/com/structurizr/documentation/Documentation.java @@ -29,9 +29,7 @@ public Documentation() { * @param section a Section object */ public void addSection(Section section) { - checkTitleIsSpecified(section.getTitle()); checkContentIsSpecified(section.getContent()); - checkSectionIsUnique(section.getTitle()); checkFormatIsSpecified(section.getFormat()); section.setOrder(calculateOrder()); @@ -56,14 +54,6 @@ private void checkFormatIsSpecified(Format format) { } } - private void checkSectionIsUnique(String title) { - for (Section section : sections) { - if (title.equals(section.getTitle())) { - throw new IllegalArgumentException("A section with a title of " + title + " already exists in this scope."); - } - } - } - private int calculateOrder() { return sections.size() + 1; } diff --git a/structurizr-core/src/com/structurizr/documentation/DocumentationContent.java b/structurizr-core/src/com/structurizr/documentation/DocumentationContent.java index 5013ce3a7..1759f81df 100644 --- a/structurizr-core/src/com/structurizr/documentation/DocumentationContent.java +++ b/structurizr-core/src/com/structurizr/documentation/DocumentationContent.java @@ -8,7 +8,6 @@ public abstract class DocumentationContent { // elementId is here for backwards compatibility private String elementId; - private String title; private String content; private Format format; @@ -29,19 +28,6 @@ void setElementId(String elementId) { this.elementId = elementId; } - /** - * Gets the title. - * - * @return the title, as a String - */ - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - /** * Gets the content. * diff --git a/structurizr-core/src/com/structurizr/documentation/Section.java b/structurizr-core/src/com/structurizr/documentation/Section.java index ea5cc0b42..941f3aaba 100644 --- a/structurizr-core/src/com/structurizr/documentation/Section.java +++ b/structurizr-core/src/com/structurizr/documentation/Section.java @@ -11,8 +11,7 @@ public final class Section extends DocumentationContent { public Section() { } - public Section(String title, Format format, String content) { - setTitle(title); + public Section(Format format, String content) { setFormat(format); setContent(content); } @@ -55,16 +54,16 @@ public boolean equals(Object object) { Section section = (Section)object; if (getElementId() != null) { - return getElementId().equals(section.getElementId()) && getTitle().equals(section.getTitle()); + return getElementId().equals(section.getElementId()) && getContent().equals(section.getContent()); } else { - return getTitle().equals(section.getTitle()); + return getContent().equals(section.getContent()); } } @Override public int hashCode() { int result = getElementId() != null ? getElementId().hashCode() : 0; - result = 31 * result + getTitle().hashCode(); + result = 31 * result + getContent().hashCode(); return result; } diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java index ee4ea8957..0526bed2a 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java @@ -15,23 +15,10 @@ public void setUp() { documentation = workspace.getDocumentation(); } - @Test - void addSection_ThrowsAnException_WhenTheTitleIsNotSpecified() { - try { - Section section = new Section(); - - documentation.addSection(section); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A title must be specified.", iae.getMessage()); - } - } - @Test void addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { try { Section section = new Section(); - section.setTitle("Title"); documentation.addSection(section); fail(); @@ -44,7 +31,6 @@ void addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { void addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { try { Section section = new Section(); - section.setTitle("Title"); section.setContent("Content"); documentation.addSection(section); @@ -54,26 +40,9 @@ void addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { } } - @Test - void addSection_ThrowsAnException_WhenASectionExistsWithTheSameTitle() { - try { - Section section = new Section(); - section.setTitle("Title"); - section.setContent("Content"); - section.setFormat(Format.Markdown); - - documentation.addSection(section); - documentation.addSection(section); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A section with a title of Title already exists in this scope.", iae.getMessage()); - } - } - @Test void addSection() { Section section = new Section(); - section.setTitle("Title"); section.setContent("Content"); section.setFormat(Format.Markdown); @@ -81,7 +50,6 @@ void addSection() { assertEquals(1, documentation.getSections().size()); assertTrue(documentation.getSections().contains(section)); - assertEquals("Title", section.getTitle()); assertEquals(Format.Markdown, section.getFormat()); assertEquals("Content", section.getContent()); assertEquals(1, section.getOrder()); @@ -89,9 +57,9 @@ void addSection() { @Test void addSection_IncrementsTheSectionOrderNumber() { - Section section1 = new Section("Title 1", Format.Markdown, "Content"); - Section section2 = new Section("Title 2", Format.Markdown, "Content"); - Section section3 = new Section("Title 3", Format.Markdown, "Content"); + Section section1 = new Section(Format.Markdown, "Content 1"); + Section section2 = new Section(Format.Markdown, "Content 2"); + Section section3 = new Section(Format.Markdown, "Content 3"); documentation.addSection(section1); documentation.addSection(section2); diff --git a/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java b/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java index 282e92c5e..0f1705d44 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java @@ -8,9 +8,8 @@ public class SectionTests { @Test void construction() { - Section section = new Section("Title", Format.Markdown, "Content"); + Section section = new Section(Format.Markdown, "Content"); - assertEquals("Title", section.getTitle()); assertEquals(Format.Markdown, section.getFormat()); assertEquals("Content", section.getContent()); } From fe12158c62634122ff96f57b5974e23cfa9e3906 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 10 Mar 2023 09:11:14 +0000 Subject: [PATCH 086/418] Updates Jackson library dependency. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ structurizr-client/build.gradle | 2 +- structurizr-core/build.gradle | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 9a4737aac..fb84c5120 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.22.1' + version = '1.22.2' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 5e26f191a..45a7bb679 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.22.2 (10th March 2023) + +- Updates Jackson library dependency. + ## 1.22.1 (5th March 2023) - Removes unused documentation section title property. diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 881ac490f..77d8bbcf7 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,7 +2,7 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.14.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.14.2' api 'org.apache.httpcomponents.client5:httpclient5:5.2.1' api 'javax.xml.bind:jaxb-api:2.3.1' diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 9bc594da4..54190e009 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,6 +1,6 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.14.1' + api 'com.fasterxml.jackson.core:jackson-annotations:2.14.2' api 'com.google.code.findbugs:jsr305:3.0.2' api 'commons-logging:commons-logging:1.2' From 864f5de25605827a59cb959613b64d50106647e0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 11 Mar 2023 06:11:41 +0000 Subject: [PATCH 087/418] Adds better backwards compatibility for removal of documentation sections. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ .../api/BackwardsCompatibilityTests.java | 14 ++++++++++++++ .../src/com/structurizr/documentation/Section.java | 12 ++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fb84c5120..813ded0db 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.22.2' + version = '1.22.3' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 45a7bb679..ed55dbae3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.22.3 (11th March 2023) + +- Adds better backwards compatibility for removal of documentation sections. + ## 1.22.2 (10th March 2023) - Updates Jackson library dependency. diff --git a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java index e4177ab26..646f93a86 100644 --- a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java +++ b/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java @@ -1,10 +1,16 @@ package com.structurizr.api; +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.model.Location; import com.structurizr.util.WorkspaceUtils; import org.junit.jupiter.api.Test; import java.io.File; +import static org.junit.jupiter.api.Assertions.assertEquals; + class BackwardsCompatibilityTests { private static final File PATH_TO_WORKSPACE_FILES = new File("test/integration/backwardsCompatibility"); @@ -16,4 +22,12 @@ void test() throws Exception { } } + @Test + void documentation() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + + assertEquals("{\"id\":0,\"name\":\"Name\",\"description\":\"Description\",\"configuration\":{},\"model\":{},\"documentation\":{\"sections\":[{\"content\":\"## Heading 1\",\"format\":\"Markdown\",\"order\":1,\"title\":\"\"}]},\"views\":{\"configuration\":{\"branding\":{},\"styles\":{},\"terminology\":{}}}}", WorkspaceUtils.toJson(workspace, false)); + } + } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/Section.java b/structurizr-core/src/com/structurizr/documentation/Section.java index 941f3aaba..85fc28002 100644 --- a/structurizr-core/src/com/structurizr/documentation/Section.java +++ b/structurizr-core/src/com/structurizr/documentation/Section.java @@ -1,5 +1,8 @@ package com.structurizr.documentation; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonInclude; + /** * A documentation section. */ @@ -16,6 +19,15 @@ public Section(Format format, String content) { setContent(content); } + /** + * This method is retained for backwards compatibility. + */ + @JsonGetter + @JsonInclude(JsonInclude.Include.ALWAYS) + public String getTitle() { + return ""; + } + /** * Gets the filename of this section. * From 39d4a04f35d6ae4bc0b67134a623b70408226e85 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 11 Mar 2023 08:16:55 +0000 Subject: [PATCH 088/418] Deprecates Enterprise and Location concepts. --- build.gradle | 2 +- docs/changelog.md | 4 ++ .../api/BackwardsCompatibilityTests.java | 17 ++++++ .../src/com/structurizr/model/Enterprise.java | 1 + .../src/com/structurizr/model/Model.java | 39 ++++++++++++-- .../structurizr/view/SystemContextView.java | 2 + .../structurizr/view/SystemLandscapeView.java | 5 +- .../com/structurizr/model/ComponentTests.java | 2 +- .../model/ContainerInstanceTests.java | 2 +- .../com/structurizr/model/ContainerTests.java | 2 +- .../com/structurizr/model/ModelTests.java | 28 +++++----- .../structurizr/model/RelationshipTests.java | 4 +- .../model/SoftwareSystemInstanceTests.java | 2 +- .../model/SoftwareSystemTests.java | 30 +++++------ .../structurizr/view/ComponentViewTests.java | 20 +++---- .../structurizr/view/ContainerViewTests.java | 22 ++++---- .../structurizr/view/ElementViewTests.java | 5 +- .../view/SystemContextViewTests.java | 18 +++---- .../view/SystemLandscapeViewTests.java | 26 ++-------- .../unit/com/structurizr/view/ViewTests.java | 52 +++++++++---------- 20 files changed, 157 insertions(+), 126 deletions(-) diff --git a/build.gradle b/build.gradle index 813ded0db..cf3b08436 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.22.3' + version = '1.23.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index ed55dbae3..6554a4f65 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.23.0 (unreleased) + +- Deprecates `Enterprise` and `Location` concepts. + ## 1.22.3 (11th March 2023) - Adds better backwards compatibility for removal of documentation sections. diff --git a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java index 646f93a86..7f30cc635 100644 --- a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java +++ b/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java @@ -22,6 +22,23 @@ void test() throws Exception { } } + @Test + void enterprise_and_location() throws Exception { + File file = new File(PATH_TO_WORKSPACE_FILES, "structurizr-36141-workspace.json"); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(file); + + assertEquals("Big Bank plc", workspace.getModel().getEnterprise().getName()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); + assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); + + // make sure enterprise and location information is not lost when going to/from JSON + workspace = WorkspaceUtils.fromJson(WorkspaceUtils.toJson(workspace, false)); + + assertEquals("Big Bank plc", workspace.getModel().getEnterprise().getName()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); + assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); + } + @Test void documentation() throws Exception { Workspace workspace = new Workspace("Name", "Description"); diff --git a/structurizr-core/src/com/structurizr/model/Enterprise.java b/structurizr-core/src/com/structurizr/model/Enterprise.java index a5a6c0257..5a4b58f2a 100644 --- a/structurizr-core/src/com/structurizr/model/Enterprise.java +++ b/structurizr-core/src/com/structurizr/model/Enterprise.java @@ -16,6 +16,7 @@ public final class Enterprise { * @param name the name, as a String * @throws IllegalArgumentException if the name is not specified */ + @Deprecated public Enterprise(String name) { if (name == null || name.trim().length() == 0) { throw new IllegalArgumentException("Name must be specified."); diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/com/structurizr/model/Model.java index b97c0d23c..3c81fdfd9 100644 --- a/structurizr-core/src/com/structurizr/model/Model.java +++ b/structurizr-core/src/com/structurizr/model/Model.java @@ -45,6 +45,7 @@ public Enterprise getEnterprise() { * * @param enterprise an Enterprise instance */ + @Deprecated public void setEnterprise(Enterprise enterprise) { this.enterprise = enterprise; } @@ -61,7 +62,7 @@ public SoftwareSystem addSoftwareSystem(@Nonnull String name) { } /** - * Creates a software system (with an unspecified location) and adds it to the model. + * Creates a software system and adds it to the model. * * @param name the name of the software system * @param description a short description of the software system @@ -69,7 +70,21 @@ public SoftwareSystem addSoftwareSystem(@Nonnull String name) { * @throws IllegalArgumentException if a software system with the same name already exists */ public SoftwareSystem addSoftwareSystem(@Nonnull String name, @Nullable String description) { - return addSoftwareSystem(Location.Unspecified, name, description); + if (getSoftwareSystemWithName(name) == null) { + SoftwareSystem softwareSystem = new SoftwareSystem(); + softwareSystem.setLocation(Location.Unspecified); + softwareSystem.setName(name); + softwareSystem.setDescription(description); + + softwareSystems.add(softwareSystem); + + softwareSystem.setId(idGenerator.generateId(softwareSystem)); + addElementToInternalStructures(softwareSystem); + + return softwareSystem; + } else { + throw new IllegalArgumentException("A top-level element named '" + name + "' already exists."); + } } /** @@ -82,6 +97,7 @@ public SoftwareSystem addSoftwareSystem(@Nonnull String name, @Nullable String d * @throws IllegalArgumentException if a software system with the same name already exists */ @Nonnull + @Deprecated public SoftwareSystem addSoftwareSystem(@Nullable Location location, @Nonnull String name, @Nullable String description) { if (getSoftwareSystemWithName(name) == null) { SoftwareSystem softwareSystem = new SoftwareSystem(); @@ -113,7 +129,7 @@ public Person addPerson(@Nonnull String name) { } /** - * Creates a person (with an unspecified location) and adds it to the model. + * Creates a person and adds it to the model. * * @param name the name of the person (e.g. "Admin User" or "Bob the Business User") * @param description a short description of the person @@ -122,7 +138,21 @@ public Person addPerson(@Nonnull String name) { */ @Nonnull public Person addPerson(@Nonnull String name, @Nullable String description) { - return addPerson(Location.Unspecified, name, description); + if (getPersonWithName(name) == null) { + Person person = new Person(); + person.setLocation(Location.Unspecified); + person.setName(name); + person.setDescription(description); + + people.add(person); + + person.setId(idGenerator.generateId(person)); + addElementToInternalStructures(person); + + return person; + } else { + throw new IllegalArgumentException("A top-level element named '" + name + "' already exists."); + } } /** @@ -135,6 +165,7 @@ public Person addPerson(@Nonnull String name, @Nullable String description) { * @throws IllegalArgumentException if a person with the same name already exists */ @Nonnull + @Deprecated public Person addPerson(Location location, @Nonnull String name, @Nullable String description) { if (getPersonWithName(name) == null) { Person person = new Person(); diff --git a/structurizr-core/src/com/structurizr/view/SystemContextView.java b/structurizr-core/src/com/structurizr/view/SystemContextView.java index 7d4ef11a2..a96595596 100644 --- a/structurizr-core/src/com/structurizr/view/SystemContextView.java +++ b/structurizr-core/src/com/structurizr/view/SystemContextView.java @@ -77,6 +77,7 @@ public void addNearestNeighbours(@Nonnull Element element) { * * @return true if the enterprise boundary is visible, false otherwise */ + @Deprecated public boolean isEnterpriseBoundaryVisible() { return enterpriseBoundaryVisible; } @@ -86,6 +87,7 @@ public boolean isEnterpriseBoundaryVisible() { * * @param enterpriseBoundaryVisible true if the enterprise boundary should be visible, false otherwise */ + @Deprecated public void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { this.enterpriseBoundaryVisible = enterpriseBoundaryVisible; } diff --git a/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java b/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java index 4cfbcd23d..d040911fe 100644 --- a/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java +++ b/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java @@ -31,8 +31,7 @@ public final class SystemLandscapeView extends StaticView { */ @Override public String getName() { - Enterprise enterprise = model.getEnterprise(); - return "System Landscape" + (enterprise != null && enterprise.getName().trim().length() > 0 ? " for " + enterprise.getName() : ""); + return "System Landscape"; } /** @@ -94,6 +93,7 @@ public void addNearestNeighbours(@Nonnull Element element) { * * @return true if the enterprise boundary is visible, false otherwise */ + @Deprecated public boolean isEnterpriseBoundaryVisible() { return enterpriseBoundaryVisible; } @@ -103,6 +103,7 @@ public boolean isEnterpriseBoundaryVisible() { * * @param enterpriseBoundaryVisible true if the enterprise boundary should be visible, false otherwise */ + @Deprecated public void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { this.enterpriseBoundaryVisible = enterpriseBoundaryVisible; } diff --git a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java index fa19b0dee..d38adc7ba 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java @@ -7,7 +7,7 @@ public class ComponentTests extends AbstractWorkspaceTestBase { - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System", "Description"); + private SoftwareSystem softwareSystem = model.addSoftwareSystem("System", "Description"); private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); @Test diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java index 445a11f22..1f0b150b7 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java @@ -7,7 +7,7 @@ public class ContainerInstanceTests extends AbstractWorkspaceTestBase { - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System", "Description"); + private SoftwareSystem softwareSystem = model.addSoftwareSystem("System", "Description"); private Container database = softwareSystem.addContainer("Database Schema", "Stores data", "MySQL"); private DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java index 159f40805..f9599c2c1 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java @@ -7,7 +7,7 @@ public class ContainerTests extends AbstractWorkspaceTestBase { - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System", "Description"); + private SoftwareSystem softwareSystem = model.addSoftwareSystem("System", "Description"); private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); @Test diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java index 3407988e6..36454db16 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java @@ -40,10 +40,9 @@ void addPerson_ThrowsAnException_WhenAnEmptyNameIsSpecified() { @Test void addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { assertTrue(model.getSoftwareSystems().isEmpty()); - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Some description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); - assertEquals(Location.External, softwareSystem.getLocation()); assertEquals("System A", softwareSystem.getName()); assertEquals("Some description", softwareSystem.getDescription()); assertEquals("1", softwareSystem.getId()); @@ -52,11 +51,11 @@ void addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWith @Test void addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSameName() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Some description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); try { - model.addSoftwareSystem(Location.External, "System A", "Description"); + model.addSoftwareSystem("System A", "Description"); fail(); } catch (Exception e) { assertEquals("A top-level element named 'System A' already exists.", e.getMessage()); @@ -69,7 +68,6 @@ void addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftw SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); assertEquals(1, model.getSoftwareSystems().size()); - assertEquals(Location.Unspecified, softwareSystem.getLocation()); assertEquals("System A", softwareSystem.getName()); assertEquals("Some description", softwareSystem.getDescription()); assertEquals("1", softwareSystem.getId()); @@ -79,10 +77,9 @@ void addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftw @Test void addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { assertTrue(model.getPeople().isEmpty()); - Person person = model.addPerson(Location.Internal, "Some internal user", "Some description"); + Person person = model.addPerson("Some internal user", "Some description"); assertEquals(1, model.getPeople().size()); - assertEquals(Location.Internal, person.getLocation()); assertEquals("Some internal user", person.getName()); assertEquals("Some description", person.getDescription()); assertEquals("1", person.getId()); @@ -91,11 +88,11 @@ void addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { @Test void addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { - Person person = model.addPerson(Location.Internal, "Admin User", "Description"); + Person person = model.addPerson("Admin User", "Description"); assertEquals(1, model.getPeople().size()); try { - model.addPerson(Location.External, "Admin User", "Description"); + model.addPerson("Admin User", "Description"); fail(); } catch (Exception e) { assertEquals("A top-level element named 'Admin User' already exists.", e.getMessage()); @@ -108,7 +105,6 @@ void addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNotExist Person person = model.addPerson("Some internal user", "Some description"); assertEquals(1, model.getPeople().size()); - assertEquals(Location.Unspecified, person.getLocation()); assertEquals("Some internal user", person.getName()); assertEquals("Some description", person.getDescription()); assertEquals("1", person.getId()); @@ -122,20 +118,20 @@ void getElement_ReturnsNull_WhenAnElementWithTheSpecifiedIdDoesNotExist() { @Test void getElement_ReturnsAnElement_WhenAnElementWithTheSpecifiedIdDoesExist() { - Person person = model.addPerson(Location.Internal, "Name", "Description"); + Person person = model.addPerson("Name", "Description"); assertSame(person, model.getElement(person.getId())); } @Test void contains_ReturnsFalse_WhenTheSpecifiedElementIsNotInTheModel() { Model newModel = new Model(); - SoftwareSystem softwareSystem = newModel.addSoftwareSystem(Location.Unspecified, "Name", "Description"); + SoftwareSystem softwareSystem = newModel.addSoftwareSystem("Name", "Description"); assertFalse(model.contains(softwareSystem)); } @Test void contains_ReturnsTrue_WhenTheSpecifiedElementIsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Unspecified, "Name", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); assertTrue(model.contains(softwareSystem)); } @@ -146,7 +142,7 @@ void getSoftwareSystemWithName_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedNa @Test void getSoftwareSystemWithName_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedNameExists() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Description"); assertSame(softwareSystem, model.getSoftwareSystemWithName("System A")); } @@ -177,7 +173,7 @@ void getSoftwareSystemWithId_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedIdDo @Test void getSoftwareSystemWithId_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedIdDoesExist() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Description"); assertSame(softwareSystem, model.getSoftwareSystemWithId(softwareSystem.getId())); } @@ -188,7 +184,7 @@ void getPersonWithName_ReturnsNull_WhenAPersonWithTheSpecifiedNameDoesNotExist() @Test void getPersonWithName_ReturnsAPerson_WhenAPersonWithTheSpecifiedNameExists() { - Person person = model.addPerson(Location.External, "Admin User", "Description"); + Person person = model.addPerson("Admin User", "Description"); assertSame(person, model.getPersonWithName("Admin User")); } diff --git a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java index f596d09fb..be7c631dd 100644 --- a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java @@ -12,8 +12,8 @@ public class RelationshipTests extends AbstractWorkspaceTestBase { @BeforeEach public void setUp() { - softwareSystem1 = model.addSoftwareSystem(Location.Internal, "Name1", "Description"); - softwareSystem2 = model.addSoftwareSystem(Location.Internal, "Name2", "Description"); + softwareSystem1 = model.addSoftwareSystem("Name1", "Description"); + softwareSystem2 = model.addSoftwareSystem("Name2", "Description"); } @Test diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java index e7019e231..6795bee01 100644 --- a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java @@ -7,7 +7,7 @@ public class SoftwareSystemInstanceTests extends AbstractWorkspaceTestBase { - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System", "Description"); + private SoftwareSystem softwareSystem = model.addSoftwareSystem("System", "Description"); private DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); @Test diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java index 03f0e9c2d..51b97f3e7 100644 --- a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java @@ -9,7 +9,7 @@ public class SoftwareSystemTests extends AbstractWorkspaceTestBase { - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "Name", "Description"); + private SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); @Test void addContainer_ThrowsAnException_WhenANullNameIsSpecified() { @@ -73,8 +73,8 @@ void GetContainerWithId_ReturnsAContainer_WhenAContainerWithTheSpecifiedIdDoesEx @Test void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { - SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); - SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); + SoftwareSystem systemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem systemB = model.addSoftwareSystem("System B", "Description"); systemA.uses(systemB, "Gets some data from"); assertEquals(1, systemA.getRelationships().size()); @@ -87,8 +87,8 @@ void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { @Test void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADifferentRelationshipAlreadyExists() { - SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); - SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); + SoftwareSystem systemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem systemB = model.addSoftwareSystem("System B", "Description"); systemA.uses(systemB, "Gets data using the REST API"); systemA.uses(systemB, "Subscribes to updates using the Streaming API"); @@ -107,8 +107,8 @@ void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADifferen @Test void uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenTheSameRelationshipAlreadyExists() { - SoftwareSystem systemA = model.addSoftwareSystem(Location.Internal, "System A", "Description"); - SoftwareSystem systemB = model.addSoftwareSystem(Location.Internal, "System B", "Description"); + SoftwareSystem systemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem systemB = model.addSoftwareSystem("System B", "Description"); systemA.uses(systemB, "Gets data using the REST API"); systemA.uses(systemB, "Gets data using the REST API"); @@ -117,8 +117,8 @@ void uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenThe @Test void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson() { - SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); - Person person = model.addPerson(Location.Internal, "User", "Description"); + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); + Person person = model.addPerson("User", "Description"); system.delivers(person, "E-mails results to"); assertEquals(1, system.getRelationships().size()); @@ -131,8 +131,8 @@ void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson() @Test void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenADifferentRelationshipAlreadyExists() { - SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); - Person person = model.addPerson(Location.Internal, "User", "Description"); + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); + Person person = model.addPerson("User", "Description"); system.delivers(person, "E-mails results to"); system.delivers(person, "Text messages results to"); @@ -152,8 +152,8 @@ void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_Wh @Test void delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenTheSameRelationshipAlreadyExists() { - SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); - Person person = model.addPerson(Location.Internal, "User", "Description"); + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); + Person person = model.addPerson("User", "Description"); system.delivers(person, "E-mails results to"); system.delivers(person, "E-mails results to"); @@ -162,13 +162,13 @@ void delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAndAPer @Test void getTags_IncludesSoftwareSystemByDefault() { - SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); assertEquals("Element,Software System", system.getTags()); } @Test void getCanonicalName() { - SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); assertEquals("SoftwareSystem://System", system.getCanonicalName()); } diff --git a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java index ddf7daf9f..9c80529d7 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java @@ -18,7 +18,7 @@ public class ComponentViewTests extends AbstractWorkspaceTestBase { @BeforeEach public void setUp() { - softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + softwareSystem = model.addSoftwareSystem("The System", "Description"); webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); view = new ComponentView(webApplication, "Key", "Some description"); } @@ -43,8 +43,8 @@ void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { @Test void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); view.addAllSoftwareSystems(); @@ -62,8 +62,8 @@ void addAllPeople_DoesNothing_WhenThereAreNoPeople() { @Test void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); view.addAllPeople(); @@ -81,10 +81,10 @@ void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { @Test void addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndComponents_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersAndComponentsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); Container database = softwareSystem.addContainer("Database", "Does something", "MySQL"); Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); @@ -264,7 +264,7 @@ void add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { @Test void add_DoesNothing_WhenTheContainerOfTheViewIsAddedViaDependency() { - final SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "Some other system", "external system that uses our web application"); + final SoftwareSystem softwareSystem = model.addSoftwareSystem("Some other system", "external system that uses our web application"); final Relationship relationshipFromExternalSystem = softwareSystem.uses(webApplication, ""); diff --git a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java index fb97520f8..356cbd0dc 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java @@ -14,7 +14,7 @@ public class ContainerViewTests extends AbstractWorkspaceTestBase { @BeforeEach public void setUp() { - softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + softwareSystem = model.addSoftwareSystem("The System", "Description"); view = new ContainerView(softwareSystem, "containers", "Description"); } @@ -37,8 +37,8 @@ void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { @Test void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); view.addAllSoftwareSystems(); @@ -56,8 +56,8 @@ void addAllPeople_DoesNothing_WhenThereAreNoPeople() { @Test void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); view.addAllPeople(); @@ -75,10 +75,10 @@ void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { @Test void addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); Container webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); Container database = softwareSystem.addContainer("Database", "Does something", "MySQL"); @@ -236,7 +236,7 @@ void addDependentSoftwareSystem() { view.addDependentSoftwareSystems(); - SoftwareSystem softwareSystem2 = model.addSoftwareSystem(Location.External, "SoftwareSystem 2", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("SoftwareSystem 2", ""); view.addDependentSoftwareSystems(); assertEquals(0, view.getElements().size()); @@ -252,7 +252,7 @@ void addDependentSoftwareSystem() { void addDependentSoftwareSystem2() { Container container1a = softwareSystem.addContainer("Container 1A", "", ""); - SoftwareSystem softwareSystem2 = model.addSoftwareSystem(Location.External, "SoftwareSystem 2", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("SoftwareSystem 2", ""); Container container2a = softwareSystem2.addContainer("Container 2-A", "", ""); model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java index 497131c33..661308e4e 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java @@ -2,7 +2,6 @@ import com.structurizr.AbstractWorkspaceTestBase; import com.structurizr.model.Element; -import com.structurizr.model.Location; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -11,14 +10,14 @@ public class ElementViewTests extends AbstractWorkspaceTestBase { @Test void copyLayoutInformationFrom_DoesNothing_WhenNullIsPassed() { - Element element = model.addSoftwareSystem(Location.External, "SystemA", ""); + Element element = model.addSoftwareSystem("SystemA", ""); ElementView elementView = new ElementView(element); elementView.copyLayoutInformationFrom(null); } @Test void copyLayoutInformationFrom_CopiesXAndY_WhenANonNullElementViewIsPassed() { - Element element = model.addSoftwareSystem(Location.External, "SystemA", ""); + Element element = model.addSoftwareSystem("SystemA", ""); ElementView elementView1 = new ElementView(element); assertEquals(0, elementView1.getX()); assertEquals(0, elementView1.getY()); diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java index 2c68a8e72..9de221b2f 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java @@ -14,7 +14,7 @@ public class SystemContextViewTests extends AbstractWorkspaceTestBase { @BeforeEach public void setUp() { - softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + softwareSystem = model.addSoftwareSystem( "The System", "Description"); view = new SystemContextView(softwareSystem, "context", "Description"); } @@ -37,8 +37,8 @@ void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { @Test void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); view.addAllSoftwareSystems(); @@ -57,8 +57,8 @@ void addAllPeople_DoesNothing_WhenThereAreNoPeople() { @Test void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); + Person userA = model.addPerson( "User A", "Description"); + Person userB = model.addPerson( "User B", "Description"); view.addAllPeople(); @@ -77,10 +77,10 @@ void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { @Test void addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + Person userA = model.addPerson( "User A", "Description"); + Person userB = model.addPerson( "User B", "Description"); view.addAllElements(); diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java b/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java index 69b922957..dade9b288 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java @@ -24,30 +24,10 @@ void construction() { } @Test - void getName_WhenNoEnterpriseIsSpecified() { + void getName() { assertEquals("System Landscape", view.getName()); } - @Test - void getName_WhenAnEnterpriseIsSpecified() { - model.setEnterprise(new Enterprise("Widgets Limited")); - assertEquals("System Landscape for Widgets Limited", view.getName()); - } - - @Test - void getName_WhenAnEmptyEnterpriseNameIsSpecified() { - assertThrows(IllegalArgumentException.class, () -> { - model.setEnterprise(new Enterprise("")); - }); - } - - @Test - void getName_WhenANullEnterpriseNameIsSpecified() { - assertThrows(IllegalArgumentException.class, () -> { - model.setEnterprise(new Enterprise(null)); - }); - } - @Test void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { view.addAllSoftwareSystems(); @@ -56,8 +36,8 @@ void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { @Test void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); view.addAllSoftwareSystems(); diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ViewTests.java index 8017e46db..b4cb96bcd 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ViewTests.java @@ -14,7 +14,7 @@ public class ViewTests extends AbstractWorkspaceTestBase { @Test void construction() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "key", "Description"); assertEquals("key", view.getKey()); assertEquals("Description", view.getDescription()); @@ -30,7 +30,7 @@ void construction_WhenTheViewKeyContainsAForwardSlashCharacter() { @Test void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); assertEquals(1, view.getElements().size()); view.addAllSoftwareSystems(); @@ -39,10 +39,10 @@ void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsInTheMo @Test void addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.Unspecified, "System B", "Description"); - SoftwareSystem softwareSystemC = model.addSoftwareSystem(Location.Unspecified, "System C", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + SoftwareSystem softwareSystemC = model.addSoftwareSystem("System C", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); view.addAllSoftwareSystems(); @@ -57,7 +57,7 @@ void addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSystems @Test void addSoftwareSystem_ThrowsAnException_WhenGivenNull() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); try { @@ -70,8 +70,8 @@ void addSoftwareSystem_ThrowsAnException_WhenGivenNull() { @Test void addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); view.add(softwareSystemA); @@ -83,7 +83,7 @@ void addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() @Test void addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); assertEquals(1, view.getElements().size()); @@ -94,10 +94,10 @@ void addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { @Test void addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); - Person person2 = model.addPerson(Location.Unspecified, "Person 2", "Description"); - Person person3 = model.addPerson(Location.Unspecified, "Person 3", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + Person person1 = model.addPerson("Person 1", "Description"); + Person person2 = model.addPerson("Person 2", "Description"); + Person person3 = model.addPerson("Person 3", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); view.addAllPeople(); @@ -112,7 +112,7 @@ void addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { @Test void addPerson_ThrowsAnException_WhenGivenNull() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); try { view.add((Person) null); @@ -124,10 +124,10 @@ void addPerson_ThrowsAnException_WhenGivenNull() { @Test void addPerson_AddsThePerson_WhenThPersonIsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); + Person person1 = model.addPerson("Person 1", "Description"); view.add(person1); assertEquals(2, view.getElements().size()); @@ -138,8 +138,8 @@ void addPerson_AddsThePerson_WhenThPersonIsInTheModel() { @Test void removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelationshipsBetweenElements() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "Software System", "Description"); - Person person = model.addPerson(Location.Unspecified, "Person", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Person person = model.addPerson("Person", "Description"); StaticView view = views.createSystemLandscapeView("context", "Description"); view.addAllSoftwareSystems(); @@ -151,11 +151,11 @@ void removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelati @Test void removeElementsWithNoRelationships_RemovesOnlyThoseElementsWithoutRelationships_WhenTheViewContainsSomeUnlinkedElements() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.Unspecified, "System B", "Description"); - Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); - Person person2 = model.addPerson(Location.Unspecified, "Person 2", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + Person person1 = model.addPerson("Person 1", "Description"); + Person person2 = model.addPerson("Person 2", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); softwareSystem.uses(softwareSystemA, "uses"); @@ -257,14 +257,14 @@ void copyLayoutInformationFrom() { @Test void getName() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); SystemContextView systemContextView = new SystemContextView(softwareSystem, "context", "Description"); assertEquals("The System - System Context", systemContextView.getName()); } @Test void removeElementsThatAreUnreachableFrom_DoesNothing_WhenANullElementIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); StaticView view = new SystemContextView(softwareSystem, "context", "Description"); view.removeElementsThatAreUnreachableFrom(null); } From 4c001b40c1b6f60792ff9f45911f20c426f5f175 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 11 Mar 2023 08:24:58 +0000 Subject: [PATCH 089/418] Deprecates Enterprise concept. --- structurizr-core/src/com/structurizr/view/Terminology.java | 1 + 1 file changed, 1 insertion(+) diff --git a/structurizr-core/src/com/structurizr/view/Terminology.java b/structurizr-core/src/com/structurizr/view/Terminology.java index 6233ff6c6..f9c0fbc2b 100644 --- a/structurizr-core/src/com/structurizr/view/Terminology.java +++ b/structurizr-core/src/com/structurizr/view/Terminology.java @@ -40,6 +40,7 @@ public String getEnterprise() { return enterprise; } + @Deprecated public void setEnterprise(String enterprise) { this.enterprise = enterprise; } From 1074d96c8e2ed758a52c187f9bdc5e7757d845c5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 11 Mar 2023 12:25:13 +0000 Subject: [PATCH 090/418] Adds properties to the model. --- docs/changelog.md | 1 + .../src/com/structurizr/model/Model.java | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 6554a4f65..a8805bf47 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,7 @@ ## 1.23.0 (unreleased) - Deprecates `Enterprise` and `Location` concepts. +- Adds properties to the model. ## 1.22.3 (11th March 2023) diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/com/structurizr/model/Model.java index 3c81fdfd9..04479e9a9 100644 --- a/structurizr-core/src/com/structurizr/model/Model.java +++ b/structurizr-core/src/com/structurizr/model/Model.java @@ -1,6 +1,7 @@ package com.structurizr.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.PropertyHolder; import com.structurizr.WorkspaceValidationException; import javax.annotation.Nonnull; @@ -11,7 +12,7 @@ /** * Represents a software architecture model, into which all model elements are added. */ -public final class Model { +public final class Model implements PropertyHolder { private IdGenerator idGenerator = new SequentialIntegerIdGeneratorStrategy(); @@ -27,6 +28,8 @@ public final class Model { private ImpliedRelationshipsStrategy impliedRelationshipsStrategy = new DefaultImpliedRelationshipsStrategy(); + private Map properties = new HashMap<>(); + Model() { } @@ -1065,4 +1068,37 @@ public void setImpliedRelationshipsStrategy(ImpliedRelationshipsStrategy implied } } + /** + * Gets the collection of name-value property pairs, as a Map. + * + * @return a Map (String, String) (empty if there are no properties) + */ + public Map getProperties() { + return new HashMap<>(properties); + } + + /** + * Adds a name-value pair property. + * + * @param name the name of the property + * @param value the value of the property + */ + public void addProperty(String name, String value) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A property name must be specified."); + } + + if (value == null || value.trim().length() == 0) { + throw new IllegalArgumentException("A property value must be specified."); + } + + properties.put(name, value); + } + + void setProperties(Map properties) { + if (properties != null) { + this.properties = new HashMap<>(properties); + } + } + } \ No newline at end of file From df577430fd372d653b417fae3a68dc2788a94e2b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 11 Mar 2023 16:33:11 +0000 Subject: [PATCH 091/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index a8805bf47..6d2c79c40 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.23.0 (unreleased) +## 1.23.0 (11th March 2023) - Deprecates `Enterprise` and `Location` concepts. - Adds properties to the model. From f069caac1c142738a2a409df9fa494c252634921 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 12 Mar 2023 09:20:57 +0000 Subject: [PATCH 092/418] Adds constant. --- structurizr-core/src/com/structurizr/model/Constants.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 structurizr-core/src/com/structurizr/model/Constants.java diff --git a/structurizr-core/src/com/structurizr/model/Constants.java b/structurizr-core/src/com/structurizr/model/Constants.java new file mode 100644 index 000000000..54a2aa8b2 --- /dev/null +++ b/structurizr-core/src/com/structurizr/model/Constants.java @@ -0,0 +1,7 @@ +package com.structurizr.model; + +public final class Constants { + + public static final String GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator"; + +} \ No newline at end of file From 47c4a9fffecfb414b95c0edb7b58b95e7c00b391 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 12 Mar 2023 14:30:26 +0000 Subject: [PATCH 093/418] Deprecates the `setExternalBoundariesVisible` methods on `ContainerView`, `ComponentView`, and `DynamicView`. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ structurizr-core/src/com/structurizr/view/ComponentView.java | 1 + structurizr-core/src/com/structurizr/view/ContainerView.java | 1 + structurizr-core/src/com/structurizr/view/DynamicView.java | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cf3b08436..2745db2e8 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.23.0' + version = '1.23.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 6d2c79c40..ef52c066f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.23.1 (unreleased) + +- Deprecates the `setExternalBoundariesVisible` methods on `ContainerView`, `ComponentView`, and `DynamicView`. + ## 1.23.0 (11th March 2023) - Deprecates `Enterprise` and `Location` concepts. diff --git a/structurizr-core/src/com/structurizr/view/ComponentView.java b/structurizr-core/src/com/structurizr/view/ComponentView.java index 4dcf9a35c..f3eb65b7a 100644 --- a/structurizr-core/src/com/structurizr/view/ComponentView.java +++ b/structurizr-core/src/com/structurizr/view/ComponentView.java @@ -308,6 +308,7 @@ public boolean getExternalContainerBoundariesVisible() { * * @param externalContainerBoundariesVisible true if external container boundaries should be visible, false otherwise */ + @Deprecated public void setExternalSoftwareSystemBoundariesVisible(boolean externalContainerBoundariesVisible) { this.externalContainerBoundariesVisible = externalContainerBoundariesVisible; } diff --git a/structurizr-core/src/com/structurizr/view/ContainerView.java b/structurizr-core/src/com/structurizr/view/ContainerView.java index 8c8cad219..3536b96fd 100644 --- a/structurizr-core/src/com/structurizr/view/ContainerView.java +++ b/structurizr-core/src/com/structurizr/view/ContainerView.java @@ -214,6 +214,7 @@ public boolean getExternalSoftwareSystemBoundariesVisible() { * * @param externalSoftwareSystemBoundariesVisible true if external software system boundaries should be visible, false otherwise */ + @Deprecated public void setExternalSoftwareSystemBoundariesVisible(boolean externalSoftwareSystemBoundariesVisible) { this.externalSoftwareSystemBoundariesVisible = externalSoftwareSystemBoundariesVisible; } diff --git a/structurizr-core/src/com/structurizr/view/DynamicView.java b/structurizr-core/src/com/structurizr/view/DynamicView.java index 6dbb48908..80f220a03 100644 --- a/structurizr-core/src/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/com/structurizr/view/DynamicView.java @@ -386,6 +386,7 @@ public boolean getExternalBoundariesVisible() { * * @param externalBoundariesVisible true if external boundaries should be visible, false otherwise */ + @Deprecated public void setExternalBoundariesVisible(boolean externalBoundariesVisible) { this.externalBoundariesVisible = externalBoundariesVisible; } From 64f4209ddbd5d3f194e651d71f8ce5506ed7c85b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 14 Mar 2023 09:00:04 +0000 Subject: [PATCH 094/418] Typo. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index ef52c066f..febeed413 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,7 +11,7 @@ ## 1.22.3 (11th March 2023) -- Adds better backwards compatibility for removal of documentation sections. +- Adds better backwards compatibility for removal of documentation section titles. ## 1.22.2 (10th March 2023) From 87dc67754b644450d85e0a0ccfaca833acac2efe Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 15 Mar 2023 12:53:13 +0000 Subject: [PATCH 095/418] Removes the check for empty content when adding a documentation section. --- docs/changelog.md | 1 + .../documentation/Documentation.java | 21 +++++++--------- .../structurizr/documentation/Section.java | 25 ------------------- .../documentation/DocumentationTests.java | 12 --------- 4 files changed, 10 insertions(+), 49 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index febeed413..ede3dba00 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,7 @@ ## 1.23.1 (unreleased) - Deprecates the `setExternalBoundariesVisible` methods on `ContainerView`, `ComponentView`, and `DynamicView`. +- Removes the check for empty content when adding a documentation section. ## 1.23.0 (11th March 2023) diff --git a/structurizr-core/src/com/structurizr/documentation/Documentation.java b/structurizr-core/src/com/structurizr/documentation/Documentation.java index 27a85bcee..8bbcf3baf 100644 --- a/structurizr-core/src/com/structurizr/documentation/Documentation.java +++ b/structurizr-core/src/com/structurizr/documentation/Documentation.java @@ -3,20 +3,18 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.structurizr.util.StringUtils; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; /** - * Represents the documentation within a workspace or software system - a collection of - * content in Markdown or AsciiDoc format, optionally with attached images. + * Represents the documentation within a workspace, software system, container, or component; + * a collection of content in Markdown or AsciiDoc format, optionally with attached images. * * See Documentation * on the Structurizr website for more details. */ public final class Documentation { - private Set

sections = new HashSet<>(); + private List
sections = new ArrayList<>(); private Set decisions = new HashSet<>(); private Set images = new HashSet<>(); @@ -29,7 +27,6 @@ public Documentation() { * @param section a Section object */ public void addSection(Section section) { - checkContentIsSpecified(section.getContent()); checkFormatIsSpecified(section.getFormat()); section.setOrder(calculateOrder()); @@ -63,13 +60,13 @@ private int calculateOrder() { * * @return a Set of {@link Section} objects */ - public Set
getSections() { - return new HashSet<>(sections); + public Collection
getSections() { + return new ArrayList<>(sections); } - void setSections(Set
sections) { + void setSections(Collection
sections) { if (sections != null) { - this.sections = new LinkedHashSet<>(sections); + this.sections = new ArrayList<>(sections); } } @@ -157,7 +154,7 @@ public boolean isEmpty() { * Removes all documentation, decisions, and images. */ public void clear() { - sections = new HashSet<>(); + sections = new ArrayList<>(); decisions = new HashSet<>(); images = new HashSet<>(); } diff --git a/structurizr-core/src/com/structurizr/documentation/Section.java b/structurizr-core/src/com/structurizr/documentation/Section.java index 85fc28002..007e53fa3 100644 --- a/structurizr-core/src/com/structurizr/documentation/Section.java +++ b/structurizr-core/src/com/structurizr/documentation/Section.java @@ -54,29 +54,4 @@ void setOrder(int order) { this.order = order; } - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - - if (object == null || getClass() != object.getClass()) { - return false; - } - - Section section = (Section)object; - if (getElementId() != null) { - return getElementId().equals(section.getElementId()) && getContent().equals(section.getContent()); - } else { - return getContent().equals(section.getContent()); - } - } - - @Override - public int hashCode() { - int result = getElementId() != null ? getElementId().hashCode() : 0; - result = 31 * result + getContent().hashCode(); - return result; - } - } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java index 0526bed2a..187a85870 100644 --- a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java +++ b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java @@ -15,18 +15,6 @@ public void setUp() { documentation = workspace.getDocumentation(); } - @Test - void addSection_ThrowsAnException_WhenTheContentIsNotSpecified() { - try { - Section section = new Section(); - - documentation.addSection(section); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("Content must be specified.", iae.getMessage()); - } - } - @Test void addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { try { From c1fdef685d5d093d89bda73b3f98217394d0d2d1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 17 Mar 2023 14:48:27 +0000 Subject: [PATCH 096/418] Updated release date. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index ede3dba00..469427bfe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.23.1 (unreleased) +## 1.23.1 (17th March 2023) - Deprecates the `setExternalBoundariesVisible` methods on `ContainerView`, `ComponentView`, and `DynamicView`. - Removes the check for empty content when adding a documentation section. From 0db6d36850e2c73c1aee2b073b854948c15771ca Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 24 Mar 2023 09:56:34 +0000 Subject: [PATCH 097/418] `DynamicView.endParallelSequences(true)` will now increment the counter when no relationships have been defined in the parallel sequence. --- build.gradle | 2 +- docs/changelog.md | 4 ++ .../com/structurizr/view/SequenceCounter.java | 6 +++ .../com/structurizr/view/SequenceNumber.java | 15 ++++++-- .../structurizr/view/DynamicViewTests.java | 38 +++++++++++++++++++ .../structurizr/view/SequenceNumberTests.java | 22 ++++++++++- 6 files changed, 82 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 2745db2e8..9f93c6964 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.23.1' + version = '1.23.2' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 469427bfe..92c4241e7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.23.2 (unreleased) + +- `DynamicView.endParallelSequences(true)` will now increment the counter when no relationships have been defined in the parallel sequence. + ## 1.23.1 (17th March 2023) - Deprecates the `setExternalBoundariesVisible` methods on `ContainerView`, `ComponentView`, and `DynamicView`. diff --git a/structurizr-core/src/com/structurizr/view/SequenceCounter.java b/structurizr-core/src/com/structurizr/view/SequenceCounter.java index dcfd3590e..8f8d52314 100644 --- a/structurizr-core/src/com/structurizr/view/SequenceCounter.java +++ b/structurizr-core/src/com/structurizr/view/SequenceCounter.java @@ -4,6 +4,7 @@ class SequenceCounter implements Cloneable { private SequenceCounter parent; private int sequence = 0; + private boolean incremented = false; SequenceCounter() { } @@ -14,6 +15,11 @@ class SequenceCounter implements Cloneable { void increment() { this.sequence++; + incremented = true; + } + + boolean incremented() { + return incremented; } int getSequence() { diff --git a/structurizr-core/src/com/structurizr/view/SequenceNumber.java b/structurizr-core/src/com/structurizr/view/SequenceNumber.java index 027938b6e..259bb98a0 100644 --- a/structurizr-core/src/com/structurizr/view/SequenceNumber.java +++ b/structurizr-core/src/com/structurizr/view/SequenceNumber.java @@ -18,9 +18,18 @@ void startParallelSequence() { void endParallelSequence(boolean endAllParallelSequencesAndContinueNumbering) { if (endAllParallelSequencesAndContinueNumbering) { - int sequence = this.counter.getSequence(); - this.counter = this.counter.getParent(); - this.counter.setSequence(sequence); + if (counter.incremented()) { + // relationships were added in this parallel sequence + int sequence = this.counter.getSequence(); + this.counter = this.counter.getParent(); + this.counter.setSequence(sequence); + } else { + // no relationships were added in this parallel sequence, so treat this as a group of parallel sequences + int sequence = this.counter.getSequence(); + this.counter = this.counter.getParent(); + this.counter.setSequence(sequence); + this.counter.increment(); + } } else { this.counter = this.counter.getParent(); } diff --git a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java index efe39030d..029224428 100644 --- a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java @@ -358,6 +358,44 @@ void parallelSequence() { assertEquals(1, view.getRelationships().stream().filter(r -> r.getOrder().equals("4")).count()); } + @Test + void parallelSequence2() { + workspace = new Workspace("Name", "Description"); + model = workspace.getModel(); + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + SoftwareSystem d = model.addSoftwareSystem("D"); + + a.uses(b, ""); + b.uses(c, ""); + b.uses(d, ""); + b.uses(a, ""); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + + RelationshipView rv1 = view.add(a, b); + + view.startParallelSequence(); + + view.startParallelSequence(); + RelationshipView rv2 = view.add(b, c); + view.endParallelSequence(); + + view.startParallelSequence(); + RelationshipView rv3 = view.add(b, d); + view.endParallelSequence(); + + view.endParallelSequence(true); + + RelationshipView rv4 = view.add(b, a); + + assertEquals("1", rv1.getOrder()); + assertEquals("2", rv2.getOrder()); + assertEquals("2", rv3.getOrder()); + assertEquals("3", rv4.getOrder()); + } + @Test void getRelationships_WhenTheOrderPropertyIsAnInteger() { containerA1.uses(containerA2, "uses"); diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java b/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java index ed1eff62f..6735450c6 100644 --- a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java @@ -14,7 +14,7 @@ void increment() { } @Test - void parallelSequences() { + void parallelSequences_1() { SequenceNumber sequenceNumber = new SequenceNumber(); assertEquals("1", sequenceNumber.getNext()); @@ -29,4 +29,24 @@ void parallelSequences() { assertEquals("3", sequenceNumber.getNext()); } + @Test + void parallelSequences_2() { + SequenceNumber sequenceNumber = new SequenceNumber(); + assertEquals("1", sequenceNumber.getNext()); + + sequenceNumber.startParallelSequence(); + + sequenceNumber.startParallelSequence(); + assertEquals("2", sequenceNumber.getNext()); + sequenceNumber.endParallelSequence(false); + + sequenceNumber.startParallelSequence(); + assertEquals("2", sequenceNumber.getNext()); + sequenceNumber.endParallelSequence(false); + + sequenceNumber.endParallelSequence(true); + + assertEquals("3", sequenceNumber.getNext()); + } + } From 20951b597652187976abdbfe1329094cbfed2d31 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 24 Mar 2023 09:57:19 +0000 Subject: [PATCH 098/418] Added release date. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 92c4241e7..63eb72923 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.23.2 (unreleased) +## 1.23.2 (24th March 2023) - `DynamicView.endParallelSequences(true)` will now increment the counter when no relationships have been defined in the parallel sequence. From f571764e471933081579144a7c43e401cabed29e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 29 Mar 2023 11:52:30 +0100 Subject: [PATCH 099/418] Adds support for deployment elements to be grouped. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ .../src/com/structurizr/model/DeploymentElement.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9f93c6964..94eb57956 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.23.2' + version = '1.24.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 63eb72923..563895978 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.24.0 (unreleased) + +- Adds support for deployment elements to be grouped. + ## 1.23.2 (24th March 2023) - `DynamicView.endParallelSequences(true)` will now increment the counter when no relationships have been defined in the parallel sequence. diff --git a/structurizr-core/src/com/structurizr/model/DeploymentElement.java b/structurizr-core/src/com/structurizr/model/DeploymentElement.java index 120a63af7..91d55cd21 100644 --- a/structurizr-core/src/com/structurizr/model/DeploymentElement.java +++ b/structurizr-core/src/com/structurizr/model/DeploymentElement.java @@ -5,7 +5,7 @@ /** * This is the superclass for model elements that describe deployment nodes, infrastructure nodes, and container instances. */ -public abstract class DeploymentElement extends Element { +public abstract class DeploymentElement extends GroupableElement { public static final String DEFAULT_DEPLOYMENT_ENVIRONMENT = "Default"; public static final String DEFAULT_DEPLOYMENT_GROUP = "Default"; From 458f3c8c7830d04cb6788e44f54535db8e8f887c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 30 Mar 2023 07:01:00 +0100 Subject: [PATCH 100/418] Updated for release. --- docs/changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 563895978..41285ad7d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,8 @@ # Changelog -## 1.24.0 (unreleased) +## 1.24.0 (30th March 2023) -- Adds support for deployment elements to be grouped. +- Adds a `group` property to deployment nodes, infrastructure nodes, software system instances, and container instances. ## 1.23.2 (24th March 2023) From a39b18d05f2ddc57ef3443645d61a203bbceff99 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 1 Apr 2023 06:51:47 +0100 Subject: [PATCH 101/418] Reduces visibility of `setOrder()` and `setDescription()` on `RelationshipView`, as they should not be public. --- .../src/com/structurizr/view/RelationshipView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structurizr-core/src/com/structurizr/view/RelationshipView.java b/structurizr-core/src/com/structurizr/view/RelationshipView.java index f3d416a05..48b992c16 100644 --- a/structurizr-core/src/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/com/structurizr/view/RelationshipView.java @@ -83,7 +83,7 @@ public String getDescription() { * * @param description the description, as a String */ - public void setDescription(String description) { + void setDescription(String description) { this.description = description; } @@ -101,7 +101,7 @@ public String getOrder() { * * @param order the order, as a String */ - public void setOrder(String order) { + void setOrder(String order) { this.order = order; } From 264fefc32fc96eeae1fd68d834902239f4f9b15d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 1 Apr 2023 06:52:08 +0100 Subject: [PATCH 102/418] Fixes IDEA's "protected method on a final class" warning. --- structurizr-core/src/com/structurizr/view/DynamicView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/view/DynamicView.java b/structurizr-core/src/com/structurizr/view/DynamicView.java index 80f220a03..79331d3ce 100644 --- a/structurizr-core/src/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/com/structurizr/view/DynamicView.java @@ -235,7 +235,7 @@ public RelationshipView add(Relationship relationship, String description) { return addRelationship(relationship, description, sequenceNumber.getNext(), false); } - protected RelationshipView addRelationship(Relationship relationship, String description, String order, boolean response) { + private RelationshipView addRelationship(Relationship relationship, String description, String order, boolean response) { RelationshipView relationshipView = addRelationship(relationship); if (relationshipView != null) { relationshipView.setDescription(description); From b3c55e7aa3ed15e1a2e0f9d52d6d0ad971d95141 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 1 Apr 2023 06:52:23 +0100 Subject: [PATCH 103/418] . --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 41285ad7d..5375586b5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## (unreleased) + +- Reduces visibility of `setOrder()` and `setDescription()` on `RelationshipView`, as they should not be public. + ## 1.24.0 (30th March 2023) - Adds a `group` property to deployment nodes, infrastructure nodes, software system instances, and container instances. From a646e80f2ae080649eb749d3fc6ac238e5fb459b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 1 Apr 2023 06:52:44 +0100 Subject: [PATCH 104/418] Adds deprecation tags to the `setLocation()` methods. --- structurizr-core/src/com/structurizr/model/Person.java | 1 + structurizr-core/src/com/structurizr/model/SoftwareSystem.java | 1 + 2 files changed, 2 insertions(+) diff --git a/structurizr-core/src/com/structurizr/model/Person.java b/structurizr-core/src/com/structurizr/model/Person.java index d4f67d3ba..858463bae 100644 --- a/structurizr-core/src/com/structurizr/model/Person.java +++ b/structurizr-core/src/com/structurizr/model/Person.java @@ -32,6 +32,7 @@ public Location getLocation() { return location; } + @Deprecated public void setLocation(Location location) { if (location != null) { this.location = location; diff --git a/structurizr-core/src/com/structurizr/model/SoftwareSystem.java b/structurizr-core/src/com/structurizr/model/SoftwareSystem.java index b59ae5075..b700c5b55 100644 --- a/structurizr-core/src/com/structurizr/model/SoftwareSystem.java +++ b/structurizr-core/src/com/structurizr/model/SoftwareSystem.java @@ -50,6 +50,7 @@ public Location getLocation() { * * @param location a Location instance */ + @Deprecated public void setLocation(Location location) { if (location != null) { this.location = location; From b4946c5b0e04abc2b815168709a7a29cd2f14c0c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 5 Apr 2023 12:27:11 +0100 Subject: [PATCH 105/418] Updated for release. --- build.gradle | 2 +- docs/changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 94eb57956..11e342f6f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.24.0' + version = '1.24.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 5375586b5..e90529616 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## (unreleased) +## 1.24.1 (5th April 2023) - Reduces visibility of `setOrder()` and `setDescription()` on `RelationshipView`, as they should not be public. From 2e6bc733443d1e0b27b05e104a402a7275b149de Mon Sep 17 00:00:00 2001 From: goto1134 <1134togo@gmail.com> Date: Wed, 17 May 2023 13:53:20 +0200 Subject: [PATCH 106/418] Extract AnimatedView interface --- .../src/com/structurizr/view/AnimatedView.java | 11 +++++++++++ .../src/com/structurizr/view/CustomView.java | 10 +++++++--- .../src/com/structurizr/view/DeploymentView.java | 10 +++++++--- .../src/com/structurizr/view/StaticView.java | 10 +++++++--- 4 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 structurizr-core/src/com/structurizr/view/AnimatedView.java diff --git a/structurizr-core/src/com/structurizr/view/AnimatedView.java b/structurizr-core/src/com/structurizr/view/AnimatedView.java new file mode 100644 index 000000000..6d0fa9e2c --- /dev/null +++ b/structurizr-core/src/com/structurizr/view/AnimatedView.java @@ -0,0 +1,11 @@ +package com.structurizr.view; + +import javax.annotation.Nonnull; +import java.util.List; + +public interface AnimatedView { + + @Nonnull + List getAnimations(); + +} diff --git a/structurizr-core/src/com/structurizr/view/CustomView.java b/structurizr-core/src/com/structurizr/view/CustomView.java index ebe20cfa2..f5b9447ef 100644 --- a/structurizr-core/src/com/structurizr/view/CustomView.java +++ b/structurizr-core/src/com/structurizr/view/CustomView.java @@ -7,6 +7,7 @@ import com.structurizr.model.Relationship; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -15,8 +16,9 @@ /** * Represents a custom view, containing custom elements. */ -public final class CustomView extends ModelView { +public final class CustomView extends ModelView implements AnimatedView { + @Nonnull private List animations = new ArrayList<>(); private Model model; @@ -124,11 +126,13 @@ public void addAnimation(CustomElement... elements) { animations.add(new Animation(animations.size() + 1, elementsInThisAnimationStep, relationshipsInThisAnimationStep)); } + @Nonnull + @Override public List getAnimations() { return new ArrayList<>(animations); } - void setAnimations(List animations) { + void setAnimations(@Nullable List animations) { if (animations != null) { this.animations = new ArrayList<>(animations); } else { @@ -184,4 +188,4 @@ public void addAllCustomElements() { }); } -} \ No newline at end of file +} diff --git a/structurizr-core/src/com/structurizr/view/DeploymentView.java b/structurizr-core/src/com/structurizr/view/DeploymentView.java index f58ae73cb..04638445e 100644 --- a/structurizr-core/src/com/structurizr/view/DeploymentView.java +++ b/structurizr-core/src/com/structurizr/view/DeploymentView.java @@ -5,17 +5,19 @@ import com.structurizr.util.StringUtils; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.*; import java.util.stream.Collectors; /** * A deployment view, used to show the mapping of container instances to deployment nodes. */ -public final class DeploymentView extends ModelView { +public final class DeploymentView extends ModelView implements AnimatedView { private Model model; private String environment = DeploymentElement.DEFAULT_DEPLOYMENT_ENVIRONMENT; + @Nonnull private List animations = new ArrayList<>(); DeploymentView() { @@ -439,11 +441,13 @@ private DeploymentNode findDeploymentNode(Element e) { return null; } + @Nonnull + @Override public List getAnimations() { return new ArrayList<>(animations); } - void setAnimations(List animations) { + void setAnimations(@Nullable List animations) { if (animations != null) { this.animations = new ArrayList<>(animations); } else { @@ -479,4 +483,4 @@ public void remove(@Nonnull CustomElement customElement) { removeElement(customElement); } -} \ No newline at end of file +} diff --git a/structurizr-core/src/com/structurizr/view/StaticView.java b/structurizr-core/src/com/structurizr/view/StaticView.java index da0dfb569..f7ea1d9dd 100644 --- a/structurizr-core/src/com/structurizr/view/StaticView.java +++ b/structurizr-core/src/com/structurizr/view/StaticView.java @@ -3,6 +3,7 @@ import com.structurizr.model.*; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -11,8 +12,9 @@ /** * The superclass for all static views (system landscape, system context, container and component views). */ -public abstract class StaticView extends ModelView { +public abstract class StaticView extends ModelView implements AnimatedView { + @Nonnull private List animations = new ArrayList<>(); StaticView() { @@ -225,11 +227,13 @@ public void addAnimation(Element... elements) { animations.add(new Animation(animations.size() + 1, elementsInThisAnimationStep, relationshipsInThisAnimationStep)); } + @Nonnull + @Override public List getAnimations() { return new ArrayList<>(animations); } - void setAnimations(List animations) { + void setAnimations(@Nullable List animations) { if (animations != null) { this.animations = new ArrayList<>(animations); } else { @@ -265,4 +269,4 @@ public void remove(@Nonnull CustomElement customElement) { removeElement(customElement); } -} \ No newline at end of file +} From e2bf73bad4391ceba6328298fce8b108dbac12b5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 7 Jul 2023 10:27:47 +0100 Subject: [PATCH 107/418] Fixes #213. --- build.gradle | 2 +- docs/changelog.md | 4 ++ .../src/com/structurizr/view/ViewSet.java | 43 +++++++++++++---- .../com/structurizr/view/ViewSetTests.java | 48 +++++++++++++------ 4 files changed, 72 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index 11e342f6f..cfda4d6fe 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.24.1' + version = '1.25.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index e90529616..344175460 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.25.0 (unreleased to Maven Central) + +- Fixes https://github.com/structurizr/java/issues/213 (Views are not created automatically if non-English characters are used in software systems' names) + ## 1.24.1 (5th April 2023) - Reduces visibility of `setOrder()` and `setDescription()` on `RelationshipView`, as they should not be public. diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/com/structurizr/view/ViewSet.java index 02e799b3f..79559dd62 100644 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/com/structurizr/view/ViewSet.java @@ -8,6 +8,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.util.*; import static com.structurizr.util.StringUtils.isNullOrEmpty; @@ -19,6 +21,13 @@ public final class ViewSet { private static final Log log = LogFactory.getLog(ViewSet.class); + public static final String SYSTEM_LANDSCAPE_VIEW_TYPE = "SystemLandscape"; + public static final String SYSTEM_CONTEXT_VIEW_TYPE = "SystemContext"; + public static final String CONTAINER_VIEW_TYPE = "Container"; + public static final String COMPONENT_VIEW_TYPE = "Component"; + public static final String DYNAMIC_VIEW_TYPE = "Dynamic"; + public static final String DEPLOYMENT_VIEW_TYPE = "Deployment"; + private Model model; private Collection customViews = new HashSet<>(); @@ -822,9 +831,27 @@ public boolean isEmpty() { return customViews.isEmpty() && systemLandscapeViews.isEmpty() && systemContextViews.isEmpty() && containerViews.isEmpty() && componentViews.isEmpty() && dynamicViews.isEmpty() && deploymentViews.isEmpty() && filteredViews.isEmpty(); } + public String generateViewKey(String prefix) { + NumberFormat format = new DecimalFormat("000"); + int counter = 1; + String key = prefix + "-" + format.format(counter); + + while (hasViewWithKey(key)) { + counter++; + key = prefix + "-" + format.format(counter); + } + + log.warn(key + " is an automatically generated view key - you will likely lose manual layout information when using automatically generated view keys."); + return key; + } + + private boolean hasViewWithKey(String key) { + return getViews().stream().anyMatch(view -> view.getKey().equals(key)); + } + public void createDefaultViews() { // create a single System Landscape diagram containing all people and software systems - SystemLandscapeView systemLandscapeView = createSystemLandscapeView("SystemLandscape", ""); + SystemLandscapeView systemLandscapeView = createSystemLandscapeView(generateViewKey(SYSTEM_LANDSCAPE_VIEW_TYPE), ""); systemLandscapeView.addDefaultElements(); systemLandscapeView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); systemLandscapeView.setEnterpriseBoundaryVisible(true); @@ -835,7 +862,7 @@ public void createDefaultViews() { // and a system context view plus container view for each software system for (SoftwareSystem softwareSystem : softwareSystems) { - String systemContextViewKey = removeNonWordCharacters(softwareSystem.getName()) + "-SystemContext"; + String systemContextViewKey = generateViewKey(SYSTEM_CONTEXT_VIEW_TYPE); SystemContextView systemContextView = createSystemContextView(softwareSystem, systemContextViewKey, ""); systemContextView.addDefaultElements(); systemContextView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); @@ -845,7 +872,7 @@ public void createDefaultViews() { List containers = new ArrayList<>(softwareSystem.getContainers()); containers.sort(Comparator.comparing(Element::getName)); - String containerViewKey = removeNonWordCharacters(softwareSystem.getName()) + "-Container"; + String containerViewKey = generateViewKey(CONTAINER_VIEW_TYPE); ContainerView containerView = createContainerView(softwareSystem, containerViewKey, ""); containerView.addDefaultElements(); containerView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); @@ -853,7 +880,7 @@ public void createDefaultViews() { for (Container container : containers) { if (container.getComponents().size() > 0) { - String componentViewKey = removeNonWordCharacters(softwareSystem.getName()) + "-" + removeNonWordCharacters(container.getName()) + "-Component"; + String componentViewKey = generateViewKey(COMPONENT_VIEW_TYPE); ComponentView componentView = createComponentView(container, componentViewKey, ""); componentView.addDefaultElements(); componentView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); @@ -899,7 +926,7 @@ public void createDefaultViews() { if (softwareSystems.isEmpty()) { // there are no container instances, but perhaps there are infrastructure nodes in this environment if (model.getElements().stream().anyMatch(e -> e instanceof InfrastructureNode && ((InfrastructureNode)e).getEnvironment().equals(deploymentEnvironment))) { - String deploymentViewKey = removeNonWordCharacters(deploymentEnvironment) + "-Deployment"; + String deploymentViewKey = generateViewKey(DEPLOYMENT_VIEW_TYPE); DeploymentView deploymentView = createDeploymentView(deploymentViewKey, ""); deploymentView.setEnvironment(deploymentEnvironment); deploymentView.addDefaultElements(); @@ -909,7 +936,7 @@ public void createDefaultViews() { softwareSystems.sort(Comparator.comparing(Element::getName)); for (SoftwareSystem softwareSystem : softwareSystems) { - String deploymentViewKey = removeNonWordCharacters(softwareSystem.getName()) + "-" + removeNonWordCharacters(deploymentEnvironment) + "-Deployment"; + String deploymentViewKey = generateViewKey(DEPLOYMENT_VIEW_TYPE); DeploymentView deploymentView = createDeploymentView(softwareSystem, deploymentViewKey, ""); deploymentView.setEnvironment(deploymentEnvironment); deploymentView.addDefaultElements(); @@ -919,10 +946,6 @@ public void createDefaultViews() { } } - private String removeNonWordCharacters(String name) { - return name.replaceAll("\\W", ""); - } - private Set getSoftwareSystemInstances(DeploymentNode deploymentNode) { Set softwareSystemInstances = new HashSet<>(deploymentNode.getSoftwareSystemInstances()); diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java b/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java index d490ae9fe..b1f1a036d 100644 --- a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java @@ -1033,19 +1033,19 @@ void createDefaultViews() { views.createDefaultViews(); assertEquals(1, views.getSystemLandscapeViews().size()); - assertEquals("SystemLandscape", views.getSystemLandscapeViews().iterator().next().getKey()); + assertEquals("SystemLandscape-001", views.getSystemLandscapeViews().iterator().next().getKey()); assertEquals(2, views.getSystemContextViews().size()); - assertSame(ss1, views.getSystemContextViews().stream().filter(v -> v.getKey().equals("SoftwareSystem1-SystemContext")).findFirst().get().getSoftwareSystem()); - assertSame(ss2, views.getSystemContextViews().stream().filter(v -> v.getKey().equals("SoftwareSystem2-SystemContext")).findFirst().get().getSoftwareSystem()); + assertSame(ss1, views.getSystemContextViews().stream().filter(v -> v.getKey().equals("SystemContext-001")).findFirst().get().getSoftwareSystem()); + assertSame(ss2, views.getSystemContextViews().stream().filter(v -> v.getKey().equals("SystemContext-002")).findFirst().get().getSoftwareSystem()); assertEquals(2, views.getContainerViews().size()); - assertSame(ss1, views.getContainerViews().stream().filter(v -> v.getKey().equals("SoftwareSystem1-Container")).findFirst().get().getSoftwareSystem()); - assertSame(ss2, views.getContainerViews().stream().filter(v -> v.getKey().equals("SoftwareSystem2-Container")).findFirst().get().getSoftwareSystem()); + assertSame(ss1, views.getContainerViews().stream().filter(v -> v.getKey().equals("Container-001")).findFirst().get().getSoftwareSystem()); + assertSame(ss2, views.getContainerViews().stream().filter(v -> v.getKey().equals("Container-002")).findFirst().get().getSoftwareSystem()); assertEquals(2, views.getComponentViews().size()); - assertSame(c1, views.getComponentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem1-Container1-Component")).findFirst().get().getContainer()); - assertSame(c2, views.getComponentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem2-Container2-Component")).findFirst().get().getContainer()); + assertSame(c1, views.getComponentViews().stream().filter(v -> v.getKey().equals("Component-001")).findFirst().get().getContainer()); + assertSame(c2, views.getComponentViews().stream().filter(v -> v.getKey().equals("Component-002")).findFirst().get().getContainer()); assertEquals(0, views.getDynamicViews().size()); assertEquals(0, views.getDeploymentViews().size()); @@ -1056,7 +1056,7 @@ void createDefaultViews() { views.createDefaultViews(); assertEquals(1, views.getDeploymentViews().size()); - assertSame("Live", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Live-Deployment")).findFirst().get().getEnvironment()); + assertSame("Live", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-001")).findFirst().get().getEnvironment()); dev.add(ss1); liveEc2.add(c1); @@ -1066,12 +1066,12 @@ void createDefaultViews() { views.createDefaultViews(); assertEquals(3, views.getDeploymentViews().size()); - assertSame(ss1, views.getDeploymentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem1-Development-Deployment")).findFirst().get().getSoftwareSystem()); - assertSame("Development", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem1-Development-Deployment")).findFirst().get().getEnvironment()); - assertSame(ss1, views.getDeploymentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem1-Live-Deployment")).findFirst().get().getSoftwareSystem()); - assertSame("Live", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem1-Live-Deployment")).findFirst().get().getEnvironment()); - assertSame(ss2, views.getDeploymentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem2-Live-Deployment")).findFirst().get().getSoftwareSystem()); - assertSame("Live", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("SoftwareSystem2-Live-Deployment")).findFirst().get().getEnvironment()); + assertSame(ss1, views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-001")).findFirst().get().getSoftwareSystem()); + assertSame("Development", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-001")).findFirst().get().getEnvironment()); + assertSame(ss1, views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-002")).findFirst().get().getSoftwareSystem()); + assertSame("Live", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-002")).findFirst().get().getEnvironment()); + assertSame(ss2, views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-003")).findFirst().get().getSoftwareSystem()); + assertSame("Live", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-003")).findFirst().get().getEnvironment()); } @Test @@ -1122,4 +1122,24 @@ void view_ordering() { assertEquals(9, systemLandscapeView2.getOrder()); } + @Test + public void createDefaultViews_ForSoftwareSystemsWithNamesUsingUTF8Characters() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem ss1 = workspace.getModel().addSoftwareSystem("English is fine"); + SoftwareSystem ss2 = workspace.getModel().addSoftwareSystem("Ðо люди говорÑÑ‚ и на других Ñзыках"); + SoftwareSystem ss3 = workspace.getModel().addSoftwareSystem("英語ã ã‘ãŒè¨€èªžã§ã¯ãªã„"); + + workspace.getViews().createDefaultViews(); + + assertEquals(4, workspace.getViews().getViews().size()); + + assertEquals(1, workspace.getViews().getSystemLandscapeViews().size()); + assertEquals("SystemLandscape-001", workspace.getViews().getSystemLandscapeViews().iterator().next().getKey()); + + assertEquals(3, workspace.getViews().getSystemContextViews().size()); + assertSame(ss1, workspace.getViews().getSystemContextViews().stream().filter(v -> v.getKey().equals("SystemContext-001")).findFirst().get().getSoftwareSystem()); + assertSame(ss2, workspace.getViews().getSystemContextViews().stream().filter(v -> v.getKey().equals("SystemContext-002")).findFirst().get().getSoftwareSystem()); + assertSame(ss3, workspace.getViews().getSystemContextViews().stream().filter(v -> v.getKey().equals("SystemContext-003")).findFirst().get().getSoftwareSystem()); + } + } \ No newline at end of file From e2a6fecb1a51f597d3185ffa85c4afbe8c8211dc Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 22 Jul 2023 12:01:44 +0100 Subject: [PATCH 108/418] Adds a way to load themes with a timeout. --- docs/changelog.md | 3 +- .../src/com/structurizr/view/ThemeUtils.java | 33 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 344175460..a8f824ccf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,9 @@ # Changelog -## 1.25.0 (unreleased to Maven Central) +## 1.25.0 (22nd July 2023) - Fixes https://github.com/structurizr/java/issues/213 (Views are not created automatically if non-English characters are used in software systems' names) +- Adds a way to load themes with a timeout. ## 1.24.1 (5th April 2023) diff --git a/structurizr-client/src/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/com/structurizr/view/ThemeUtils.java index 22c177e62..418787407 100644 --- a/structurizr-client/src/com/structurizr/view/ThemeUtils.java +++ b/structurizr-client/src/com/structurizr/view/ThemeUtils.java @@ -8,13 +8,16 @@ import com.structurizr.io.WorkspaceWriterException; import com.structurizr.util.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; import org.apache.hc.core5.http.io.entity.EntityUtils; import java.io.*; import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; /** * Some utility methods for exporting themes to JSON. @@ -23,6 +26,8 @@ public final class ThemeUtils { private static final int HTTP_OK_STATUS = 200; + private static final int DEFAULT_TIMEOUT_IN_MILLISECONDS = 10000; + /** * Serializes the theme (element and relationship styles) in the specified workspace to a file, as a JSON string. * @@ -62,13 +67,37 @@ public static String toJson(Workspace workspace) throws Exception { /** * Loads (and inlines) the element and relationship styles from the themes defined in the workspace, into the workspace itself. * This implementation simply copies the styles from all themes into the workspace. + * This uses a default timeout value of 10000ms. * * @param workspace a Workspace object * @throws Exception if something goes wrong */ public static void loadThemes(Workspace workspace) throws Exception { + loadThemes(workspace, DEFAULT_TIMEOUT_IN_MILLISECONDS); + } + + /** + * Loads (and inlines) the element and relationship styles from the themes defined in the workspace, into the workspace itself. + * This implementation simply copies the styles from all themes into the workspace. + * + * @param workspace a Workspace object + * @param timeoutInMilliseconds the timeout in milliseconds + * @throws Exception if something goes wrong + */ + public static void loadThemes(Workspace workspace, int timeoutInMilliseconds) throws Exception { for (String url : workspace.getViews().getConfiguration().getThemes()) { - CloseableHttpClient httpClient = HttpClients.createSystem(); + ConnectionConfig connectionConfig = ConnectionConfig.custom() + .setConnectTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) + .setSocketTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) + .build(); + + BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); + cm.setConnectionConfig(connectionConfig); + + CloseableHttpClient httpClient = HttpClientBuilder.create() + .setConnectionManager(cm) + .build(); + HttpGet httpGet = new HttpGet(url); CloseableHttpResponse response = httpClient.execute(httpGet); From 53bceb7627949e1d454c95c88c53f1ce1c82fbaa Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 26 Jul 2023 14:45:31 +0100 Subject: [PATCH 109/418] Adds a `clearUsers()` method to clear configured users on the workspace configuration. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ .../configuration/WorkspaceConfiguration.java | 7 +++++++ .../configuration/WorkspaceConfigurationTests.java | 10 ++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cfda4d6fe..a2810d4dd 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.25.0' + version = '1.25.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index a8f824ccf..0c89b4453 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.25.1 (unreleased to Maven Central) + +- Adds a `clearUsers()` method to clear configured users on the workspace configuration. + ## 1.25.0 (22nd July 2023) - Fixes https://github.com/structurizr/java/issues/213 (Views are not created automatically if non-English characters are used in software systems' names) diff --git a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java index 63627feea..d46eb3ba0 100644 --- a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java +++ b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java @@ -48,4 +48,11 @@ public void addUser(String username, Role role) { users.add(new User(username, role)); } + /** + * Clears all configured users. + */ + public void clearUsers() { + this.users = new HashSet<>(); + } + } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java index 16d067eb3..23ba9deeb 100644 --- a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -50,4 +50,14 @@ void addUser_AddsAUser() { assertEquals(Role.ReadOnly, configuration.getUsers().iterator().next().getRole()); } + @Test + void clearUsers() { + WorkspaceConfiguration configuration = new WorkspaceConfiguration(); + configuration.addUser("user@domain.com", Role.ReadOnly); + assertEquals(1, configuration.getUsers().size()); + + configuration.clearUsers(); + assertEquals(0, configuration.getUsers().size()); + } + } \ No newline at end of file From 69fbc7a26b917d6cb18ad56c2b61bb2a95ccccac Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 26 Jul 2023 14:47:37 +0100 Subject: [PATCH 110/418] Updated for release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 0c89b4453..d5462431b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.25.1 (unreleased to Maven Central) +## 1.25.1 (26th July 2023) - Adds a `clearUsers()` method to clear configured users on the workspace configuration. From 611c6f442af1efd6719180db4308be276ecde907 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 28 Jul 2023 10:18:01 +0100 Subject: [PATCH 111/418] Make User constructor public. --- .../com/structurizr/configuration/User.java | 12 ++++- .../configuration/WorkspaceConfiguration.java | 17 +++---- .../structurizr/configuration/UserTests.java | 48 +++++++++++++++++++ .../WorkspaceConfigurationTests.java | 17 ++++--- 4 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 structurizr-core/test/unit/com/structurizr/configuration/UserTests.java diff --git a/structurizr-core/src/com/structurizr/configuration/User.java b/structurizr-core/src/com/structurizr/configuration/User.java index efad4930c..cdfd4e3c1 100644 --- a/structurizr-core/src/com/structurizr/configuration/User.java +++ b/structurizr-core/src/com/structurizr/configuration/User.java @@ -1,5 +1,7 @@ package com.structurizr.configuration; +import com.structurizr.util.StringUtils; + /** * Represents a user, and the role-based access they have to a workspace. */ @@ -11,7 +13,15 @@ public final class User { User() { } - User(String username, Role role) { + public User(String username, Role role) { + if (StringUtils.isNullOrEmpty(username)) { + throw new IllegalArgumentException("A username must be specified."); + } + + if (role == null) { + throw new IllegalArgumentException("A role must be specified."); + } + setUsername(username); setRole(role); } diff --git a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java index d46eb3ba0..aacce39c0 100644 --- a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java +++ b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java @@ -30,6 +30,15 @@ void setUsers(Set users) { } } + /** + * Adds a user. + * + * @param user a User object representing the username and role + */ + public void addUser(User user) { + users.add(user); + } + /** * Adds a user, with the specified username and role. * @@ -37,14 +46,6 @@ void setUsers(Set users) { * @param role the user's role */ public void addUser(String username, Role role) { - if (StringUtils.isNullOrEmpty(username)) { - throw new IllegalArgumentException("A username must be specified."); - } - - if (role == null) { - throw new IllegalArgumentException("A role must be specified."); - } - users.add(new User(username, role)); } diff --git a/structurizr-core/test/unit/com/structurizr/configuration/UserTests.java b/structurizr-core/test/unit/com/structurizr/configuration/UserTests.java new file mode 100644 index 000000000..efacd7398 --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/configuration/UserTests.java @@ -0,0 +1,48 @@ +package com.structurizr.configuration; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class UserTests { + + @Test + void construct_ThrowsAnException_WhenANullUsernameIsSpecified() { + try { + new User(null, Role.ReadWrite); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A username must be specified.", iae.getMessage()); + } + } + + @Test + void construct_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { + try { + new User(" ", Role.ReadWrite); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A username must be specified.", iae.getMessage()); + } + } + + @Test + void construct_ThrowsAnException_WhenANullRoleIsSpecified() { + try { + new User("user@domain.com", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A role must be specified.", iae.getMessage()); + } + } + + @Test + void comstruct() { + User user = new User("user@domain.com", Role.ReadOnly); + + assertEquals("user@domain.com", user.getUsername()); + assertEquals(Role.ReadOnly, user.getRole()); + } + +} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java index 23ba9deeb..2f2cd68fc 100644 --- a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -2,8 +2,7 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; public class WorkspaceConfigurationTests { @@ -33,7 +32,7 @@ void addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { void addUser_ThrowsAnException_WhenANullRoleIsSpecified() { try { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); - configuration.addUser("user@domain.com", null); + configuration.addUser("user@example.com", null); fail(); } catch (IllegalArgumentException iae) { assertEquals("A role must be specified.", iae.getMessage()); @@ -43,11 +42,17 @@ void addUser_ThrowsAnException_WhenANullRoleIsSpecified() { @Test void addUser_AddsAUser() { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); - configuration.addUser("user@domain.com", Role.ReadOnly); + configuration.addUser("user1@example.com", Role.ReadOnly); assertEquals(1, configuration.getUsers().size()); - assertEquals("user@domain.com", configuration.getUsers().iterator().next().getUsername()); - assertEquals(Role.ReadOnly, configuration.getUsers().iterator().next().getRole()); + User user = configuration.getUsers().stream().filter(u -> u.getUsername().equals("user1@example.com")).findFirst().get(); + assertEquals(Role.ReadOnly, user.getRole()); + + configuration.addUser("user2@example.com", Role.ReadWrite); + + assertEquals(2, configuration.getUsers().size()); + user = configuration.getUsers().stream().filter(u -> u.getUsername().equals("user2@example.com")).findFirst().get(); + assertEquals(Role.ReadWrite, user.getRole()); } @Test From 826129c1ce1f657acfe47921940a52af93d63a06 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 28 Jul 2023 10:18:29 +0100 Subject: [PATCH 112/418] Adds the ability to specify the workspace visibility (private/public) via the workspace configuration. --- docs/changelog.md | 4 ++++ .../structurizr/configuration/Visibility.java | 8 ++++++++ .../configuration/WorkspaceConfiguration.java | 19 +++++++++++++++++++ .../WorkspaceConfigurationTests.java | 9 +++++++++ 4 files changed, 40 insertions(+) create mode 100644 structurizr-core/src/com/structurizr/configuration/Visibility.java diff --git a/docs/changelog.md b/docs/changelog.md index d5462431b..1898e9f4d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.26.0 (unreleased to Maven Central) + +- Adds the ability to specify the workspace visibility (private/public) via the workspace configuration. + ## 1.25.1 (26th July 2023) - Adds a `clearUsers()` method to clear configured users on the workspace configuration. diff --git a/structurizr-core/src/com/structurizr/configuration/Visibility.java b/structurizr-core/src/com/structurizr/configuration/Visibility.java new file mode 100644 index 000000000..a61fff520 --- /dev/null +++ b/structurizr-core/src/com/structurizr/configuration/Visibility.java @@ -0,0 +1,8 @@ +package com.structurizr.configuration; + +public enum Visibility { + + Private, + Public + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java index aacce39c0..18a87c961 100644 --- a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java +++ b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java @@ -10,11 +10,30 @@ */ public final class WorkspaceConfiguration { + private Visibility visibility = null; private Set users = new HashSet<>(); WorkspaceConfiguration() { } + /** + * Gets the visibility of this workspace (private or public). + * + * @return a Visibility enum + */ + public Visibility getVisibility() { + return visibility; + } + + /** + * Gets the visibility of this workspace (private or public). + * + * @param visibility a Visibility enum + */ + void setVisibility(Visibility visibility) { + this.visibility = visibility; + } + /** * Gets the set of users should have read-write or read-only access to the workspace. * diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java index 2f2cd68fc..529339033 100644 --- a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -55,6 +55,15 @@ void addUser_AddsAUser() { assertEquals(Role.ReadWrite, user.getRole()); } + @Test + void visibility() { + WorkspaceConfiguration configuration = new WorkspaceConfiguration(); + assertNull(configuration.getVisibility()); + + configuration.setVisibility(Visibility.Private); + assertEquals(Visibility.Private, configuration.getVisibility()); + } + @Test void clearUsers() { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); From 2c0218be907fee0a748f99cf5092d271fcf3b5ff Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 28 Jul 2023 10:32:51 +0100 Subject: [PATCH 113/418] Updated for release. --- build.gradle | 2 +- docs/changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index a2810d4dd..a362d07a6 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.25.1' + version = '1.26.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 1898e9f4d..fbbcfec75 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.26.0 (unreleased to Maven Central) +## 1.26.0 (28th July 2023) - Adds the ability to specify the workspace visibility (private/public) via the workspace configuration. From c7e33ab4effad9d97d0f75dffc64e85a852070de Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 28 Jul 2023 11:06:50 +0100 Subject: [PATCH 114/418] Setter needs to be public. --- build.gradle | 2 +- docs/changelog.md | 2 +- .../structurizr/configuration/WorkspaceConfiguration.java | 6 +++--- .../configuration/WorkspaceConfigurationTests.java | 3 +++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index a362d07a6..69fb87957 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.26.0' + version = '1.26.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index fbbcfec75..15f4e97b6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.26.0 (28th July 2023) +## 1.26.1 (28th July 2023) - Adds the ability to specify the workspace visibility (private/public) via the workspace configuration. diff --git a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java index 18a87c961..d937b3c33 100644 --- a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java +++ b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java @@ -26,11 +26,11 @@ public Visibility getVisibility() { } /** - * Gets the visibility of this workspace (private or public). + * Sets the visibility of this workspace (private or public). * - * @param visibility a Visibility enum + * @param visibility a Visibility enum, or null to indicate that no changes should be made */ - void setVisibility(Visibility visibility) { + public void setVisibility(Visibility visibility) { this.visibility = visibility; } diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java index 529339033..c4a2f99f2 100644 --- a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -62,6 +62,9 @@ void visibility() { configuration.setVisibility(Visibility.Private); assertEquals(Visibility.Private, configuration.getVisibility()); + + configuration.setVisibility(null); + assertNull(configuration.getVisibility()); } @Test From 940a0e9b2533c5017c9fad9fef11e30a065af9f7 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 18 Sep 2023 16:56:27 +0100 Subject: [PATCH 115/418] - Upgrades dependencies, targets Java 17. --- build.gradle | 6 +++--- docs/changelog.md | 4 ++++ structurizr-client/build.gradle | 6 +++--- structurizr-core/build.gradle | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 69fb87957..5a0c3fe9d 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.26.1' + version = '1.27.0' repositories { mavenCentral() @@ -34,8 +34,8 @@ subprojects { proj -> compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 17 + targetCompatibility = 17 java { withJavadocJar() diff --git a/docs/changelog.md b/docs/changelog.md index 15f4e97b6..556c54910 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.27.0 (unreleased) + +- Upgrades dependencies, targets Java 17. + ## 1.26.1 (28th July 2023) - Adds the ability to specify the workspace visibility (private/public) via the workspace configuration. diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 77d8bbcf7..300d750ed 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,11 +2,11 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.14.2' + api 'com.fasterxml.jackson.core:jackson-databind:2.15.1' api 'org.apache.httpcomponents.client5:httpclient5:5.2.1' - api 'javax.xml.bind:jaxb-api:2.3.1' + api 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' } diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 54190e009..3ffe4688a 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,10 +1,10 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.14.2' + api 'com.fasterxml.jackson.core:jackson-annotations:2.15.1' api 'com.google.code.findbugs:jsr305:3.0.2' api 'commons-logging:commons-logging:1.2' - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' - testImplementation 'org.assertj:assertj-core:3.23.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.assertj:assertj-core:3.24.2' } \ No newline at end of file From fae8d71a832fa22e52305c98ec462c973a5126a5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 18 Sep 2023 16:58:16 +0100 Subject: [PATCH 116/418] Remove Java 11 build. --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b449b38bd..ab8bc8cd1 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '17' ] + java: [ '17' ] max-parallel: 1 steps: From fb58852a1b3e614c83696e1afc069460b25e5f3f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 19 Sep 2023 13:13:19 +0100 Subject: [PATCH 117/418] Adds a 'url' property to 'RelationshipView' (see https://github.com/structurizr/java/issues/214). --- docs/changelog.md | 1 + .../structurizr/view/RelationshipView.java | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 556c54910..f049cd2f2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,7 @@ ## 1.27.0 (unreleased) - Upgrades dependencies, targets Java 17. +- Adds a 'url' property to 'RelationshipView' (see https://github.com/structurizr/java/issues/214). ## 1.26.1 (28th July 2023) diff --git a/structurizr-core/src/com/structurizr/view/RelationshipView.java b/structurizr-core/src/com/structurizr/view/RelationshipView.java index 48b992c16..56f5c87a3 100644 --- a/structurizr-core/src/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/com/structurizr/view/RelationshipView.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; +import com.structurizr.util.Url; import java.util.Collection; import java.util.LinkedHashSet; @@ -20,6 +22,7 @@ public final class RelationshipView { private Relationship relationship; private String id; private String description; + private String url; private String order; private Boolean response; private Set vertices = new LinkedHashSet<>(); @@ -87,6 +90,35 @@ void setDescription(String description) { this.description = description; } + /** + * Gets the URL where more information about this relationship instance can be found. + * + * @return a URL as a String + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL where more information about this relationship instance can be found. + * + * @param url the URL as a String + * @throws IllegalArgumentException if the URL is not a well-formed URL + */ + public void setUrl(String url) { + if (StringUtils.isNullOrEmpty(url)) { + this.url = null; + } else { + if (url.startsWith(Url.WORKSPACE_URL_PREFIX)) { + this.url = url; + } else if (Url.isUrl(url)) { + this.url = url; + } else { + throw new IllegalArgumentException(url + " is not a valid URL."); + } + } + } + /** * Gets the order of this relationship (used in dynamic views only; e.g. 1.0, 1.1, 2.0, etc). * From 703be9071a7ec27938143d1fa30ef9e48bdcbc1c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 23 Oct 2023 11:26:48 +0100 Subject: [PATCH 118/418] Adds release date. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index f049cd2f2..7c290b7f6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.27.0 (unreleased) +## 1.27.0 (23rd October 2023) - Upgrades dependencies, targets Java 17. - Adds a 'url' property to 'RelationshipView' (see https://github.com/structurizr/java/issues/214). From 843b364958d43ce7af49fa6c632db4ae1e125733 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 12 Nov 2023 13:22:04 +0000 Subject: [PATCH 119/418] Adds a flag to determine whether automatic layout has been applied or not. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ .../com/structurizr/view/AutomaticLayout.java | 20 +++++++++++++++++++ .../view/AutomaticLayoutTests.java | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5a0c3fe9d..4fd305958 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.27.0' + version = '1.27.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 7c290b7f6..7386bb23b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.27.1 (unreleased) + +- Adds a flag to determine whether automatic layout has been applied or not. + ## 1.27.0 (23rd October 2023) - Upgrades dependencies, targets Java 17. diff --git a/structurizr-core/src/com/structurizr/view/AutomaticLayout.java b/structurizr-core/src/com/structurizr/view/AutomaticLayout.java index 8cf63a1c6..5b373d038 100644 --- a/structurizr-core/src/com/structurizr/view/AutomaticLayout.java +++ b/structurizr-core/src/com/structurizr/view/AutomaticLayout.java @@ -11,6 +11,7 @@ public final class AutomaticLayout { private int nodeSeparation; private int edgeSeparation; private boolean vertices; + private boolean applied; AutomaticLayout() { } @@ -22,6 +23,7 @@ public final class AutomaticLayout { setNodeSeparation(nodeSeparation); setEdgeSeparation(edgeSeparation); setVertices(vertices); + setApplied(false); } /** @@ -118,6 +120,24 @@ void setVertices(boolean vertices) { this.vertices = vertices; } + /** + * Returns whether automatic layout has been applied. + * + * @return true if automatic layout has been applied, false otherwise + */ + public boolean isApplied() { + return applied; + } + + /** + * Sets whether automatic layout has been applied. + * + * @param applied true if automatic layout has been applied, false otherwise + */ + public void setApplied(boolean applied) { + this.applied = applied; + } + public enum Implementation { Graphviz, Dagre diff --git a/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java b/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java index 3ffd208d9..c4f9c6269 100644 --- a/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java +++ b/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java @@ -15,6 +15,7 @@ void setAutomaticLayout() { assertEquals(200, automaticLayout.getNodeSeparation()); assertEquals(300, automaticLayout.getEdgeSeparation()); assertTrue(automaticLayout.isVertices()); + assertFalse(automaticLayout.isApplied()); } @Test From 7d55bbbbabcea69bbaa2bb3ff11a11a2fb1258d5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Nov 2023 10:47:59 +0000 Subject: [PATCH 120/418] Adds support for perspective values. --- build.gradle | 2 +- docs/changelog.md | 3 ++- .../src/com/structurizr/model/ModelItem.java | 19 ++++++++++++++++--- .../com/structurizr/model/Perspective.java | 18 ++++++++++++++++-- .../com/structurizr/model/ModelItemTests.java | 16 ++++++++++++---- 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 4fd305958..7cba07ffa 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.27.1' + version = '1.28.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 7386bb23b..c17704247 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,9 @@ # Changelog -## 1.27.1 (unreleased) +## 1.28.0 (unreleased) - Adds a flag to determine whether automatic layout has been applied or not. +- Adds support for perspective values. ## 1.27.0 (23rd October 2023) diff --git a/structurizr-core/src/com/structurizr/model/ModelItem.java b/structurizr-core/src/com/structurizr/model/ModelItem.java index 4cdf2d304..ac81b3918 100644 --- a/structurizr-core/src/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/com/structurizr/model/ModelItem.java @@ -189,11 +189,24 @@ void setPerspectives(Set perspectives) { * Adds a perspective to this model item. * * @param name the name of the perspective (e.g. "Security", must be unique) - * @param description a description of the perspective + * @param description the description of the perspective * @return a Perspective object * @throws IllegalArgumentException if perspective details are not specified, or the named perspective exists already */ public Perspective addPerspective(String name, String description) { + return addPerspective(name, description, ""); + } + + /** + * Adds a perspective to this model item. + * + * @param name the name of the perspective (e.g. "Technical Debt", must be unique) + * @param description the description of the perspective (e.g. "High") + * @param value the value of the perspective + * @return a Perspective object + * @throws IllegalArgumentException if perspective details are not specified, or the named perspective exists already + */ + public Perspective addPerspective(String name, String description, String value) { if (StringUtils.isNullOrEmpty(name)) { throw new IllegalArgumentException("A name must be specified."); } @@ -202,11 +215,11 @@ public Perspective addPerspective(String name, String description) { throw new IllegalArgumentException("A description must be specified."); } - if (perspectives.stream().filter(p -> p.getName().equals(name)).count() > 0) { + if (perspectives.stream().anyMatch(p -> p.getName().equals(name))) { throw new IllegalArgumentException("A perspective named \"" + name + "\" already exists."); } - Perspective perspective = new Perspective(name, description); + Perspective perspective = new Perspective(name, description, value); perspectives.add(perspective); return perspective; diff --git a/structurizr-core/src/com/structurizr/model/Perspective.java b/structurizr-core/src/com/structurizr/model/Perspective.java index 985944674..b20285392 100644 --- a/structurizr-core/src/com/structurizr/model/Perspective.java +++ b/structurizr-core/src/com/structurizr/model/Perspective.java @@ -8,14 +8,15 @@ public final class Perspective { private String name; private String description; - // todo link this perspective to architecture decision records + private String value; Perspective() { } - Perspective(String name, String description) { + Perspective(String name, String description, String value) { this.name = name; this.description = description; + this.value = value; } /** @@ -44,6 +45,19 @@ void setDescription(String description) { this.description = description; } + /** + * Gets the value of this perspective. + * + * @return the value of this perspective, as a String + */ + public String getValue() { + return value; + } + + void setValue(String value) { + this.value = value; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java index 7013a0227..9cb5565f7 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java @@ -200,10 +200,18 @@ void addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecified() { @Test void addPerspective_AddsAPerspective() { Element element = model.addSoftwareSystem("Name", "Description"); - Perspective perspective = element.addPerspective("Security", "Data is encrypted at rest."); - assertEquals("Security", perspective.getName()); - assertEquals("Data is encrypted at rest.", perspective.getDescription()); - assertTrue(element.getPerspectives().contains(perspective)); + + Perspective securityPerspective = element.addPerspective("Security", "Data is encrypted at rest."); + assertEquals("Security", securityPerspective.getName()); + assertEquals("Data is encrypted at rest.", securityPerspective.getDescription()); + assertEquals("", securityPerspective.getValue()); + assertTrue(element.getPerspectives().contains(securityPerspective)); + + Perspective technicalDebtPerspective = element.addPerspective("Technical Debt", "High tech debt due to feature X being delivered rapidly.", "High"); + assertEquals("Technical Debt", technicalDebtPerspective.getName()); + assertEquals("High tech debt due to feature X being delivered rapidly.", technicalDebtPerspective.getDescription()); + assertEquals("High", technicalDebtPerspective.getValue()); + assertTrue(element.getPerspectives().contains(technicalDebtPerspective)); } @Test From c73ecff9983bd5995f0a02f0587019d23e31de87 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Nov 2023 12:23:53 +0000 Subject: [PATCH 121/418] Adds the ability to scope and validate a workspace. --- docs/changelog.md | 1 + .../src/com/structurizr/Workspace.java | 20 ++++++ .../src/com/structurizr/WorkspaceScope.java | 8 +++ .../LandscapeWorkspaceScopeValidator.java | 26 ++++++++ ...SoftwareSystemWorkspaceScopeValidator.java | 21 +++++++ .../UndefinedWorkspaceScopeValidator.java | 12 ++++ .../WorkspaceScopeValidationException.java | 9 +++ .../validation/WorkspaceScopeValidator.java | 9 +++ .../WorkspaceScopeValidatorFactory.java | 18 ++++++ .../unit/com/structurizr/WorkspaceTests.java | 12 ++++ ...LandscapeWorkspaceScopeValidatorTests.java | 56 +++++++++++++++++ ...areSystemWorkspaceScopeValidatorTests.java | 61 +++++++++++++++++++ .../WorkspaceScopeValidatorFactoryTests.java | 23 +++++++ 13 files changed, 276 insertions(+) create mode 100644 structurizr-core/src/com/structurizr/WorkspaceScope.java create mode 100644 structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java create mode 100644 structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java create mode 100644 structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java create mode 100644 structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java create mode 100644 structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java create mode 100644 structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java create mode 100644 structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java create mode 100644 structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java create mode 100644 structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java diff --git a/docs/changelog.md b/docs/changelog.md index c17704247..ad8485f4f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,7 @@ - Adds a flag to determine whether automatic layout has been applied or not. - Adds support for perspective values. +- Adds the ability to scope and validate a workspace. ## 1.27.0 (23rd October 2023) diff --git a/structurizr-core/src/com/structurizr/Workspace.java b/structurizr-core/src/com/structurizr/Workspace.java index 3c2dc0334..e95cdd75a 100644 --- a/structurizr-core/src/com/structurizr/Workspace.java +++ b/structurizr-core/src/com/structurizr/Workspace.java @@ -23,6 +23,8 @@ public final class Workspace extends AbstractWorkspace implements Documentable { private static final Log log = LogFactory.getLog(Workspace.class); + private WorkspaceScope scope = null; + private Model model = createModel(); private ViewSet viewSet; private Documentation documentation; @@ -44,6 +46,24 @@ public Workspace(String name, String description) { documentation = new Documentation(); } + /** + * Gets the scope of this workspace + * + * @return a WorkspaceScope enum, or null if undefined + */ + public WorkspaceScope getScope() { + return scope; + } + + /** + * Sets the workspace scope. + * + * @param scope a WorkspaceScope enum, or null if undefined + */ + public void setScope(WorkspaceScope scope) { + this.scope = scope; + } + /** * Gets the software architecture model. * diff --git a/structurizr-core/src/com/structurizr/WorkspaceScope.java b/structurizr-core/src/com/structurizr/WorkspaceScope.java new file mode 100644 index 000000000..2f6ebdadc --- /dev/null +++ b/structurizr-core/src/com/structurizr/WorkspaceScope.java @@ -0,0 +1,8 @@ +package com.structurizr; + +public enum WorkspaceScope { + + Landscape, + SoftwareSystem + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java new file mode 100644 index 000000000..feb6b4abd --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java @@ -0,0 +1,26 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.model.Model; +import com.structurizr.model.SoftwareSystem; + +/** + * Validates that the workspace does not define containers and software system level documentation. + */ +public class LandscapeWorkspaceScopeValidator implements WorkspaceScopeValidator { + + @Override + public void validate(Workspace workspace) throws WorkspaceScopeValidationException { + Model model = workspace.getModel(); + for (SoftwareSystem softwareSystem : model.getSoftwareSystems()) { + if (softwareSystem.getContainers().size() > 0) { + throw new WorkspaceScopeValidationException("Workspace is landscape scoped, but the software system named " + softwareSystem.getName() + " has containers."); + } + + if (!softwareSystem.getDocumentation().isEmpty()) { + throw new WorkspaceScopeValidationException("Workspace is landscape scoped, but the software system named " + softwareSystem.getName() + " has documentation."); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java new file mode 100644 index 000000000..d2f7ce32b --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java @@ -0,0 +1,21 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.model.Model; + +/** + * Validates that the workspace only defines detail (containers and documentation) for a single software system. + */ +public class SoftwareSystemWorkspaceScopeValidator implements WorkspaceScopeValidator { + + @Override + public void validate(Workspace workspace) throws WorkspaceScopeValidationException { + Model model = workspace.getModel(); + long softwareSystemsWithContainersOrDocumentation = model.getSoftwareSystems().stream().filter(ss -> ss.getContainers().size() > 0 || !ss.getDocumentation().isEmpty()).count(); + + if (softwareSystemsWithContainersOrDocumentation > 1) { + throw new WorkspaceScopeValidationException("Workspace is software system scoped, but multiple software systems have containers and/or documentation defined."); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java new file mode 100644 index 000000000..ffd4d3865 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java @@ -0,0 +1,12 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; + +public class UndefinedWorkspaceScopeValidator implements WorkspaceScopeValidator { + + @Override + public void validate(Workspace workspace) throws WorkspaceScopeValidationException { + // no-op + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java new file mode 100644 index 000000000..5183f3d3b --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java @@ -0,0 +1,9 @@ +package com.structurizr.validation; + +public class WorkspaceScopeValidationException extends Exception { + + public WorkspaceScopeValidationException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java new file mode 100644 index 000000000..c54361736 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java @@ -0,0 +1,9 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; + +public interface WorkspaceScopeValidator { + + void validate(Workspace workspace) throws WorkspaceScopeValidationException; + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java new file mode 100644 index 000000000..b01491788 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java @@ -0,0 +1,18 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.WorkspaceScope; + +public final class WorkspaceScopeValidatorFactory { + + public static WorkspaceScopeValidator getValidator(Workspace workspace) { + if (workspace.getScope() == WorkspaceScope.Landscape) { + return new LandscapeWorkspaceScopeValidator(); + } else if (workspace.getScope() == WorkspaceScope.SoftwareSystem) { + return new SoftwareSystemWorkspaceScopeValidator(); + } else { + return new UndefinedWorkspaceScopeValidator(); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java index 651057bbe..055868a2e 100644 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java @@ -73,4 +73,16 @@ void hydrate_DoesNotCrash() { workspace.hydrate(); } + @Test + void scope() { + Workspace workspace = new Workspace("Name", "Description"); + assertNull(workspace.getScope()); // default scope is undefined + + workspace.setScope(WorkspaceScope.SoftwareSystem); + assertEquals(WorkspaceScope.SoftwareSystem, workspace.getScope()); + + workspace.setScope(null); + assertNull(workspace.getScope()); + } + } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java b/structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java new file mode 100644 index 000000000..9e473da2e --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java @@ -0,0 +1,56 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.validation.LandscapeWorkspaceScopeValidator; +import com.structurizr.validation.WorkspaceScopeValidationException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class LandscapeWorkspaceScopeValidatorTests { + + private final LandscapeWorkspaceScopeValidator validator = new LandscapeWorkspaceScopeValidator(); + + @Test + void validate() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + validator.validate(workspace); + + workspace.getModel().addPerson("User"); + validator.validate(workspace); + + workspace.getModel().addSoftwareSystem("A"); + validator.validate(workspace); + + workspace.getModel().addSoftwareSystem("B"); + validator.validate(workspace); + } + + @Test + void validate_ThrowsAnException_WhenContainersAreDefined() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").addContainer("AA"); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is landscape scoped, but the software system named A has containers.", e.getMessage()); + } + } + + @Test + void validate_ThrowsAnException_WhenSoftwareSystemDocumentationIsDefined() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is landscape scoped, but the software system named A has documentation.", e.getMessage()); + } + } + +} diff --git a/structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java b/structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java new file mode 100644 index 000000000..be87ea63a --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java @@ -0,0 +1,61 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.validation.SoftwareSystemWorkspaceScopeValidator; +import com.structurizr.validation.WorkspaceScopeValidationException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class SoftwareSystemWorkspaceScopeValidatorTests { + + private final SoftwareSystemWorkspaceScopeValidator validator = new SoftwareSystemWorkspaceScopeValidator(); + + @Test + void validate() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + validator.validate(workspace); + + workspace.getModel().addPerson("User"); + validator.validate(workspace); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + a.addContainer("AA"); + a.getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + validator.validate(workspace); + + workspace.getModel().addSoftwareSystem("B"); + validator.validate(workspace); + } + + @Test + void validate_ThrowsAnException_WhenMultipleSoftwareSystemsDefineContainers() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").addContainer("AA"); + workspace.getModel().addSoftwareSystem("B").addContainer("BB"); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is software system scoped, but multiple software systems have containers and/or documentation defined.", e.getMessage()); + } + } + + @Test + void validate_ThrowsAnException_WhenMultipleSoftwareSystemsDefineDocumentation() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + workspace.getModel().addSoftwareSystem("B").getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is software system scoped, but multiple software systems have containers and/or documentation defined.", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java b/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java new file mode 100644 index 000000000..a8b9e4a84 --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java @@ -0,0 +1,23 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.WorkspaceScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WorkspaceScopeValidatorFactoryTests { + + @Test + void getValidator() { + Workspace workspace = new Workspace("Name", "Description"); + assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof UndefinedWorkspaceScopeValidator); + + workspace.setScope(WorkspaceScope.Landscape); + assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof LandscapeWorkspaceScopeValidator); + + workspace.setScope(WorkspaceScope.SoftwareSystem); + assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof SoftwareSystemWorkspaceScopeValidator); + } + +} \ No newline at end of file From f976dac160914ccda7f7ae1fd9e4c580522e931b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Nov 2023 12:36:11 +0000 Subject: [PATCH 122/418] Move scope to workspace configuration. --- .../src/com/structurizr/Workspace.java | 20 ------------------ .../configuration/WorkspaceConfiguration.java | 21 +++++++++++++++++-- .../{ => configuration}/WorkspaceScope.java | 2 +- .../WorkspaceScopeValidatorFactory.java | 6 +++--- .../unit/com/structurizr/WorkspaceTests.java | 12 ----------- .../WorkspaceConfigurationTests.java | 12 +++++++++++ .../WorkspaceScopeValidatorFactoryTests.java | 6 +++--- 7 files changed, 38 insertions(+), 41 deletions(-) rename structurizr-core/src/com/structurizr/{ => configuration}/WorkspaceScope.java (63%) diff --git a/structurizr-core/src/com/structurizr/Workspace.java b/structurizr-core/src/com/structurizr/Workspace.java index e95cdd75a..3c2dc0334 100644 --- a/structurizr-core/src/com/structurizr/Workspace.java +++ b/structurizr-core/src/com/structurizr/Workspace.java @@ -23,8 +23,6 @@ public final class Workspace extends AbstractWorkspace implements Documentable { private static final Log log = LogFactory.getLog(Workspace.class); - private WorkspaceScope scope = null; - private Model model = createModel(); private ViewSet viewSet; private Documentation documentation; @@ -46,24 +44,6 @@ public Workspace(String name, String description) { documentation = new Documentation(); } - /** - * Gets the scope of this workspace - * - * @return a WorkspaceScope enum, or null if undefined - */ - public WorkspaceScope getScope() { - return scope; - } - - /** - * Sets the workspace scope. - * - * @param scope a WorkspaceScope enum, or null if undefined - */ - public void setScope(WorkspaceScope scope) { - this.scope = scope; - } - /** * Gets the software architecture model. * diff --git a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java index d937b3c33..bd19777a4 100644 --- a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java +++ b/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java @@ -1,7 +1,5 @@ package com.structurizr.configuration; -import com.structurizr.util.StringUtils; - import java.util.HashSet; import java.util.Set; @@ -10,12 +8,31 @@ */ public final class WorkspaceConfiguration { + private WorkspaceScope scope = null; private Visibility visibility = null; private Set users = new HashSet<>(); WorkspaceConfiguration() { } + /** + * Gets the scope of this workspace + * + * @return a WorkspaceScope enum, or null if undefined + */ + public WorkspaceScope getScope() { + return scope; + } + + /** + * Sets the workspace scope. + * + * @param scope a WorkspaceScope enum, or null if undefined + */ + public void setScope(WorkspaceScope scope) { + this.scope = scope; + } + /** * Gets the visibility of this workspace (private or public). * diff --git a/structurizr-core/src/com/structurizr/WorkspaceScope.java b/structurizr-core/src/com/structurizr/configuration/WorkspaceScope.java similarity index 63% rename from structurizr-core/src/com/structurizr/WorkspaceScope.java rename to structurizr-core/src/com/structurizr/configuration/WorkspaceScope.java index 2f6ebdadc..8d271c504 100644 --- a/structurizr-core/src/com/structurizr/WorkspaceScope.java +++ b/structurizr-core/src/com/structurizr/configuration/WorkspaceScope.java @@ -1,4 +1,4 @@ -package com.structurizr; +package com.structurizr.configuration; public enum WorkspaceScope { diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java index b01491788..ce011e601 100644 --- a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java +++ b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java @@ -1,14 +1,14 @@ package com.structurizr.validation; import com.structurizr.Workspace; -import com.structurizr.WorkspaceScope; +import com.structurizr.configuration.WorkspaceScope; public final class WorkspaceScopeValidatorFactory { public static WorkspaceScopeValidator getValidator(Workspace workspace) { - if (workspace.getScope() == WorkspaceScope.Landscape) { + if (workspace.getConfiguration().getScope() == WorkspaceScope.Landscape) { return new LandscapeWorkspaceScopeValidator(); - } else if (workspace.getScope() == WorkspaceScope.SoftwareSystem) { + } else if (workspace.getConfiguration().getScope() == WorkspaceScope.SoftwareSystem) { return new SoftwareSystemWorkspaceScopeValidator(); } else { return new UndefinedWorkspaceScopeValidator(); diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java index 055868a2e..651057bbe 100644 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java @@ -73,16 +73,4 @@ void hydrate_DoesNotCrash() { workspace.hydrate(); } - @Test - void scope() { - Workspace workspace = new Workspace("Name", "Description"); - assertNull(workspace.getScope()); // default scope is undefined - - workspace.setScope(WorkspaceScope.SoftwareSystem); - assertEquals(WorkspaceScope.SoftwareSystem, workspace.getScope()); - - workspace.setScope(null); - assertNull(workspace.getScope()); - } - } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java index c4a2f99f2..bdc982e9a 100644 --- a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java +++ b/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -55,6 +55,18 @@ void addUser_AddsAUser() { assertEquals(Role.ReadWrite, user.getRole()); } + @Test + void scope() { + WorkspaceConfiguration configuration = new WorkspaceConfiguration(); + assertNull(configuration.getScope()); // default scope is undefined + + configuration.setScope(WorkspaceScope.SoftwareSystem); + assertEquals(WorkspaceScope.SoftwareSystem, configuration.getScope()); + + configuration.setScope(null); + assertNull(configuration.getScope()); + } + @Test void visibility() { WorkspaceConfiguration configuration = new WorkspaceConfiguration(); diff --git a/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java b/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java index a8b9e4a84..dd79b0b7e 100644 --- a/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java +++ b/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java @@ -1,7 +1,7 @@ package com.structurizr.validation; import com.structurizr.Workspace; -import com.structurizr.WorkspaceScope; +import com.structurizr.configuration.WorkspaceScope; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -13,10 +13,10 @@ void getValidator() { Workspace workspace = new Workspace("Name", "Description"); assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof UndefinedWorkspaceScopeValidator); - workspace.setScope(WorkspaceScope.Landscape); + workspace.getConfiguration().setScope(WorkspaceScope.Landscape); assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof LandscapeWorkspaceScopeValidator); - workspace.setScope(WorkspaceScope.SoftwareSystem); + workspace.getConfiguration().setScope(WorkspaceScope.SoftwareSystem); assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof SoftwareSystemWorkspaceScopeValidator); } From 6dad755767ef24b463bdc2d06a76ce3d0e7d705a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Nov 2023 12:36:23 +0000 Subject: [PATCH 123/418] Bump dependencies. --- structurizr-client/build.gradle | 2 +- structurizr-core/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 300d750ed..def7a58e6 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,7 +2,7 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.15.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.16.0' api 'org.apache.httpcomponents.client5:httpclient5:5.2.1' api 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 3ffe4688a..dae5405a4 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,6 +1,6 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.15.1' + api 'com.fasterxml.jackson.core:jackson-annotations:2.16.0' api 'com.google.code.findbugs:jsr305:3.0.2' api 'commons-logging:commons-logging:1.2' From a8a4e43954602c58ac0dd6db297d5e5b2d7a85d2 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Nov 2023 12:52:56 +0000 Subject: [PATCH 124/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index ad8485f4f..43bd669e9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.28.0 (unreleased) +## 1.28.0 (19th November 2023) - Adds a flag to determine whether automatic layout has been applied or not. - Adds support for perspective values. From c99d42ed1fc504fe181fdddc2039a35e71ed3aed Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 11 Dec 2023 09:20:14 +0000 Subject: [PATCH 125/418] `AbstractWorkspace.clearConfiguration()` creates a new instance rather than nulling it. --- build.gradle | 2 +- docs/changelog.md | 4 ++++ structurizr-core/src/com/structurizr/AbstractWorkspace.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7cba07ffa..4adaad5f2 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.28.0' + version = '1.28.1' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 43bd669e9..23b40bc47 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.28.1 (11th December 2023) + +- `AbstractWorkspace.clearConfiguration()` creates a new instance rather than nulling it. + ## 1.28.0 (19th November 2023) - Adds a flag to determine whether automatic layout has been applied or not. diff --git a/structurizr-core/src/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/com/structurizr/AbstractWorkspace.java index c78f9c0fd..82f750ace 100644 --- a/structurizr-core/src/com/structurizr/AbstractWorkspace.java +++ b/structurizr-core/src/com/structurizr/AbstractWorkspace.java @@ -228,7 +228,7 @@ private WorkspaceConfiguration createWorkspaceConfiguration() { * Clears the configuration associated with this workspace. */ public void clearConfiguration() { - this.configuration = null; + this.configuration = createWorkspaceConfiguration(); } /** From 8a7da17d58e82881ded40901ae9080363b0fe4ad Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 11 Dec 2023 09:22:55 +0000 Subject: [PATCH 126/418] Fix test. --- .../com/structurizr/encryption/EncryptedWorkspaceTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java b/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java index a7e508781..f52794996 100644 --- a/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java +++ b/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java @@ -39,7 +39,8 @@ void construction_WhenTwoParametersAreSpecified() throws Exception { assertEquals("structurizr-java", encryptedWorkspace.getLastModifiedAgent()); assertEquals(1234, encryptedWorkspace.getId()); assertEquals("user@domain.com", encryptedWorkspace.getConfiguration().getUsers().iterator().next().getUsername()); - assertNull(workspace.getConfiguration()); + assertNotNull(workspace.getConfiguration()); + assertTrue(workspace.getConfiguration().getUsers().isEmpty()); assertSame(workspace, encryptedWorkspace.getWorkspace()); assertSame(encryptionStrategy, encryptedWorkspace.getEncryptionStrategy()); From 1a90ec8420229030a27efba668d2c623499e5cf1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 22 Dec 2023 13:34:34 +0000 Subject: [PATCH 127/418] Adds `com.structurizr.api.AdminApiClient` as a client for the cloud service/on-premises admin APIs. --- build.gradle | 2 +- docs/changelog.md | 4 + settings.gradle | 2 +- .../structurizr/api/AbstractApiClient.java | 54 ++ .../com/structurizr/api/AdminApiClient.java | 156 ++++++ .../structurizr/api/StructurizrClient.java | 505 +----------------- .../structurizr/api/WorkspaceApiClient.java | 457 ++++++++++++++++ .../structurizr/api/WorkspaceMetadata.java | 82 +++ .../src/com/structurizr/api/Workspaces.java | 28 + 9 files changed, 788 insertions(+), 502 deletions(-) create mode 100644 structurizr-client/src/com/structurizr/api/AbstractApiClient.java create mode 100644 structurizr-client/src/com/structurizr/api/AdminApiClient.java create mode 100644 structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java create mode 100644 structurizr-client/src/com/structurizr/api/WorkspaceMetadata.java create mode 100644 structurizr-client/src/com/structurizr/api/Workspaces.java diff --git a/build.gradle b/build.gradle index 4adaad5f2..b1052523f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.28.1' + version = '1.29.0' repositories { mavenCentral() diff --git a/docs/changelog.md b/docs/changelog.md index 23b40bc47..7e2e1a926 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 1.29.0 (unreleased) + +- Adds `com.structurizr.api.AdminApiClient` as a client for the cloud service/on-premises admin APIs. + ## 1.28.1 (11th December 2023) - `AbstractWorkspace.clearConfiguration()` creates a new instance rather than nulling it. diff --git a/settings.gradle b/settings.gradle index 8872b5782..62816092a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -rootProject.name = 'structurizr' +rootProject.name = 'structurizr-java' include 'structurizr-client' include 'structurizr-core' \ No newline at end of file diff --git a/structurizr-client/src/com/structurizr/api/AbstractApiClient.java b/structurizr-client/src/com/structurizr/api/AbstractApiClient.java new file mode 100644 index 000000000..df6086c1b --- /dev/null +++ b/structurizr-client/src/com/structurizr/api/AbstractApiClient.java @@ -0,0 +1,54 @@ +package com.structurizr.api; + +import com.structurizr.util.StringUtils; + +public abstract class AbstractApiClient { + + protected static final String VERSION = Package.getPackage("com.structurizr.api").getImplementationVersion(); + protected static final String STRUCTURIZR_FOR_JAVA_AGENT = "structurizr-java/" + VERSION; + + protected static final String STRUCTURIZR_CLOUD_SERVICE_API_URL = "https://api.structurizr.com"; + protected static final String WORKSPACE_PATH = "/workspace"; + + protected String url; + protected String agent = STRUCTURIZR_FOR_JAVA_AGENT; + + String getUrl() { + return url; + } + + protected void setUrl(String url) { + if (url == null || url.trim().length() == 0) { + throw new IllegalArgumentException("The API URL must not be null or empty."); + } + + if (url.endsWith("/")) { + this.url = url.substring(0, url.length() - 1); + } else { + this.url = url; + } + } + + /** + * Gets the agent string used to identify this client instance. + * + * @return "structurizr-java/{version}", unless overridden + */ + public String getAgent() { + return agent; + } + + /** + * Sets the agent string used to identify this client instance. + * + * @param agent the agent string + */ + public void setAgent(String agent) { + if (StringUtils.isNullOrEmpty(agent)) { + throw new IllegalArgumentException("An agent must be provided."); + } + + this.agent = agent.trim(); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/com/structurizr/api/AdminApiClient.java b/structurizr-client/src/com/structurizr/api/AdminApiClient.java new file mode 100644 index 000000000..cb84ff595 --- /dev/null +++ b/structurizr-client/src/com/structurizr/api/AdminApiClient.java @@ -0,0 +1,156 @@ +package com.structurizr.api; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.structurizr.util.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hc.core5.http.HttpStatus; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +/** + * A client for the Structurizr Admin API. + */ +public class AdminApiClient extends AbstractApiClient { + + private static final Log log = LogFactory.getLog(AdminApiClient.class); + + private final String username; + private final String apiKey; + + /** + * Creates a new API client with the specified on-premises API URL and key. + * + * @param url the URL of your Structurizr instance + * @param username the username (only required for the Structurizr cloud service) + * @param apiKey the API key of your workspace + */ + public AdminApiClient(String url, String username, String apiKey) { + setUrl(url); + + this.username = username; + + if (apiKey == null || apiKey.trim().length() == 0) { + throw new IllegalArgumentException("The API key must not be null or empty."); + } + + this.apiKey = apiKey; + } + + /** + * Gets a list of all workspaces. + * + * @return a List of WorkspaceMetadata objects + * @throws StructurizrClientException if an error occurs + */ + public List getWorkspaces() throws StructurizrClientException { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + WORKSPACE_PATH)) + .header(HttpHeaders.AUTHORIZATION, createAuthorizationHeader()) + .header(HttpHeaders.USER_AGENT, agent) + .build(); + HttpClient client = HttpClient.newHttpClient(); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + String json = response.body(); + + if (response.statusCode() == HttpStatus.SC_OK) { + Workspaces workspaces = objectMapper.readValue(response.body(), Workspaces.class); + return workspaces.getWorkspaces(); + } else { + ApiResponse apiResponse = ApiResponse.parse(json); + throw new StructurizrClientException(apiResponse.getMessage()); + } + } catch (Exception e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + /** + * Creates a new workspace. + * + * @return a WorkspaceMetadata object representing the new workspace + * @throws StructurizrClientException if an error occurs + */ + public WorkspaceMetadata createWorkspace() throws StructurizrClientException { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + WORKSPACE_PATH)) + .POST(HttpRequest.BodyPublishers.noBody()) + .header(HttpHeaders.AUTHORIZATION, createAuthorizationHeader()) + .header(HttpHeaders.USER_AGENT, agent) + .build(); + HttpClient client = HttpClient.newHttpClient(); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + String json = response.body(); + + if (response.statusCode() == HttpStatus.SC_OK) { + return objectMapper.readValue(json, WorkspaceMetadata.class); + } else { + ApiResponse apiResponse = ApiResponse.parse(json); + throw new StructurizrClientException(apiResponse.getMessage()); + } + } catch (Exception e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + /** + * Deletes a workspace. + * + * @param workspaceId the ID of the workspace to delete + * @throws StructurizrClientException if an error occurs + */ + public void deleteWorkspace(int workspaceId) throws StructurizrClientException { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + WORKSPACE_PATH + "/" + workspaceId)) + .DELETE() + .header(HttpHeaders.AUTHORIZATION, createAuthorizationHeader()) + .header(HttpHeaders.USER_AGENT, agent) + .build(); + HttpClient client = HttpClient.newHttpClient(); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + String json = response.body(); + + if (response.statusCode() == HttpStatus.SC_OK) { + ApiResponse apiResponse = ApiResponse.parse(json); + log.debug(apiResponse.getMessage()); + } else { + ApiResponse apiResponse = ApiResponse.parse(json); + throw new StructurizrClientException(apiResponse.getMessage()); + } + } catch (Exception e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + private String createAuthorizationHeader() { + if (StringUtils.isNullOrEmpty(username)) { + return apiKey; + } else { + return username + ":" + apiKey; + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/com/structurizr/api/StructurizrClient.java b/structurizr-client/src/com/structurizr/api/StructurizrClient.java index 1368170b6..b7ce1fcd9 100644 --- a/structurizr-client/src/com/structurizr/api/StructurizrClient.java +++ b/structurizr-client/src/com/structurizr/api/StructurizrClient.java @@ -1,515 +1,20 @@ package com.structurizr.api; -import com.structurizr.Workspace; -import com.structurizr.encryption.EncryptedWorkspace; -import com.structurizr.encryption.EncryptionLocation; -import com.structurizr.encryption.EncryptionStrategy; -import com.structurizr.io.json.EncryptedJsonReader; -import com.structurizr.io.json.EncryptedJsonWriter; -import com.structurizr.io.json.JsonReader; -import com.structurizr.io.json.JsonWriter; -import com.structurizr.model.IdGenerator; -import com.structurizr.util.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.hc.client5.http.classic.methods.HttpDelete; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.classic.methods.HttpPut; -import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.apache.hc.core5.http.io.entity.StringEntity; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.io.StringWriter; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.text.SimpleDateFormat; -import java.util.Base64; -import java.util.Date; -import java.util.Properties; - /** - * A client for the Structurizr API (https://api.structurizr.com) - * that allows you to get and put Structurizr workspaces in a JSON format. + * A client for the Structurizr workspace API that allows you to get and put Structurizr workspaces in a JSON format. */ -public final class StructurizrClient { - - private static final Log log = LogFactory.getLog(StructurizrClient.class); - - private static final String VERSION = Package.getPackage("com.structurizr.api").getImplementationVersion(); - private static final String STRUCTURIZR_FOR_JAVA_AGENT = "structurizr-java/" + VERSION; - - private static final String STRUCTURIZR_CLOUD_API_URL = "https://api.structurizr.com"; - - private static final String STRUCTURIZR_API_URL = "structurizr.api.url"; - private static final String STRUCTURIZR_API_KEY = "structurizr.api.key"; - private static final String STRUCTURIZR_API_SECRET = "structurizr.api.secret"; +public class StructurizrClient extends WorkspaceApiClient { - private static final String WORKSPACE_PATH = "/workspace/"; - - private String agent = STRUCTURIZR_FOR_JAVA_AGENT; - private String user; - - private String url; - private String apiKey; - private String apiSecret; - - private EncryptionStrategy encryptionStrategy; - - private IdGenerator idGenerator = null; - private boolean mergeFromRemote = true; - private File workspaceArchiveLocation = new File("."); - - /** - * Creates a new Structurizr client based upon configuration in a structurizr.properties file - * on the classpath with the following name-value pairs: - * - structurizr.api.url - * - structurizr.api.key - * - structurizr.api.secret - * - * @throws StructurizrClientException if something goes wrong - */ public StructurizrClient() throws StructurizrClientException { - try (InputStream in = - StructurizrClient.class.getClassLoader().getResourceAsStream("structurizr.properties")) { - Properties properties = new Properties(); - if (in != null) { - properties.load(in); - - setUrl(properties.getProperty(STRUCTURIZR_API_URL)); - setApiKey(properties.getProperty(STRUCTURIZR_API_KEY)); - setApiSecret(properties.getProperty(STRUCTURIZR_API_SECRET)); - } else { - throw new StructurizrClientException("Could not find a structurizr.properties file on the classpath."); - } - } catch (IOException e) { - log.error(e); - throw new StructurizrClientException(e); - } + super(); } - /** - * Creates a new Structurizr API client with the specified API key and secret, - * for the default API URL (https://api.structurizr.com). - * - * @param apiKey the API key of your workspace - * @param apiSecret the API secret of your workspace - */ public StructurizrClient(String apiKey, String apiSecret) { - this(STRUCTURIZR_CLOUD_API_URL, apiKey, apiSecret); + super(apiKey, apiSecret); } - /** - * Creates a new Structurizr client with the specified API URL, key and secret. - * - * @param url the URL of your Structurizr instance - * @param apiKey the API key of your workspace - * @param apiSecret the API secret of your workspace - */ public StructurizrClient(String url, String apiKey, String apiSecret) { - setUrl(url); - setApiKey(apiKey); - setApiSecret(apiSecret); - } - - /** - * Sets the ID generator to use when parsing a JSON workspace definition. - * - * @param idGenerator an IdGenerator implementation - */ - public void setIdGenerator(IdGenerator idGenerator) { - this.idGenerator = idGenerator; - } - - /** - * Gets the agent string used to identify this client instance. - * - * @return "structurizr-java/{version}", unless overridden - */ - public String getAgent() { - return agent; - } - - /** - * Sets the agent string used to identify this client instance. - * - * @param agent the agent string - */ - public void setAgent(String agent) { - if (StringUtils.isNullOrEmpty(agent)) { - throw new IllegalArgumentException("An agent must be provided."); - } - - this.agent = agent.trim(); - } - - /** - * Gets the API URL that this client is for. - * - * @return the API URL, as a String - */ - public String getUrl() { - return this.url; - } - - private void setUrl(String url) { - if (url == null || url.trim().length() == 0) { - throw new IllegalArgumentException("The API URL must not be null or empty."); - } - - if (url.endsWith("/")) { - this.url = url.substring(0, url.length() - 1); - } else { - this.url = url; - } - } - - String getApiKey() { - return apiKey; - } - - private void setApiKey(String apiKey) { - if (apiKey == null || apiKey.trim().length() == 0) { - throw new IllegalArgumentException("The API key must not be null or empty."); - } - - this.apiKey = apiKey; - } - - String getApiSecret() { - return apiSecret; - } - - private void setApiSecret(String apiSecret) { - if (apiSecret == null || apiSecret.trim().length() == 0) { - throw new IllegalArgumentException("The API secret must not be null or empty."); - } - - this.apiSecret = apiSecret; - } - - /** - * Gets the location where a copy of the workspace is archived when it is retrieved from the server. - * - * @return a File instance representing a directory, or null if this client instance is not archiving - */ - public File getWorkspaceArchiveLocation() { - return this.workspaceArchiveLocation; - } - - /** - * Sets the location where a copy of the workspace will be archived whenever it is retrieved from - * the server. Set this to null if you don't want archiving. - * - * @param workspaceArchiveLocation a File instance representing a directory, or null if - * you don't want archiving - */ - public void setWorkspaceArchiveLocation(File workspaceArchiveLocation) { - this.workspaceArchiveLocation = workspaceArchiveLocation; - } - - /** - * Sets the encryption strategy for use when getting or putting workspaces. - * - * @param encryptionStrategy an EncryptionStrategy implementation - */ - public void setEncryptionStrategy(EncryptionStrategy encryptionStrategy) { - this.encryptionStrategy = encryptionStrategy; - } - - /** - * Specifies whether the layout of diagrams from a remote workspace should be retained when putting - * a new version of the workspace. - * - * @param mergeFromRemote true if layout information should be merged from the remote workspace, false otherwise - */ - public void setMergeFromRemote(boolean mergeFromRemote) { - this.mergeFromRemote = mergeFromRemote; - } - - /** - * Locks the workspace with the given ID. - * - * @param workspaceId the ID of your workspace - * @return true if the workspace could be locked, false otherwise - * @throws StructurizrClientException if there are problems related to the network, authorization, etc - */ - public boolean lockWorkspace(long workspaceId) throws StructurizrClientException { - return manageLockForWorkspace(workspaceId, true); - } - - /** - * Unlocks the workspace with the given ID. - * - * @param workspaceId the ID of your workspace - * @return true if the workspace could be unlocked, false otherwise - * @throws StructurizrClientException if there are problems related to the network, authorization, etc - */ - public boolean unlockWorkspace(long workspaceId) throws StructurizrClientException { - return manageLockForWorkspace(workspaceId, false); - } - - private boolean manageLockForWorkspace(long workspaceId, boolean lock) throws StructurizrClientException { - if (workspaceId <= 0) { - throw new IllegalArgumentException("The workspace ID must be a positive integer."); - } - - try (CloseableHttpClient httpClient = HttpClients.createSystem()) { - HttpUriRequestBase httpRequest; - - if (lock) { - log.info("Locking workspace with ID " + workspaceId); - httpRequest = new HttpPut(url + WORKSPACE_PATH + workspaceId + "/lock?user=" + getUser() + "&agent=" + agent); - } else { - log.info("Unlocking workspace with ID " + workspaceId); - httpRequest = new HttpDelete(url + WORKSPACE_PATH + workspaceId + "/lock?user=" + getUser() + "&agent=" + agent); - } - - addHeaders(httpRequest, "", ""); - debugRequest(httpRequest, null); - - try (CloseableHttpResponse response = httpClient.execute(httpRequest)) { - debugResponse(response); - - String responseText = EntityUtils.toString(response.getEntity()); - ApiResponse apiResponse = ApiResponse.parse(responseText); - log.info(responseText); - - if (response.getCode() == HttpStatus.SC_OK) { - return apiResponse.isSuccess(); - } else { - throw new StructurizrClientException(apiResponse.getMessage()); - } - } - } catch (Exception e) { - log.error(e); - throw new StructurizrClientException(e); - } - } - - /** - * Gets the workspace with the given ID. - * - * @param workspaceId the workspace ID - * @return a Workspace instance - * @throws StructurizrClientException if there are problems related to the network, authorization, JSON deserialization, etc - */ - public Workspace getWorkspace(long workspaceId) throws StructurizrClientException { - if (workspaceId <= 0) { - throw new IllegalArgumentException("The workspace ID must be a positive integer."); - } - - try (CloseableHttpClient httpClient = HttpClients.createSystem()) { - log.info("Getting workspace with ID " + workspaceId); - HttpGet httpGet = new HttpGet(url + WORKSPACE_PATH + workspaceId); - addHeaders(httpGet, "", ""); - debugRequest(httpGet, null); - - try (CloseableHttpResponse response = httpClient.execute(httpGet)) { - debugResponse(response); - - String json = EntityUtils.toString(response.getEntity()); - if (response.getCode() == HttpStatus.SC_OK) { - archiveWorkspace(workspaceId, json); - - if (encryptionStrategy == null) { - if (json.contains("\"encryptionStrategy\"") && json.contains("\"ciphertext\"")) { - log.warn("The JSON may contain a client-side encrypted workspace, but no passphrase has been specified."); - } - - JsonReader jsonReader = new JsonReader(); - jsonReader.setIdGenerator(idGenerator); - return jsonReader.read(new StringReader(json)); - } else { - EncryptedWorkspace encryptedWorkspace = new EncryptedJsonReader().read(new StringReader(json)); - - if (encryptedWorkspace.getEncryptionStrategy() != null) { - encryptedWorkspace.getEncryptionStrategy().setPassphrase(encryptionStrategy.getPassphrase()); - return encryptedWorkspace.getWorkspace(); - } else { - // this workspace isn't encrypted, even though the client has an encryption strategy set - JsonReader jsonReader = new JsonReader(); - jsonReader.setIdGenerator(idGenerator); - return jsonReader.read(new StringReader(json)); - } - } - } else { - ApiResponse apiResponse = ApiResponse.parse(json); - throw new StructurizrClientException(apiResponse.getMessage()); - } - } - } catch (Exception e) { - log.error(e); - throw new StructurizrClientException(e); - } - } - - /** - * Updates the given workspace. - * - * @param workspaceId the workspace ID - * @param workspace the workspace instance to update - * @throws StructurizrClientException if there are problems related to the network, authorization, JSON serialization, etc - */ - public void putWorkspace(long workspaceId, Workspace workspace) throws StructurizrClientException { - if (workspace == null) { - throw new IllegalArgumentException("The workspace must not be null."); - } else if (workspaceId <= 0) { - throw new IllegalArgumentException("The workspace ID must be a positive integer."); - } - - try (CloseableHttpClient httpClient = HttpClients.createSystem()) { - if (mergeFromRemote) { - Workspace remoteWorkspace = getWorkspace(workspaceId); - if (remoteWorkspace != null) { - workspace.getViews().copyLayoutInformationFrom(remoteWorkspace.getViews()); - workspace.getViews().getConfiguration().copyConfigurationFrom(remoteWorkspace.getViews().getConfiguration()); - } - } - - workspace.setId(workspaceId); - workspace.setThumbnail(null); - workspace.setLastModifiedDate(new Date()); - workspace.setLastModifiedAgent(agent); - workspace.setLastModifiedUser(getUser()); - - workspace.countAndLogWarnings(); - - HttpPut httpPut = new HttpPut(url + WORKSPACE_PATH + workspaceId); - - StringWriter stringWriter = new StringWriter(); - if (encryptionStrategy == null) { - JsonWriter jsonWriter = new JsonWriter(false); - jsonWriter.write(workspace, stringWriter); - } else { - EncryptedWorkspace encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); - encryptionStrategy.setLocation(EncryptionLocation.Client); - EncryptedJsonWriter jsonWriter = new EncryptedJsonWriter(false); - jsonWriter.write(encryptedWorkspace, stringWriter); - } - - StringEntity stringEntity = new StringEntity(stringWriter.toString(), ContentType.APPLICATION_JSON); - httpPut.setEntity(stringEntity); - addHeaders(httpPut, EntityUtils.toString(stringEntity), ContentType.APPLICATION_JSON.toString()); - - debugRequest(httpPut, EntityUtils.toString(stringEntity)); - - log.info("Putting workspace with ID " + workspaceId); - try (CloseableHttpResponse response = httpClient.execute(httpPut)) { - String json = EntityUtils.toString(response.getEntity()); - if (response.getCode() == HttpStatus.SC_OK) { - debugResponse(response); - log.info(json); - } else { - ApiResponse apiResponse = ApiResponse.parse(json); - throw new StructurizrClientException(apiResponse.getMessage()); - } - } - } catch (Exception e) { - log.error(e); - throw new StructurizrClientException(e); - } - } - - private void debugRequest(HttpUriRequestBase httpRequest, String content) { - if (log.isDebugEnabled()) { - log.debug(httpRequest.getMethod() + " " + httpRequest.getPath()); - Header[] headers = httpRequest.getHeaders(); - for (Header header : headers) { - log.debug(header.getName() + ": " + header.getValue()); - } - if (content != null) { - log.debug(content); - } - } - } - - private void debugResponse(CloseableHttpResponse response) { - log.debug(response.getCode()); - } - - private void addHeaders(HttpUriRequestBase httpRequest, String content, String contentType) throws Exception { - String httpMethod = httpRequest.getMethod(); - String path = httpRequest.getPath(); - String contentMd5 = new Md5Digest().generate(content); - String nonce = "" + System.currentTimeMillis(); - - HashBasedMessageAuthenticationCode hmac = new HashBasedMessageAuthenticationCode(apiSecret); - HmacContent hmacContent = new HmacContent(httpMethod, path, contentMd5, contentType, nonce); - httpRequest.addHeader(HttpHeaders.USER_AGENT, agent); - httpRequest.addHeader(HttpHeaders.AUTHORIZATION, new HmacAuthorizationHeader(apiKey, hmac.generate(hmacContent.toString())).format()); - httpRequest.addHeader(HttpHeaders.NONCE, nonce); - - if (httpMethod.equals("PUT")) { - httpRequest.addHeader(HttpHeaders.CONTENT_MD5, Base64.getEncoder().encodeToString(contentMd5.getBytes("UTF-8"))); - httpRequest.addHeader(HttpHeaders.CONTENT_TYPE, contentType); - } - } - - private void archiveWorkspace(long workspaceId, String json) { - if (this.workspaceArchiveLocation == null) { - return; - } - - File archiveFile = new File(workspaceArchiveLocation, createArchiveFileName(workspaceId)); - try (FileWriter fileWriter = new FileWriter(archiveFile)) { - fileWriter.write(json); - fileWriter.flush(); - - debugArchivedWorkspaceLocation(archiveFile); - } catch (Exception e) { - log.warn("Could not archive JSON to " + archiveFile.getAbsolutePath()); - } - } - - private void debugArchivedWorkspaceLocation(File archiveFile) { - if (log.isDebugEnabled()) { - try { - log.debug("Workspace from server archived to " + archiveFile.getCanonicalPath()); - } catch (IOException ioe) { - log.debug("Workspace from server archived to " + archiveFile.getAbsolutePath()); - } - } - } - - private String createArchiveFileName(long workspaceId) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); - return "structurizr-" + workspaceId + "-" + sdf.format(new Date()) + ".json"; - } - - public void setUser(String user) { - this.user = user; - } - - private String getUser() { - if (!StringUtils.isNullOrEmpty(user)) { - return user; - } else { - String username = System.getProperty("user.name"); - - if (username.contains("@")) { - return username; - } else { - String hostname = null; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException uhe) { - // ignore - } - - return username + (!StringUtils.isNullOrEmpty(hostname) ? "@" + hostname : ""); - } - } + super(url, apiKey, apiSecret); } } \ No newline at end of file diff --git a/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java new file mode 100644 index 000000000..668a62fd5 --- /dev/null +++ b/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java @@ -0,0 +1,457 @@ +package com.structurizr.api; + +import com.structurizr.Workspace; +import com.structurizr.encryption.EncryptedWorkspace; +import com.structurizr.encryption.EncryptionLocation; +import com.structurizr.encryption.EncryptionStrategy; +import com.structurizr.io.json.EncryptedJsonReader; +import com.structurizr.io.json.EncryptedJsonWriter; +import com.structurizr.io.json.JsonReader; +import com.structurizr.io.json.JsonWriter; +import com.structurizr.model.IdGenerator; +import com.structurizr.util.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hc.client5.http.classic.methods.HttpDelete; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; + +import java.io.*; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Date; +import java.util.Properties; + +/** + * A client for the Structurizr workspace API that allows you to get and put Structurizr workspaces in a JSON format. + */ +public class WorkspaceApiClient extends AbstractApiClient { + + private static final Log log = LogFactory.getLog(WorkspaceApiClient.class); + + private static final String STRUCTURIZR_API_URL = "structurizr.api.url"; + private static final String STRUCTURIZR_API_KEY = "structurizr.api.key"; + private static final String STRUCTURIZR_API_SECRET = "structurizr.api.secret"; + + private String user; + + private String apiKey; + private String apiSecret; + + private EncryptionStrategy encryptionStrategy; + + private IdGenerator idGenerator = null; + private boolean mergeFromRemote = true; + private File workspaceArchiveLocation = new File("."); + + /** + * Creates a new Structurizr client based upon configuration in a structurizr.properties file + * on the classpath with the following name-value pairs: + * - structurizr.api.url + * - structurizr.api.key + * - structurizr.api.secret + * + * @throws StructurizrClientException if something goes wrong + */ + public WorkspaceApiClient() throws StructurizrClientException { + try (InputStream in = + WorkspaceApiClient.class.getClassLoader().getResourceAsStream("structurizr.properties")) { + Properties properties = new Properties(); + if (in != null) { + properties.load(in); + + setUrl(properties.getProperty(STRUCTURIZR_API_URL)); + setApiKey(properties.getProperty(STRUCTURIZR_API_KEY)); + setApiSecret(properties.getProperty(STRUCTURIZR_API_SECRET)); + } else { + throw new StructurizrClientException("Could not find a structurizr.properties file on the classpath."); + } + } catch (IOException e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + /** + * Creates a new Structurizr API client with the specified API key and secret, for the Structurizr cloud service. + * + * @param apiKey the API key of your workspace + * @param apiSecret the API secret of your workspace + */ + public WorkspaceApiClient(String apiKey, String apiSecret) { + this(STRUCTURIZR_CLOUD_SERVICE_API_URL, apiKey, apiSecret); + } + + /** + * Creates a new Structurizr client with the specified API URL, key and secret. + * + * @param url the URL of your Structurizr instance + * @param apiKey the API key of your workspace + * @param apiSecret the API secret of your workspace + */ + public WorkspaceApiClient(String url, String apiKey, String apiSecret) { + setUrl(url); + setApiKey(apiKey); + setApiSecret(apiSecret); + } + + /** + * Sets the ID generator to use when parsing a JSON workspace definition. + * + * @param idGenerator an IdGenerator implementation + */ + public void setIdGenerator(IdGenerator idGenerator) { + this.idGenerator = idGenerator; + } + + String getApiKey() { + return apiKey; + } + + private void setApiKey(String apiKey) { + if (apiKey == null || apiKey.trim().length() == 0) { + throw new IllegalArgumentException("The API key must not be null or empty."); + } + + this.apiKey = apiKey; + } + + String getApiSecret() { + return apiSecret; + } + + private void setApiSecret(String apiSecret) { + if (apiSecret == null || apiSecret.trim().length() == 0) { + throw new IllegalArgumentException("The API secret must not be null or empty."); + } + + this.apiSecret = apiSecret; + } + + /** + * Gets the location where a copy of the workspace is archived when it is retrieved from the server. + * + * @return a File instance representing a directory, or null if this client instance is not archiving + */ + public File getWorkspaceArchiveLocation() { + return this.workspaceArchiveLocation; + } + + /** + * Sets the location where a copy of the workspace will be archived whenever it is retrieved from + * the server. Set this to null if you don't want archiving. + * + * @param workspaceArchiveLocation a File instance representing a directory, or null if + * you don't want archiving + */ + public void setWorkspaceArchiveLocation(File workspaceArchiveLocation) { + this.workspaceArchiveLocation = workspaceArchiveLocation; + } + + /** + * Sets the encryption strategy for use when getting or putting workspaces. + * + * @param encryptionStrategy an EncryptionStrategy implementation + */ + public void setEncryptionStrategy(EncryptionStrategy encryptionStrategy) { + this.encryptionStrategy = encryptionStrategy; + } + + /** + * Specifies whether the layout of diagrams from a remote workspace should be retained when putting + * a new version of the workspace. + * + * @param mergeFromRemote true if layout information should be merged from the remote workspace, false otherwise + */ + public void setMergeFromRemote(boolean mergeFromRemote) { + this.mergeFromRemote = mergeFromRemote; + } + + /** + * Locks the workspace with the given ID. + * + * @param workspaceId the ID of your workspace + * @return true if the workspace could be locked, false otherwise + * @throws StructurizrClientException if there are problems related to the network, authorization, etc + */ + public boolean lockWorkspace(long workspaceId) throws StructurizrClientException { + return manageLockForWorkspace(workspaceId, true); + } + + /** + * Unlocks the workspace with the given ID. + * + * @param workspaceId the ID of your workspace + * @return true if the workspace could be unlocked, false otherwise + * @throws StructurizrClientException if there are problems related to the network, authorization, etc + */ + public boolean unlockWorkspace(long workspaceId) throws StructurizrClientException { + return manageLockForWorkspace(workspaceId, false); + } + + private boolean manageLockForWorkspace(long workspaceId, boolean lock) throws StructurizrClientException { + if (workspaceId <= 0) { + throw new IllegalArgumentException("The workspace ID must be a positive integer."); + } + + try (CloseableHttpClient httpClient = HttpClients.createSystem()) { + HttpUriRequestBase httpRequest; + + if (lock) { + log.info("Locking workspace with ID " + workspaceId); + httpRequest = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId + "/lock?user=" + getUser() + "&agent=" + agent); + } else { + log.info("Unlocking workspace with ID " + workspaceId); + httpRequest = new HttpDelete(url + WORKSPACE_PATH + "/" + workspaceId + "/lock?user=" + getUser() + "&agent=" + agent); + } + + addHeaders(httpRequest, "", ""); + debugRequest(httpRequest, null); + + try (CloseableHttpResponse response = httpClient.execute(httpRequest)) { + debugResponse(response); + + String responseText = EntityUtils.toString(response.getEntity()); + ApiResponse apiResponse = ApiResponse.parse(responseText); + log.info(responseText); + + if (response.getCode() == HttpStatus.SC_OK) { + return apiResponse.isSuccess(); + } else { + throw new StructurizrClientException(apiResponse.getMessage()); + } + } + } catch (Exception e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + /** + * Gets the workspace with the given ID. + * + * @param workspaceId the workspace ID + * @return a Workspace instance + * @throws StructurizrClientException if there are problems related to the network, authorization, JSON deserialization, etc + */ + public Workspace getWorkspace(long workspaceId) throws StructurizrClientException { + if (workspaceId <= 0) { + throw new IllegalArgumentException("The workspace ID must be a positive integer."); + } + + try (CloseableHttpClient httpClient = HttpClients.createSystem()) { + log.info("Getting workspace with ID " + workspaceId); + HttpGet httpGet = new HttpGet(url + WORKSPACE_PATH + "/" + workspaceId); + addHeaders(httpGet, "", ""); + debugRequest(httpGet, null); + + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + debugResponse(response); + + String json = EntityUtils.toString(response.getEntity()); + if (response.getCode() == HttpStatus.SC_OK) { + archiveWorkspace(workspaceId, json); + + if (encryptionStrategy == null) { + if (json.contains("\"encryptionStrategy\"") && json.contains("\"ciphertext\"")) { + log.warn("The JSON may contain a client-side encrypted workspace, but no passphrase has been specified."); + } + + JsonReader jsonReader = new JsonReader(); + jsonReader.setIdGenerator(idGenerator); + return jsonReader.read(new StringReader(json)); + } else { + EncryptedWorkspace encryptedWorkspace = new EncryptedJsonReader().read(new StringReader(json)); + + if (encryptedWorkspace.getEncryptionStrategy() != null) { + encryptedWorkspace.getEncryptionStrategy().setPassphrase(encryptionStrategy.getPassphrase()); + return encryptedWorkspace.getWorkspace(); + } else { + // this workspace isn't encrypted, even though the client has an encryption strategy set + JsonReader jsonReader = new JsonReader(); + jsonReader.setIdGenerator(idGenerator); + return jsonReader.read(new StringReader(json)); + } + } + } else { + ApiResponse apiResponse = ApiResponse.parse(json); + throw new StructurizrClientException(apiResponse.getMessage()); + } + } + } catch (Exception e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + /** + * Updates the given workspace. + * + * @param workspaceId the workspace ID + * @param workspace the workspace instance to update + * @throws StructurizrClientException if there are problems related to the network, authorization, JSON serialization, etc + */ + public void putWorkspace(long workspaceId, Workspace workspace) throws StructurizrClientException { + if (workspace == null) { + throw new IllegalArgumentException("The workspace must not be null."); + } else if (workspaceId <= 0) { + throw new IllegalArgumentException("The workspace ID must be a positive integer."); + } + + try (CloseableHttpClient httpClient = HttpClients.createSystem()) { + if (mergeFromRemote) { + Workspace remoteWorkspace = getWorkspace(workspaceId); + if (remoteWorkspace != null) { + workspace.getViews().copyLayoutInformationFrom(remoteWorkspace.getViews()); + workspace.getViews().getConfiguration().copyConfigurationFrom(remoteWorkspace.getViews().getConfiguration()); + } + } + + workspace.setId(workspaceId); + workspace.setThumbnail(null); + workspace.setLastModifiedDate(new Date()); + workspace.setLastModifiedAgent(agent); + workspace.setLastModifiedUser(getUser()); + + workspace.countAndLogWarnings(); + + HttpPut httpPut = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId); + + StringWriter stringWriter = new StringWriter(); + if (encryptionStrategy == null) { + JsonWriter jsonWriter = new JsonWriter(false); + jsonWriter.write(workspace, stringWriter); + } else { + EncryptedWorkspace encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); + encryptionStrategy.setLocation(EncryptionLocation.Client); + EncryptedJsonWriter jsonWriter = new EncryptedJsonWriter(false); + jsonWriter.write(encryptedWorkspace, stringWriter); + } + + StringEntity stringEntity = new StringEntity(stringWriter.toString(), ContentType.APPLICATION_JSON); + httpPut.setEntity(stringEntity); + addHeaders(httpPut, EntityUtils.toString(stringEntity), ContentType.APPLICATION_JSON.toString()); + + debugRequest(httpPut, EntityUtils.toString(stringEntity)); + + log.info("Putting workspace with ID " + workspaceId); + try (CloseableHttpResponse response = httpClient.execute(httpPut)) { + String json = EntityUtils.toString(response.getEntity()); + if (response.getCode() == HttpStatus.SC_OK) { + debugResponse(response); + log.info(json); + } else { + ApiResponse apiResponse = ApiResponse.parse(json); + throw new StructurizrClientException(apiResponse.getMessage()); + } + } + } catch (Exception e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + private void debugRequest(HttpUriRequestBase httpRequest, String content) { + if (log.isDebugEnabled()) { + log.debug(httpRequest.getMethod() + " " + httpRequest.getPath()); + Header[] headers = httpRequest.getHeaders(); + for (Header header : headers) { + log.debug(header.getName() + ": " + header.getValue()); + } + if (content != null) { + log.debug(content); + } + } + } + + private void debugResponse(CloseableHttpResponse response) { + log.debug(response.getCode()); + } + + private void addHeaders(HttpUriRequestBase httpRequest, String content, String contentType) throws Exception { + String httpMethod = httpRequest.getMethod(); + String path = httpRequest.getPath(); + String contentMd5 = new Md5Digest().generate(content); + String nonce = "" + System.currentTimeMillis(); + + HashBasedMessageAuthenticationCode hmac = new HashBasedMessageAuthenticationCode(apiSecret); + HmacContent hmacContent = new HmacContent(httpMethod, path, contentMd5, contentType, nonce); + httpRequest.addHeader(HttpHeaders.USER_AGENT, agent); + httpRequest.addHeader(HttpHeaders.AUTHORIZATION, new HmacAuthorizationHeader(apiKey, hmac.generate(hmacContent.toString())).format()); + httpRequest.addHeader(HttpHeaders.NONCE, nonce); + + if (httpMethod.equals("PUT")) { + httpRequest.addHeader(HttpHeaders.CONTENT_MD5, Base64.getEncoder().encodeToString(contentMd5.getBytes(StandardCharsets.UTF_8))); + httpRequest.addHeader(HttpHeaders.CONTENT_TYPE, contentType); + } + } + + private void archiveWorkspace(long workspaceId, String json) { + if (this.workspaceArchiveLocation == null) { + return; + } + + File archiveFile = new File(workspaceArchiveLocation, createArchiveFileName(workspaceId)); + try (FileWriter fileWriter = new FileWriter(archiveFile)) { + fileWriter.write(json); + fileWriter.flush(); + + debugArchivedWorkspaceLocation(archiveFile); + } catch (Exception e) { + log.warn("Could not archive JSON to " + archiveFile.getAbsolutePath()); + } + } + + private void debugArchivedWorkspaceLocation(File archiveFile) { + if (log.isDebugEnabled()) { + try { + log.debug("Workspace from server archived to " + archiveFile.getCanonicalPath()); + } catch (IOException ioe) { + log.debug("Workspace from server archived to " + archiveFile.getAbsolutePath()); + } + } + } + + private String createArchiveFileName(long workspaceId) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + return "structurizr-" + workspaceId + "-" + sdf.format(new Date()) + ".json"; + } + + public void setUser(String user) { + this.user = user; + } + + private String getUser() { + if (!StringUtils.isNullOrEmpty(user)) { + return user; + } else { + String username = System.getProperty("user.name"); + + if (username.contains("@")) { + return username; + } else { + String hostname = null; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException uhe) { + // ignore + } + + return username + (!StringUtils.isNullOrEmpty(hostname) ? "@" + hostname : ""); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/com/structurizr/api/WorkspaceMetadata.java b/structurizr-client/src/com/structurizr/api/WorkspaceMetadata.java new file mode 100644 index 000000000..81df2367d --- /dev/null +++ b/structurizr-client/src/com/structurizr/api/WorkspaceMetadata.java @@ -0,0 +1,82 @@ +package com.structurizr.api; + +public class WorkspaceMetadata { + + private int id; + private String name; + private String description; + private String apiKey; + private String apiSecret; + + private String privateUrl; + private String publicUrl; + private String shareableUrl; + + WorkspaceMetadata() { + } + + public int getId() { + return id; + } + + void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + void setDescription(String description) { + this.description = description; + } + + public String getApiKey() { + return apiKey; + } + + void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getApiSecret() { + return apiSecret; + } + + void setApiSecret(String apiSecret) { + this.apiSecret = apiSecret; + } + + public String getPrivateUrl() { + return privateUrl; + } + + void setPrivateUrl(String privateUrl) { + this.privateUrl = privateUrl; + } + + public String getPublicUrl() { + return publicUrl; + } + + void setPublicUrl(String publicUrl) { + this.publicUrl = publicUrl; + } + + public String getShareableUrl() { + return shareableUrl; + } + + void setShareableUrl(String shareableUrl) { + this.shareableUrl = shareableUrl; + } + +} \ No newline at end of file diff --git a/structurizr-client/src/com/structurizr/api/Workspaces.java b/structurizr-client/src/com/structurizr/api/Workspaces.java new file mode 100644 index 000000000..bf578ec89 --- /dev/null +++ b/structurizr-client/src/com/structurizr/api/Workspaces.java @@ -0,0 +1,28 @@ +package com.structurizr.api; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +class Workspaces { + + private List workspaces; + + Workspaces() { + } + + List getWorkspaces() { + return new ArrayList<>(workspaces); + } + + void setWorkspaces(List workspaces) { + if (workspaces == null) { + this.workspaces = new ArrayList<>(); + } else { + this.workspaces = workspaces; + } + + this.workspaces.sort(Comparator.comparingInt(WorkspaceMetadata::getId)); + } + +} \ No newline at end of file From b3eeea86c7a75204524e141970b2cd1c3274afc6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 22 Dec 2023 13:35:13 +0000 Subject: [PATCH 128/418] Typo. --- structurizr-core/src/com/structurizr/view/ImageView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/view/ImageView.java b/structurizr-core/src/com/structurizr/view/ImageView.java index 861ce76c9..b67845a32 100644 --- a/structurizr-core/src/com/structurizr/view/ImageView.java +++ b/structurizr-core/src/com/structurizr/view/ImageView.java @@ -77,7 +77,7 @@ public void setContent(String content) { } /** - * Gets the the content type of this view (e.g. "image/png"). + * Gets the content type of this view (e.g. "image/png"). * * @return the content type, as a String */ From 33797b6978c7a335b5461698f2c840f330fcbd63 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 22 Dec 2023 13:45:36 +0000 Subject: [PATCH 129/418] . --- structurizr-client/src/com/structurizr/api/AdminApiClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-client/src/com/structurizr/api/AdminApiClient.java b/structurizr-client/src/com/structurizr/api/AdminApiClient.java index cb84ff595..55a9c79db 100644 --- a/structurizr-client/src/com/structurizr/api/AdminApiClient.java +++ b/structurizr-client/src/com/structurizr/api/AdminApiClient.java @@ -24,7 +24,7 @@ public class AdminApiClient extends AbstractApiClient { private final String apiKey; /** - * Creates a new API client with the specified on-premises API URL and key. + * Creates a new admin API client. * * @param url the URL of your Structurizr instance * @param username the username (only required for the Structurizr cloud service) From 0ccffba53b87917a5697836241a38f22a7cb3fec Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Dec 2023 12:23:04 +0000 Subject: [PATCH 130/418] Adds support for inter-workspace URLs of the form `{workspace:123456}/diagrams`. --- docs/changelog.md | 1 + .../src/com/structurizr/model/ModelItem.java | 4 +++- .../src/com/structurizr/util/Url.java | 3 ++- .../com/structurizr/model/ModelItemTests.java | 16 +++++++++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 7e2e1a926..e02d80837 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,7 @@ ## 1.29.0 (unreleased) - Adds `com.structurizr.api.AdminApiClient` as a client for the cloud service/on-premises admin APIs. +- Adds support for inter-workspace URLs of the form `{workspace:123456}/diagrams`. ## 1.28.1 (11th December 2023) diff --git a/structurizr-core/src/com/structurizr/model/ModelItem.java b/structurizr-core/src/com/structurizr/model/ModelItem.java index ac81b3918..2be654cd6 100644 --- a/structurizr-core/src/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/com/structurizr/model/ModelItem.java @@ -123,7 +123,9 @@ public void setUrl(String url) { if (StringUtils.isNullOrEmpty(url)) { this.url = null; } else { - if (url.startsWith(Url.WORKSPACE_URL_PREFIX)) { + if (url.startsWith(Url.INTRA_WORKSPACE_URL_PREFIX)) { + this.url = url; + } else if (url.matches(Url.INTER_WORKSPACE_URL_REGEX)) { this.url = url; } else if (Url.isUrl(url)) { this.url = url; diff --git a/structurizr-core/src/com/structurizr/util/Url.java b/structurizr-core/src/com/structurizr/util/Url.java index 49e69ab87..adf0989a4 100644 --- a/structurizr-core/src/com/structurizr/util/Url.java +++ b/structurizr-core/src/com/structurizr/util/Url.java @@ -8,7 +8,8 @@ */ public class Url { - public static final String WORKSPACE_URL_PREFIX = "{workspace}"; + public static final String INTRA_WORKSPACE_URL_PREFIX = "{workspace}"; + public static final String INTER_WORKSPACE_URL_REGEX = "\\{workspace:\\d+\\}.*"; /** * Determines whether the supplied string is a valid URL. diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java index 9cb5565f7..783a1c548 100644 --- a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java +++ b/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java @@ -234,10 +234,24 @@ void setUrl_AcceptsAUrl() { } @Test - void setUrl_AcceptsAWorkspaceUrl() { + void setUrl_AcceptsAnIntraWorkspaceUrl() { Element element = model.addSoftwareSystem("Name"); element.setUrl("{workspace}/diagrams#key"); assertEquals("{workspace}/diagrams#key", element.getUrl()); } + @Test + void setUrl_AcceptsAnInterWorkspaceUrl() { + Element element = model.addSoftwareSystem("Name"); + + element.setUrl("{workspace:123456}"); + assertEquals("{workspace:123456}", element.getUrl()); + + element.setUrl("{workspace:123456}/diagrams#key"); + assertEquals("{workspace:123456}/diagrams#key", element.getUrl()); + + element.setUrl("{workspace:123456}/documentation"); + assertEquals("{workspace:123456}/documentation", element.getUrl()); + } + } \ No newline at end of file From 5e7793ad571b271df1536264ef742e11f50c18ac Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Dec 2023 12:27:07 +0000 Subject: [PATCH 131/418] . --- .../src/com/structurizr/view/RelationshipView.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/structurizr-core/src/com/structurizr/view/RelationshipView.java b/structurizr-core/src/com/structurizr/view/RelationshipView.java index 56f5c87a3..e9d01b480 100644 --- a/structurizr-core/src/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/com/structurizr/view/RelationshipView.java @@ -109,7 +109,9 @@ public void setUrl(String url) { if (StringUtils.isNullOrEmpty(url)) { this.url = null; } else { - if (url.startsWith(Url.WORKSPACE_URL_PREFIX)) { + if (url.startsWith(Url.INTRA_WORKSPACE_URL_PREFIX)) { + this.url = url; + } else if (url.matches(Url.INTER_WORKSPACE_URL_REGEX)) { this.url = url; } else if (Url.isUrl(url)) { this.url = url; From 0d352c18430887214e5036be40d0bc5f805c2076 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Dec 2023 12:27:34 +0000 Subject: [PATCH 132/418] . --- structurizr-core/src/com/structurizr/view/ModelView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/structurizr-core/src/com/structurizr/view/ModelView.java b/structurizr-core/src/com/structurizr/view/ModelView.java index b10a556f7..c80ddb72a 100644 --- a/structurizr-core/src/com/structurizr/view/ModelView.java +++ b/structurizr-core/src/com/structurizr/view/ModelView.java @@ -15,6 +15,7 @@ /** * The superclass for all views that show elements/relationships from the model, namely: * + * - Custom views * - System landscape views * - System context views * - Container views From aa74fb0d38745d86513ac5a1edb40b3a13882470 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Dec 2023 14:55:02 +0000 Subject: [PATCH 133/418] Deprecates the original StructurizrClient class and properties file configuration. --- .../structurizr/api/StructurizrClient.java | 36 ++++++++++++++++++- .../structurizr/api/WorkspaceApiClient.java | 35 ++---------------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/structurizr-client/src/com/structurizr/api/StructurizrClient.java b/structurizr-client/src/com/structurizr/api/StructurizrClient.java index b7ce1fcd9..422bad04c 100644 --- a/structurizr-client/src/com/structurizr/api/StructurizrClient.java +++ b/structurizr-client/src/com/structurizr/api/StructurizrClient.java @@ -1,12 +1,46 @@ package com.structurizr.api; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + /** * A client for the Structurizr workspace API that allows you to get and put Structurizr workspaces in a JSON format. + * + * @deprecated Use WorkspaceApiClient instead */ +@Deprecated public class StructurizrClient extends WorkspaceApiClient { + private static final String STRUCTURIZR_API_URL = "structurizr.api.url"; + private static final String STRUCTURIZR_API_KEY = "structurizr.api.key"; + private static final String STRUCTURIZR_API_SECRET = "structurizr.api.secret"; + + /** + * Creates a new Structurizr client based upon configuration in a structurizr.properties file + * on the classpath with the following name-value pairs: + * - structurizr.api.url + * - structurizr.api.key + * - structurizr.api.secret + * + * @throws StructurizrClientException if something goes wrong + */ public StructurizrClient() throws StructurizrClientException { - super(); + try (InputStream in = + WorkspaceApiClient.class.getClassLoader().getResourceAsStream("structurizr.properties")) { + Properties properties = new Properties(); + if (in != null) { + properties.load(in); + + setUrl(properties.getProperty(STRUCTURIZR_API_URL)); + setApiKey(properties.getProperty(STRUCTURIZR_API_KEY)); + setApiSecret(properties.getProperty(STRUCTURIZR_API_SECRET)); + } else { + throw new StructurizrClientException("Could not find a structurizr.properties file on the classpath."); + } + } catch (IOException e) { + throw new StructurizrClientException(e); + } } public StructurizrClient(String apiKey, String apiSecret) { diff --git a/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java index 668a62fd5..29bbb8332 100644 --- a/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java @@ -41,10 +41,6 @@ public class WorkspaceApiClient extends AbstractApiClient { private static final Log log = LogFactory.getLog(WorkspaceApiClient.class); - private static final String STRUCTURIZR_API_URL = "structurizr.api.url"; - private static final String STRUCTURIZR_API_KEY = "structurizr.api.key"; - private static final String STRUCTURIZR_API_SECRET = "structurizr.api.secret"; - private String user; private String apiKey; @@ -56,32 +52,7 @@ public class WorkspaceApiClient extends AbstractApiClient { private boolean mergeFromRemote = true; private File workspaceArchiveLocation = new File("."); - /** - * Creates a new Structurizr client based upon configuration in a structurizr.properties file - * on the classpath with the following name-value pairs: - * - structurizr.api.url - * - structurizr.api.key - * - structurizr.api.secret - * - * @throws StructurizrClientException if something goes wrong - */ - public WorkspaceApiClient() throws StructurizrClientException { - try (InputStream in = - WorkspaceApiClient.class.getClassLoader().getResourceAsStream("structurizr.properties")) { - Properties properties = new Properties(); - if (in != null) { - properties.load(in); - - setUrl(properties.getProperty(STRUCTURIZR_API_URL)); - setApiKey(properties.getProperty(STRUCTURIZR_API_KEY)); - setApiSecret(properties.getProperty(STRUCTURIZR_API_SECRET)); - } else { - throw new StructurizrClientException("Could not find a structurizr.properties file on the classpath."); - } - } catch (IOException e) { - log.error(e); - throw new StructurizrClientException(e); - } + protected WorkspaceApiClient() { } /** @@ -120,7 +91,7 @@ String getApiKey() { return apiKey; } - private void setApiKey(String apiKey) { + protected void setApiKey(String apiKey) { if (apiKey == null || apiKey.trim().length() == 0) { throw new IllegalArgumentException("The API key must not be null or empty."); } @@ -132,7 +103,7 @@ String getApiSecret() { return apiSecret; } - private void setApiSecret(String apiSecret) { + protected void setApiSecret(String apiSecret) { if (apiSecret == null || apiSecret.trim().length() == 0) { throw new IllegalArgumentException("The API secret must not be null or empty."); } From c7ba5ab2eb127bb2ae812693f35acc02e7b550f2 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Dec 2023 15:22:41 +0000 Subject: [PATCH 134/418] Move docs. --- README.md | 62 +------------ docs/api-client.md | 95 -------------------- docs/binaries.md | 7 -- docs/building.md | 9 -- docs/client-side-encryption.md | 23 ----- docs/component-diagram.md | 17 ---- docs/container-diagram.md | 15 ---- docs/deployment-diagram.md | 11 --- docs/dynamic-diagram.md | 19 ---- docs/faq.md | 19 ---- docs/filtered-views.md | 39 -------- docs/getting-started.md | 94 ------------------- docs/images/filtered-views-1.png | Bin 165819 -> 0 bytes docs/images/filtered-views-2.png | Bin 187584 -> 0 bytes docs/images/getting-started-1.png | Bin 130986 -> 0 bytes docs/images/getting-started-2.png | Bin 133350 -> 0 bytes docs/images/getting-started-diagram-key.png | Bin 42766 -> 0 bytes docs/images/implied-relationships-1.png | Bin 54035 -> 0 bytes docs/images/structurizr-banner.png | Bin 53460 -> 0 bytes docs/images/structurizr-logo.png | Bin 10206 -> 0 bytes docs/images/structurizr-overview.png | Bin 330672 -> 0 bytes docs/images/styling-elements-1.png | Bin 171437 -> 0 bytes docs/images/styling-elements-2.png | Bin 175074 -> 0 bytes docs/images/styling-elements-3.png | Bin 175296 -> 0 bytes docs/images/styling-elements-4.png | Bin 193247 -> 0 bytes docs/images/styling-elements-5.png | Bin 382973 -> 0 bytes docs/images/styling-elements-6.png | Bin 75001 -> 0 bytes docs/images/styling-relationships-1.png | Bin 171403 -> 0 bytes docs/images/styling-relationships-2.png | Bin 170530 -> 0 bytes docs/images/styling-relationships-3.png | Bin 170369 -> 0 bytes docs/images/styling-relationships-4.png | Bin 90321 -> 0 bytes docs/implied-relationships.md | 93 ------------------- docs/model.md | 20 ----- docs/styling-elements.md | 78 ---------------- docs/styling-relationships.md | 48 ---------- docs/system-context-diagram.md | 13 --- docs/system-landscape-diagram.md | 13 --- docs/views.md | 29 ------ 38 files changed, 4 insertions(+), 700 deletions(-) delete mode 100644 docs/api-client.md delete mode 100644 docs/binaries.md delete mode 100644 docs/building.md delete mode 100644 docs/client-side-encryption.md delete mode 100644 docs/component-diagram.md delete mode 100644 docs/container-diagram.md delete mode 100644 docs/deployment-diagram.md delete mode 100644 docs/dynamic-diagram.md delete mode 100644 docs/faq.md delete mode 100644 docs/filtered-views.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/images/filtered-views-1.png delete mode 100644 docs/images/filtered-views-2.png delete mode 100644 docs/images/getting-started-1.png delete mode 100644 docs/images/getting-started-2.png delete mode 100644 docs/images/getting-started-diagram-key.png delete mode 100644 docs/images/implied-relationships-1.png delete mode 100644 docs/images/structurizr-banner.png delete mode 100644 docs/images/structurizr-logo.png delete mode 100644 docs/images/structurizr-overview.png delete mode 100644 docs/images/styling-elements-1.png delete mode 100644 docs/images/styling-elements-2.png delete mode 100644 docs/images/styling-elements-3.png delete mode 100644 docs/images/styling-elements-4.png delete mode 100644 docs/images/styling-elements-5.png delete mode 100644 docs/images/styling-elements-6.png delete mode 100644 docs/images/styling-relationships-1.png delete mode 100644 docs/images/styling-relationships-2.png delete mode 100644 docs/images/styling-relationships-3.png delete mode 100644 docs/images/styling-relationships-4.png delete mode 100644 docs/implied-relationships.md delete mode 100644 docs/model.md delete mode 100644 docs/styling-elements.md delete mode 100644 docs/styling-relationships.md delete mode 100644 docs/system-context-diagram.md delete mode 100644 docs/system-landscape-diagram.md delete mode 100644 docs/views.md diff --git a/README.md b/README.md index b6069bca3..9b45a95ef 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,9 @@ -![Structurizr](docs/images/structurizr-banner.png) - # Structurizr for Java This GitHub repository is (1) a client library for the [Structurizr](https://structurizr.com) cloud service and on-premises installation -and (2) a way to create a Structurizr workspace using Java code. Looking for the [Structurizr DSL](https://github.com/structurizr/dsl) instead? - -## A quick example - -As an example, the following Java code can be used to create a software architecture __model__ and an associated __view__ that describes a user using a software system, based upon the [C4 model](https://c4model.com). - -```java -public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Getting Started", "This is a model of my software system."); - Model model = workspace.getModel(); - - Person user = model.addPerson("User", "A user of my software system."); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "My software system."); - user.uses(softwareSystem, "Uses"); - - ViewSet views = workspace.getViews(); - SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); -} -``` - -The view can then be exported to be visualised using the [Structurizr cloud service/on-premises installation/Lite](https://structurizr.com), -or other formats including PlantUML, Mermaid, DOT, and WebSequenceDiagrams via the [structurizr-export library](https://github.com/structurizr/export). - -## Table of contents +and (2) a way to create a Structurizr workspace using Java code. -* [Changelog](docs/changelog.md) -* Introduction - * [Getting started](docs/getting-started.md) - * [Basic concepts](https://structurizr.com/help/concepts) (workspaces, models, views and documentation) - * [C4 model](https://c4model.com) - * [Examples](https://github.com/structurizr/examples) - * [Binaries](docs/binaries.md) - * [Building from source](docs/building.md) - * [FAQ](docs/faq.md) -* Model - * [Creating your model](docs/model.md) - * [Implied relationships](docs/implied-relationships.md) -* Views - * [Creating views](docs/views.md) - * [System Context diagram](docs/system-context-diagram.md) - * [Container diagram](docs/container-diagram.md) - * [Component diagram](docs/component-diagram.md) - * [Dynamic diagram](docs/dynamic-diagram.md) - * [Deployment diagram](docs/deployment-diagram.md) - * [System Landscape diagram](docs/system-landscape-diagram.md) - * [Styling elements](docs/styling-elements.md) - * [Styling relationships](docs/styling-relationships.md) - * [Filtered views](docs/filtered-views.md) -* Cloud service/on-premises installation - * [API client](docs/api-client.md) - * [Client-side encryption](docs/client-side-encryption.md) -* Related projects - * [structurizr-dsl](https://github.com/structurizr/dsl): A text-based DSL for authoring Structurizr workspaces. - * [structurizr-export](https://github.com/structurizr/export): Export model and views to external formats (e.g. PlantUML, Mermaid, etc). - * [structurizr-import](https://github.com/structurizr/import): Import Markdown/AsciiDoc documentation/ADRs into a Structurizr workspace. +Looking for the [Structurizr DSL](https://github.com/structurizr/dsl) instead? +- [Documentation](https://docs.structurizr.com/java) +- [Changelog](docs/changelog.md) \ No newline at end of file diff --git a/docs/api-client.md b/docs/api-client.md deleted file mode 100644 index 7a45400ac..000000000 --- a/docs/api-client.md +++ /dev/null @@ -1,95 +0,0 @@ -# API client - -The Structurizr for Java library includes a client for the [Structurizr web API](https://api.structurizr.com), which allows you to get and put workspaces using JSON over HTTPS. This page provides a quick overview of how to use the API client. - -## Configuration - -The are two ways to configure the API client. - -### 1. Programmatically - -The easiest way to configure the API client is to provide values for the API key and API secret programmatically. Each workspace has its own API key and secret, the values for which can be found on [your Structurizr dashboard](https://structurizr.com/dashboard). - -```java -StructurizrClient structurizrClient = new StructurizrClient("key", "secret"); -``` - -If you're using the [on-premises installation](https://structurizr.com/help/on-premises), there is a three argument version of the constructor where you can also specify the API URL. - -```java -StructurizrClient structurizrClient = new StructurizrClient("url", "key", "secret"); -``` - -### 2. Properties file - -If you would like to separate your API credentials from the code, you can configure the values in a Java properties file. This should be named ```structurizr.properties``` and located on the classpath. - -``` -structurizr.api.url=https://api.structurizr.com -structurizr.api.key=key -structurizr.api.secret=secret -``` - -The API client can then be constructed using the default, no args, constructor. - -```java -StructurizrClient structurizrClient = new StructurizrClient(); -``` - -## Usage - -The following operations are available on the API client. - -### 1. getWorkspace - -This allows you to get the content of a remote workspace. - -```java -Workspace workspace = structurizrClient.getWorkspace(1234); -``` - -By default, a copy of the workspace (as a JSON document) is archived to the current working directory. You can modify this behaviour by calling ```setWorkspaceArchiveLocation```. A ```null``` value will disable archiving. - -### 2. putWorkspace - -This allows you to overwrite an existing remote workspace. If the ```mergeFromRemote``` property (on the ```StructurizrClient``` instance) is set to ```true``` (this is the default), any layout information (i.e. the location of boxes on diagrams) is preserved where possible (i.e. where diagram elements haven't been renamed). - -```java -structurizrClient.putWorkspace(1234, workspace); -``` - -### 3. lockWorkspace - -If your workspace supports sharing (not available with the Free Plan), you can optionally attempt to lock your workspace before writing to it, to prevent concurrent updates. - -```java -structurizrClient.lockWorkspace(1234); -``` - -This method returns a boolean; ```true``` if the workspace could be locked, ```false``` otherwise. - -### 4. unlockWorkspace - -Similarly, you can unlock a workspace. - -```java -structurizrClient.unlockWorkspace(1234); -``` - -This method also returns a boolean; ```true``` if the workspace could be unlocked, ```false``` otherwise. - -## SSL handshake errors - -SSL handshake errors are likely if using a self-signed certificate with the on-premises installation, because the Structurizr client program runtime won't trust a self-signed certificate by default. - -If this happens, you can use the ```javax.net.ssl.trustStore``` JVM option to point to your keystore. For example: - -``` -java -Djavax.net.ssl.trustStore=/some/path/to/keystore.jks YourJavaProgram -``` - -Alternatively, you can specify this property in your Java program: - -``` -System.setProperty("javax.net.ssl.trustStore", "/some/path/to/keystore.jks"); -``` diff --git a/docs/binaries.md b/docs/binaries.md deleted file mode 100644 index 74ef946e0..000000000 --- a/docs/binaries.md +++ /dev/null @@ -1,7 +0,0 @@ -# Binaries -The "Structurizr for Java" binaries are hosted on [Maven Central](https://repo1.maven.org/maven2/com/structurizr/) and the dependencies for use with Maven, Ivy, Gradle, etc are as follows. - -Name | Description ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- -com.structurizr:structurizr-core | The core library that can used to create software architecture models. -com.structurizr:structurizr-client | The API client for publishing models on the Structurizr cloud service and on-premises installation. \ No newline at end of file diff --git a/docs/building.md b/docs/building.md deleted file mode 100644 index a68e36640..000000000 --- a/docs/building.md +++ /dev/null @@ -1,9 +0,0 @@ -# Building - -To build this repo from the sources (you'll need `git` and Java 11+ installed)... - -``` -git clone https://github.com/structurizr/java.git structurizr-java -cd structurizr-java -./gradlew -``` \ No newline at end of file diff --git a/docs/client-side-encryption.md b/docs/client-side-encryption.md deleted file mode 100644 index 5115c7db1..000000000 --- a/docs/client-side-encryption.md +++ /dev/null @@ -1,23 +0,0 @@ -# Client-side encryption - -> This feature is not available with a free Structurizr cloud service account. - -The JSON representation of your workspace is stored on the Structurizr cloud service using AES encryption with a 128-bit key, a random salt and a server-side passphrase. For additional peace of mind, you can choose to encrypt your workspace with your own passphrase on the client before uploading it to Structurizr. In order to view a client-side encrypted workspace, you will be asked to enter your passphrase when you open the workspace in your web browser. The passphrase is then used to decrypt the workspace in your web browser - at no point does the passphrase leave your computer. - -To use client-side encryption, simply create an instance of ```AesEncryptionStrategy``` and associate it with your ```StructurizrClient``` instance. For example: - -```java -StructurizrClient structurizrClient = new StructurizrClient("key", "secret"); -structurizrClient.setEncryptionStrategy(new AesEncryptionStrategy("password")); -structurizrClient.putWorkspace(1234, workspace); -``` - -The default key size is 128 bits and the default iteration count is 1000. An alternative constructor for AesEncryptionStrategy takes the following parameters: - -- The key size (number of bits; e.g. 128, 192 or 256). -- The iteration count (used when generating keys). -- The passphrase. - -In addition, a random salt and initialization vector are generated automatically for you, using Java's ```SecureRandom``` class. - -See [ClientSideEncryption.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/ClientSideEncryption.java) for a full example, and [https://structurizr.com/share/41](https://structurizr.com/share/41) to access the workspace. \ No newline at end of file diff --git a/docs/component-diagram.md b/docs/component-diagram.md deleted file mode 100644 index 2edbe47b7..000000000 --- a/docs/component-diagram.md +++ /dev/null @@ -1,17 +0,0 @@ -# Component diagram - -Following on from a Container Diagram, next you can zoom in and decompose each container further to identify the major structural building blocks and their interactions. - -The Component diagram shows how a container is made up of a number of "components", what each of those components are, their responsibilities and the technology/implementation details. - -## Example - -This is an example Component diagram for a fictional Internet Banking System, showing some (rather than all) of the components within the API Application. Here, there are two Spring MVC Rest Controllers providing access points for the JSON/HTTPS API, with each controller subsequently using other components to access data from the Database and Mainframe Banking System. - -![An example Component diagram](https://static.structurizr.com/workspace/36141/diagrams/Components.png) - -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#Components](https://structurizr.com/share/36141/diagrams#Components) for the diagram. - -### Extracting components automatically - -Please note that, in a real-world scenario, you would probably want to extract components automatically from a codebase with the [component finder](https://github.com/structurizr/java-extensions/blob/master/docs/component-finder.md), using static analysis and reflection techniques. \ No newline at end of file diff --git a/docs/container-diagram.md b/docs/container-diagram.md deleted file mode 100644 index ebc56d8d2..000000000 --- a/docs/container-diagram.md +++ /dev/null @@ -1,15 +0,0 @@ -# Container diagram - -Once you understand how your system fits in to the overall IT environment, a really useful next step is to zoom-in to the system boundary with a Container diagram. A "container" is something like a web application, desktop application, mobile app, database, file system, etc. Essentially, a container is a separately runnable/deployable unit that executes code or stores data. - -The Container diagram shows the high-level shape of the software architecture and how responsibilities are distributed across it. It also shows the major technology choices and how the containers communicate with one another. It's a simple, high-level technology focussed diagram that is useful for software developers and support/operations staff alike. - -## Example - -This is an example Container diagram for a fictional Internet Banking System. It shows that the Internet Banking System is made up of five containers: a server-side Web Application, a Single-Page Application, a Mobile App, a server-side API Application, and a Database. The Web Application is a Java/Spring MVC web application that simply serves static content (HTML, CSS and JavaScript), including the content that makes up the Single-Page Application. The Single-Page Application is an Angular application that runs in the customer's web browser, providing all of the Internet banking features. Alternatively, customers can use the cross-platform Xamarin Mobile App, to access a subset of the Internet banking functionality. - -Both the Single-Page Application and Mobile App use a JSON/HTTPS API, which is provided by another Java/Spring MVC application running on the server. The API Application gets user information from the Database (a relational database schema). The API Application also communicates with the existing Mainframe Banking System, using a propreitary XML/HTTPS interface, to get information about bank accounts or make transactions. The API Application also uses the existing E-mail System if it needs to send e-mails to customers. - -![An example Container diagram](https://static.structurizr.com/workspace/36141/diagrams/Containers.png) - -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#Containers](https://structurizr.com/share/36141/diagrams#Containers) for the diagram. \ No newline at end of file diff --git a/docs/deployment-diagram.md b/docs/deployment-diagram.md deleted file mode 100644 index 39b80fc92..000000000 --- a/docs/deployment-diagram.md +++ /dev/null @@ -1,11 +0,0 @@ -# Deployment diagram - -A deployment diagram allows you to illustrate how containers in the static model are mapped to infrastructure. This deployment diagram is based upon a [UML deployment diagram](https://en.wikipedia.org/wiki/Deployment_diagram), although simplified slightly to show the mapping between containers and deployment nodes. A deployment node is something like physical infrastructure (e.g. a physical server or device), virtualised infrastructure (e.g. IaaS, PaaS, a virtual machine), containerised infrastructure (e.g. a Docker container), an execution environment (e.g. a database server, Java EE web/application server, Microsoft IIS), etc. Deployment nodes can be nested. - -## Example - -As an example, a Deployment diagram for the live environment of a simplified, fictional Internet Banking System might look something like this. In summary, it shows the deployment of the Web Application and the Database, with a secondary Database being used for failover purposes. - -![An example Deployment diagram](https://static.structurizr.com/workspace/36141/diagrams/LiveDeployment.png) - -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#LiveDeployment](https://structurizr.com/share/36141/diagrams#LiveDeployment) for the diagram. \ No newline at end of file diff --git a/docs/dynamic-diagram.md b/docs/dynamic-diagram.md deleted file mode 100644 index 8443a8234..000000000 --- a/docs/dynamic-diagram.md +++ /dev/null @@ -1,19 +0,0 @@ -# Dynamic diagram - -A simple dynamic diagram can be useful when you want to show how elements in a static model collaborate at runtime to implement a user story, use case, feature, etc. This dynamic diagram is based upon a [UML communication diagram](https://en.wikipedia.org/wiki/Communication_diagram) (previously known as a "UML collaboration diagram"). It is similar to a [UML sequence diagram](https://en.wikipedia.org/wiki/Sequence_diagram) although it allows a free-form arrangement of diagram elements with numbered interactions to indicate ordering. - -## Example - -As an example, a Dynamic diagram describing the customer sign in process for a simplified, fictional Internet Banking System might look something like this. In summary, it shows the components involved in the sign in process, and the interactions between them. - -![An example Dynamic diagram](https://static.structurizr.com/workspace/36141/diagrams/SignIn.png) - -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#SignIn](https://structurizr.com/share/36141/diagrams#SignIn) for the diagram. - -### Adding relationships - -In order to add a relationship between two elements to a dynamic view, that relationship must already exist between the two elements in the static view. - -### Parallel behaviour - -Showing parallel behaviour is also possible using the ```startParallelSequence()``` and ```endParallelSequence()``` methods on the ```DynamicView``` class. See [MicroservicesExample.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/MicroservicesExample.java) and [https://structurizr.com/share/4241#CustomerUpdateEvent](https://structurizr.com/share/4241#CustomerUpdateEvent) for an example. \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 29f78a937..000000000 --- a/docs/faq.md +++ /dev/null @@ -1,19 +0,0 @@ -# Frequently asked questions - -## Why are many classes final with package-protected members, and not open to extension? - -First and foremost, this repo is a client library for the [Structurizr cloud service and on-premises installation](https://structurizr.com). -It allows you to write Java code to create an in-memory object graph representing a software architecture model and views (a "workspace"), serialize that to JSON, and upload it via a web API. -The workspace has an [OpenAPI definition](https://github.com/structurizr/json/blob/master/structurizr.yaml), but this library also implements a number of rules (think of them as the "business logic") to ensure that the workspace is valid. -These rules include, for example, ensuring that all containers with a software system have unique names, and that you can't add components to a system context view. - -Removing the `final` modifier from the classes and leaving them open for extension allows you to bypass/break these rules, which will likely lead to the serialized workspace definitions being incompatible with the various diagram rendering tools -(i.e. the Structurizr cloud service/on-premises installation/Lite, plus the PlantUML/Mermaid exporters). -The output of this library also needs to be compatible with client libraries written in other languages. - -You are welcome to fork this library for your own purposes. -Alternatively, you can build a thin wrapper around the library, to provide your own custom functionality, or perhaps a more fluent API ... many teams have done this. - -## Can I submit a pull request? - -It depends on the nature of the change. Please open an issue first to discuss it. diff --git a/docs/filtered-views.md b/docs/filtered-views.md deleted file mode 100644 index c0e700872..000000000 --- a/docs/filtered-views.md +++ /dev/null @@ -1,39 +0,0 @@ -# Filtered views - -A filtered view represents a view on top of another view, which can be used to include or exclude specific elements and/or relationships, based upon their tag. The benefit of using filtered views is that element and relationship positions are shared between the views. - -Filtered views can be created on top of static views only; i.e. System Landscape, System Context, Container and Component views. - -## Example - -As an example, let's imagine an organisation where a User uses Software System A for tasks 1 and 2. - -![A diagram showing the current state](images/filtered-views-1.png) - -And, in the future, Software System B will be introduced to fulfil task 2. - -![A diagram showing the future state](images/filtered-views-2.png) - -With Structurizr for Java, you can illustrate this by defining a single context diagram with two filtered views on top; one showing the current state and the other showing future state. - -```java -Person user = model.addPerson("User", "A description of the user."); -SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", "A description of software system A."); -SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", "A description of software system B."); -softwareSystemB.addTags(FUTURE_STATE); - -user.uses(softwareSystemA, "Uses for tasks 1 and 2").addTags(CURRENT_STATE); -user.uses(softwareSystemA, "Uses for task 1").addTags(FUTURE_STATE); -user.uses(softwareSystemB, "Uses for task 2").addTags(FUTURE_STATE); - -ViewSet views = workspace.getViews(); -SystemLandscapeView systemLandscapeView = views.createSystemLandscapeView("SystemLandscape", "An example System Landscape diagram."); -systemLandscapeView.addAllElements(); - -views.createFilteredView(systemLandscapeView, "CurrentState", "The current system landscape.", FilterMode.Exclude, FUTURE_STATE); -views.createFilteredView(systemLandscapeView, "FutureState", "The future state system landscape after Software System B is live.", FilterMode.Exclude, CURRENT_STATE); -``` - -In summary, you create a view with all of the elements and relationships that you want to show, and then create one or more filtered views on top, specifying the tags that you'd like to include or exclude. - -See [FilteredViews.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/FilteredViews.java) for the full code, and [https://structurizr.com/share/19911/diagrams](https://structurizr.com/share/19911/diagrams) for the diagram. \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 5a5c3425f..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,94 +0,0 @@ -# Getting started - -Here is a quick overview of how to get started with Structurizr for Java so that you can create a software architecture model as code. -You can find the code at [GettingStarted.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/GettingStarted.java) -and the live example workspace at [https://structurizr.com/share/25441](https://structurizr.com/share/25441). - -For more examples, please see [structurizr-examples](https://github.com/structurizr/examples/tree/main/java/src/main/java/com/structurizr/example). - -## 1. Dependencies - -The Structurizr for Java binaries are hosted on [Maven Central](https://repo1.maven.org/maven2/com/structurizr/structurizr-client/) and the dependencies for use with Maven, Ivy, Gradle, etc are as follows. - -Name | Description ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- -com.structurizr:structurizr-client | The Structurizr API client library. - -## 2. Create a Java program - -The software architecture model is going to be created by a short Java program, so we'll need to start by creating a new Java class, with a ```main``` method as follows: - -```java -public class GettingStarted { - - public static void main(String[] args) throws Exception { - // all of the Structurizr code will go here - } - -} -``` - -## 3. Create a model - -The first step is to create a workspace in which the software architecture model will reside. - -```java -Workspace workspace = new Workspace("Getting Started", "This is a model of my software system."); -Model model = workspace.getModel(); -``` - -Now let's add some elements to the model to describe a user using a software system. - -```java -Person user = model.addPerson("User", "A user of my software system."); -SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "My software system."); -user.uses(softwareSystem, "Uses"); -``` - -## 4. Create some views - -With the model created, we need to create some views with which to visualise it. - -```java -ViewSet views = workspace.getViews(); -SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); -contextView.addAllSoftwareSystems(); -contextView.addAllPeople(); -``` - -## 5. Add some colour and shapes - -Optionally, elements and relationships can be styled by specifying colours, sizes and shapes. - -```java -Styles styles = views.getConfiguration().getStyles(); -styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#1168bd").color("#ffffff"); -styles.addElementStyle(Tags.PERSON).background("#08427b").color("#ffffff").shape(Shape.Person); -``` - -## 6. Upload to Structurizr - -Structurizr provides a web API to get and put workspaces, and an API client is provided by the ```StructurizrClient``` class. - -```java -StructurizrClient structurizrClient = new StructurizrClient("key", "secret"); -structurizrClient.putWorkspace(25441, workspace); -``` - -> In order to upload your model to Structurizr using the web API, you'll need to [sign up for free](https://structurizr.com/signup) to get your own API key and secret. See [Structurizr - Workspaces](https://structurizr.com/help/workspaces) for information about finding your workspace ID, API key and secret. - -## 7. Open the workspace in Structurizr - -Once you've run your program to create and upload the workspace, you can now sign in to your Structurizr account, and open the workspace from [your dashboard](https://structurizr.com/dashboard). The result should be a diagram like this: - -![Getting Started with Structurizr for Java](images/getting-started-1.png) - -By default, Structurizr does not auto-layout your diagram elements. The diagram layout can be modified by dragging the elements around the diagram canvas in the diagram editor, and the layout saved using the "Save workspace" button. See [Structurizr - Help - Diagram editor](https://structurizr.com/help/diagram-editor) for more information. - -![Getting Started with Structurizr for Java](images/getting-started-2.png) - -A diagram key is automatically generated based upon the styles in the model. Click the "i" button on the toolbar (or press the 'i' key) to display the diagram key. - -![A diagram key](images/getting-started-diagram-key.png) - -When you upload a new version of the same workspace, the Structurizr client will try to retain the diagram layout information. See [API client](api-client.md) for more details. diff --git a/docs/images/filtered-views-1.png b/docs/images/filtered-views-1.png deleted file mode 100644 index 8212d29e943ff3fba1bf01d35f87988989860a15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165819 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Ɩ=;|_bn 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:VsglslG0z(8uVbc+WrkgoyYQ7}7P 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 diff --git a/docs/images/filtered-views-2.png b/docs/images/filtered-views-2.png deleted file mode 100644 index 8d833165813342d5324a27593ec9e18c746b43d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 187584 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Ɩ=;|_bn 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⁢BDgf;vb6hi?XSD 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�UI%ZKf8G0{UfZA5AmX|fd2BuB#58_UX&-F1 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∑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

aL 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 MeNE*eB}Bmg06*xwf&c&j diff --git a/docs/images/getting-started-2.png b/docs/images/getting-started-2.png deleted file mode 100644 index 43cb96307851fd2621684c21e02ff7bcd39e497d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133350 zcmeFZby!tv*EcE%QX)tyEg&GNl9EdWF({=$LXnV`?!{6GX{4l)?(SBa#iBzR>F&-m z+57qSnrpwl&pCg7*SXmL?B!(6`yON5ztLmPFLE-H__!3fmo8nxfA&=T)ul@~xtA_o z>BU9|u4HiV9s>VEvwS7_=u&n&)dKKa^}T|Mm5Q{KppKad=bLwCZ*@8COw571E?p9~ z69j&m=vuv@vokR^wG^}yq5ta+LE!iKWiEQUzizQI5}{X-mZN)YW}!>R%gMuemtGW? zj*d>);+>x0EAc1)c^vqc2)%)omAN1nm#wWWr!6<9nT0;ry$25-aNWJnb^ksGa0iE_ zy{Xk3I}TGzhQAm2KkJC=TIyK5H@A9kW=eOy?whw})>b0)^yeG>^WWceTD{jp?qq8D zAGQDnxz7K>b&vBd*FS3mj|!h(6_mAjuM6yazP{)^;lJ)gx%QuZgt^W)2NCl(r+-}q zSQW(;=K4o$qPS8&nN=@cf?Rqg{!qaVZF$r=K*QP|Yu$0pbnI!SCq@yY0%KkuE0;`G z|Mg5*wLw<0F{?7;V(KFJ?Xw-klcJK zE*-}KLvts5lwVO&W4X#%F}TdBTDrF^(y?aQ!f4wevKKYJWgi2zsgAOYSh1=2E#Z0z z4IKlUkPh;vA23h!EKiBFFaPZxG;}vtNH5j@vclhs6DmRID2j`48UDD-e+Yok8D9C{ z2>I(4Y&JSV{9!e>>whNp?`7x+c`yI3{G8wNnUv7e-6ZShf8C1EGvYr~{YfmqpMK(E zi`Nw^z4@Z~Rw^(O+`^ zH{rWNRM21szxr8y{wu|>)SLfJ_z=2GR|t=>5$TivMlk@Fs~2$jpAxu$%OA0I0hd23 z_61xn;PSUgzi>wX)0hHRE}YSyefb4kF5rTcz(uJ0=eYPH)cs>_a{-qNxcr^mT_nnn$^~35-~y7s#Q?*_0K;F|$wf!{-;VX~VC$kI z{m1z8|BrCFd!zV1>U$%QB6P>yYvXw|UEawa9_8^1(GC_^ z4y-<@$a}Z(^S1Y_*i4+8H#vu0>qERycC}oAlTy3ioBF~g4f#oIVO$z{P75FHU39XF zq3z&zA7F9l2%8G(LLOa|msTJIm^PxD^Va<1s!VZvWKgr=4|Ah?)*DWer;|PM>}s_RiX}_;(G3rTX|gj4PElZ^qayhgGjPPx;^Z*+r~%4F7cbj?)ebXK3j1c-Z18 zHhCc+KZcHxU3%J-F<`1zV58$eGGF@S`p0WLJ>yxM6rCFc4ky{8H*UU9Q7IG|`9|oM z{6a$Z!A7aPGWF3gm%V0=^29z^N_29Pz-unl6?zEp)%owW_$8<8ZgCkoV^#xxSWjfR z^>58v>B?O9;}lw+02T}T*+nk4=DARuc%bnOhpt$ znhrYb%{MLBlwSVi+pqmWXr$UXiE`2JYLSNrjpOos7uz*4-M+j#R3e6h-<>NA@lf87 zg0%t%9r9A>I(W}R1?{Po;STkx)sJQ)cgw4{A)Wo&+%s_tKRYR^p12P)JibF^QaBu~ zShLYrb6PAfE3jGyH&pnXLdy9Es9Z0&o8~d6PZ`0S9&mqxxV} zy+gfVwROpi^2AtM(^6YgYn;N?i(YS}m*zdenn|MA%uWf?G`L%1Ujb7wKur~I7iww5m+7O1`^61-be+}CFDyUx+; zU7=Iud5Z?(s7$Kc0i9q}~08VL; z=GM>CI>+J1jugI=hF#G8g7Je|txDEMFAc#bbP0A7V4Hh$KLD%?JLFIInx)^*hpq^D zz4D@*ZkG-7= SR(tq?cvK`>-#c5Sm`8)vVd%?h+>!V_XMga+)`Wzf{oh~ogQW{& zX3Xsnu~5G(K{7;P_ec_VFDdXhJsI^)8Mmt4ee%;Ob!Z2*Ro#xtL$CyI058!|V1m!1 zo@BpXVxaP91l~Gyzglv&tt#3aYt%pL!={ktS!=lM-W{-1uzOzuvTDj!YR?B<@#>h=qRqbAMv$nop|>%vJM2P}?E-F!qQ8O3~XOeY*s6z`+(@>f_1) zU#*?O&aB$lFQ4%aM~5 z%nczn1Mse4&vQ30yyG^y@UjnUp-xqR0Mj_UQb!=PU$6$P!)eO;Haf5(4N?F!bY~_y!j%Gdm1`)6a%VkcZREIV z?Z~)dl+!Gz;;wFdfw1f79`VzN#}z#*mg-rvZnO?Pw3U_~EQ78A6wW#jKG#y~zq&##YJRmz^7ERAg+)pS)|79s+Zfvvy_EEPQ5 z0xr0BO2GV~8;g4`hei!(B0PoOnZ(z2elbXNxC|Se?aC@i@7Jq?Wlost>k294%gUDn zUzH-UJF9`7L~*?iDJCIzzt$GHAGkM4a$`*WHS3T;7tSv0Hjoy(NWIXNx|}q(zo|xuV*&x7#fNbeC+`@|&- zQQL!^wbNeR_f4}C=(epQmvUoPzOF_`s&yJv&W+?GCvPwFTtk(&2C+f$@Jcpy8w`AE zkT@)KEe$AqVeLRl(A?78MMv9(pnW2s002P`%}`La<;I|y=)va%{R7N@>5`&y>E^0F zD+7B=43^iS_{6D)P1VdY9-PN2hNn)p-uUIoGydv01hWzl3q&O~F&W>G;(%3?3rt15 zFg;p0v$rxB=Qp&1MSFZn4ozz8(?#?@18FGC2?)Usw$+kAboD--=rQVtZm5sT8tu$? zI`Cw^)=g0*Ki%`4%hFU%&{byzQ)@xEezl9LJo5=i7KRMwK)hcyu_NW}U;2N(1wczf zbMyT*PUEX~Rt6>%%IsElsUn4raeaqf4Qr0O1=rI0x;a9TJfNWmV!pmbQC49HiW%c< z(DkFwe1qM|UZ(OC+922}LWgYo55fjaSIEGM0ILO#w-%vD0iu+bxb%34)_I{dvO7NV zGM-_v6`TW8(6?gVE0VKj*W)QzrjR&UKrYVR*l>{lafL8w&_s|#eA=Q!F!`zB@55VDE4Ge@8rnt5=H2BIaJFUehVUjj!>?a zYxrM5vLR#co1T6)vr>+k@4hg5UbW&R(P~In=Pc>K+5Qkh%>Fofkdwj+O6lVG&?8O9 zPiKtyQfBxxXw)K;5pTao7rPt`N6IJYZ}3*2CMC)MFf?YYP`-!I_GF@xcjht^a1d7Z z^Trd4`DTVjH?95o$~oc1GT`&CCnEH0TudM<#cnY#{t+rHW}fN6g`{# zB*muV5MB)qBGJ%Cf#}%gao0W2<8*f&VH+tPd?$u{j@9K(47??kPq#<x>5gbFEaODCaMPajEc+k$7Vb=OHTq}yv_lH5k!8s^Ct=1Sgh62(8 zF)&RrnYBR90i^h9RJw7c=ACTSkimIt%96zXCN3p6kaZ-S=LVo0%Uf>&5@a((d#7(E zOFrrQ&25Lu`ux!arIso6N|5CO${Nqlv)#KwIN)uWuroi@jNnj&SFVik+WIzm>UEpx zfHKnNmMwn-!X3*~uXA;SNWLNx(R;Wyeps;KI|fozh%00b zV7H3j<_Z`~QiS`PyfY^jN#BX3EbP_!oUQGh%{gyp1c42NGaV4TePU8U2T}Y0$BKq- zs0?N(W1Ugd_5N_4_?^EZ% z$_*WFu!wfe-=X9qEh{Al6N#m%6r_*kob(M|)z0 zZ1|63E~SGjD3Bd=7o422k((gv(yUN>S)AMCI58jI-O`yfg$4!^KU%nTwzQv}|07mLJN}vodQYr*4Y2Cy%{-vO&J%jo2W@ygzOITNv zC$6|O2ADNgz|JUPtnh%AddHc2;8)Qd!~nYd_X{^!x#fwbJDH=KP8!h`jn$Dv?f zff2~@u!r7*ZJFu>pqOMcc;c2cAD^iS(sh(s#sKeSek<F)lW7=1lR-V7}xSaaa9yfOoJSaIlp!@+Ep0fjP@LNLAGRJE7eQ?m)92k&Ia{&g;y zxXLK0csiiFN*t?Bpr_uk-Ck9@P1!ZyfZnG%#24aUGI@Q{sq?*^Eq+*nsU zv^|Er?}X;?*OnlMa|wnE*kn>VAxsch%kbD$d1@HV;Ugo=ZyudQtIyh8(fO%o1_nGN zZWvH>Q*S{U(1RGe#47oAZ-!?5%8q_aSA6Hb8vUbL`QZ$R{y!Snl3Fg2tpM~mk+1xp z7MZbw>(-a`MVx;!{cA^NV8@tTK^E|iOt_h^T~sy_AXfCxXV>6wRPvTP+inKSf=>C* zEM}@`&T67}jbH&B&fLuuj+bg>Hr_?a-Z08J0jngKb{WMg<=1|dz`G{4Jn=R|$|_$v zR4h!#e82k>IagGy?ui$QE&QLA2beLY5~r40@0h7RTip5wn8j5knwNg{`$}uzSp0uuGYV7FW%;GV0YlvbhSVzVO9^$ZpTHYnIGnqfsyuG68;&CpIQ>RXXo4rc z730x<$;$nf8BQOH;i3m&iP2 z=BL>w?!`Z_g%f1t`5EeMlcYD0S>HjP!)^-Icz&0gH=npvAcsa-^=`AhalZc z1wPfMmQ_*!3Xua!T$?;+N6)&Ln;l#YW_3`?I;`hb=Z%CfA=-0G+&6vRmUn<$H!#bd+dT(c%x&b@BpMddI4QgBwMix|IZ72(pFKDGJpK<~WY^LkN;AGGaF+^C_JI(Lm*5n5aD zQ=}v^;~) zPZA(J1ccP4twuyD*1+Ei?E$p_(SKY0GN5eNo>XlKf=ED0?Yul=kB<28iZEt=h9Gab zlVK*1(F{0Xg6`_X0(KKM_;?$hos=T zKdg*oAG*7GS*mr?>C*nn=u1rT@{jcNUS9Mzd2t`B>NVdE)NnM`*BeOn-q87STY^|x ziswp2SG~Az7mJO~8fjh}y_fdG1#{aY=$@#`fRKRyRO#^M@LY_g&F+!j*a2cwgrK_3 z=d*#);u1(JVP8pq!L!Sfc=lRc${bCrOz+?iseok|z=ci}e{i385)`_`C1UK3>8Zr7 zYP1@rbBk#OTbPV z1G}&i*cBeRgc8);gtb%8F}HO0ZF!PpK<<|X;v*Pqsz7wnLE?BZ7{%2eYs7-|h%lA( zTaB-ww4!SHm1oy+>kAvFLqi6+O0by+0zeP5P@&;;{S0Rd1le3UD|F{_TUGO~sS@!L z{1xGs-*Wd*40e8-r>Ba!1yXHD90b@G?_K<7P|*_-h)Y$|5gHySG1~@k8#(!{bLjK@ z2S*dlcMRNAAZvt85(;qp1`%ce78D;$@n}cbF2(n)f(#Kyews8B50h=DSCyBsfjf$& z`$-f)$dP$K!~xPu36}Ly=O^!;kY?Z*rF)y(B|S*|);cuvoq1VZs4VG=b}v{y%>)4F z$KSACf^aYQ6a6vPj>tjWOSAyslz(28;N5=5D*M2$S1@CjBOP`VrBse9M;9WKf_K;W z7D$~Saj6*T=|=rSYf*bl&Y|hX%1T|ysf|h>Kc&TlWk(cjZjNe|Eflumo)MN|)S$V8 zLN|yD&c3&*3&O(yd_dB$r;*Xt+wJ#uheF&=M-3lB@w!_!JGuRbp79mNF~3>|F-77F zh_p*837pW${X#!7D>;>^8({S|aY{FiQm>T{53jZoOx|=|>F1hwgN+iSNifpgQWFn) z&jMby6q0$T%_q)5Q&rj;Mv$aX=l{iffsHLvEF`H_djxbRVKW;b)yjx#KfqG${y$QE z7_eNX)=lR2VE^6ZTOu7+JWGst6z_Q*H}NfhGCfr&D$ed=0wQ%^hOSo7I;$yl@hJ`# z-^F?v|5Be1o)t3;JuuA~u~Um|^&(Qdi;{p%$GVcnAMi8NgKTr$XG~VMR_M{Es{5Dv zEa8Uf&sE|(-D!eI<&~6dS_t++@i_`xh`||>1ScSpX<}nL5ThhfwSIL2!Y7{8^vhO# zQTHPcN$QUqW?$S>)z!WCLCCmV@8M_mr(n1T`vGUO&{l+^X?M}LwhZyQjWLOB)2>7$ zX4_I#D}8O@RJ<~UB9<83r=GJ5_1NCXumMIyUb;Q)>}a(TJjh#O!_ZGO(7g0}z|@o~ zXy=G_C3@fkE=vBD#0oHx&TR%ht?3d-jiv?zGsV5+jLExQi^tcBKPP0QwM)sVs!A!< zXu?D8e*O-|HXRg`QGCnRR2(#>1o?uuEc!ND{@NHHAr(_Z%J^qYM~Yad&eU(2!uYRL zKr$m_v4<%f?_^D1zXU=D1X$EYaD!k^j5|BClD;qb@#z!QuYpN(2(N|;^_-d^jWDMa zMzpIaO#ruB+HMAK+JacXwnG(`(Jo+Mel`|p_o5?GBOlvdV7<+%S0N|QBDPDPCd%VC z4(i8<^8p{Biij?r!@|>t$G*c(20FYX5<|Kyc0LjK(+{DQ(PN?=4>i_lv`}V3QwO)%9x#frf{3`2@|E7NRCOQ7iGr- zVI|Md%ug_|h~wnWZK`f^J=mr`!@H`bA41tK%KlhRgnw(QPYwO%nSIxR?;O?jpl-F7 z^I=F}05ni=(cK!dV^ap%bihYo+6+21hom?Q(KoXbWg%LM^mvj6_db%A-@7hkR;=`% z;s!66eq0&Iqr!<~santp24~kk6f(8?jZ@LIAEcrFf;?XFNlc5oUc0&P9nUWYA%s?_ zIjl5c0HFzbggRJSordV+!)6&h*TzN5a}ZyKgi4Z_;vG!C;)?*v^cGhwUHJrz4p#^_ zc4GH}6*o@;4p^NC4IaW?nAz`3hK)oljtQYYgcZ9!UysHO( zgd`u(z*`+Lzrdh*16x{iIc&Df?zq(ag%gqUkdM-#Zvo52(E~qBPk9KehL>PT^mMn% z6;yb^T*Al*_d9fQT#mN83g+6`*m9(b-jAb;gsPD**cSliWvIl&a_WJ{nt&7H6=n4X z%9#iNXYH*+Ma{A)Mgzp{`^vI{C`mkLRh#;bv)ilcr9ay7n zjMDE5%_R32U7II1daS%QUSZ{Kng}{_LKacLqEAN3Gk|UAG)A(M=*d}o)oG1vB@KhD zVlhXCfw~0-f`7uQZzO-QtZVFAWVU^SY|W@;nG9l}I1~&4NF3>Tz=Ea`2HJ@IHmpQ! z|J}$@P*c^}MkH~v)9#GVd+xdVj_vS*0=KfQ^srY02Ytp=cPL&+n8a3;7JMCKvX!Bt zC@vEb2=*B}!MysfdmIFfe)IK&bCX@?5ad+#1bpMin?N56qB zytC-Y?{B|4dlVCSJXm-@r*-=w1A!Waw`k<9Vsql*Y+HwpV&OBz!iF8*W|ga|A|;s) zn#uQ-NSo3!x*N4FA09?ZSrnGbSQOfpa31-*wjF=DWK!7Qs#P;21lIfMKtLsEq}}B} z7;MI^*DNI;->&7*iGeO;ec57T&{ce?sx;m37{&{#_|#6~ z9dFS3zcxoy1&N;Aw=~K`^VOkdAdUEHpz72*?e}Yk$HpxyEq#amASs{>PoJCFz47PZ zW^!xjv2>Pj~!AU%H4}q3j-~nCQDxe%T!Ps^x@7^rHmAp$d>kR?jd;L5xBQuzlMo zXX#$E3eGen8*uQQUXD|**yelHemi%YPsvTM+q%f;9k)7G35PeaVpW6&9rJ>yQ?#NB zuvY>f#mI8B0m=-cHya?-(zn^5lq)VAGlq8Whugr&Q3sY*TVdq$BzpOoD7YTqse@%1 zpiV(;&qtn$k-t~t3BvVV`N+*i&Yjc!nj+Gts==k{AoGo8#g|nYb$pST027a_{+tHE z0hFQHMZBk;{roRM=?4T4r&4TTOMq>gt3)hxTYpp&uZ(##u?KJ0Us|8l%k0x+l=N$c zo@zSM2*a1tuU6UhIW_|;@cj!UlotzL-*LPFp8d>X0lc->emTx?TLZenrpT)On6_pm=;?ti!2ajS^(+p2fWuKUm;9kvk@>(> zzGC)QNk&7>yYR%Lkpqw7(2!`0^;8v(p|r;pWd4smKt?yX98irk=w+YSe>@NbjyJM3 zy(Agse8McBqFF^`j1_ktxx^Pq(TLW%JUd`Hsc0I zKTI(W9i(;U;P*y`k~Jr$GH^sZs4bElkWr5QFkwTP)75=%9yHnr>m~y-xRnEbP>G6z z)BGUIKWaY``mUT~HGxf?s@Pu8hnSxs`I1U%O|}}lcJno z)FphgZ$(J0e+b|$*F5kj(>$)L@*B1EM)%e#1+n@E?@tm@>(6yEaGPJ`+n4fqli7N? zoJ!JWs#?{G4P(Ba&0NWE2Mr|N9jz=eZ@ghuFxBylfvvjzA?LM0F)r z37i$}GKU;Qr*ou)#-$9YbC31~Cry#okRMO;Q8?>v#z^E@9a% znZ#LXrsKtkd2WI+=Xyam5vndw=o~P9Zu>;U;z5#_K(DFVfW^MHQP(3R4GAy$uGzqp zt9?fw@zFXeyjSn*=Q%X@F1F02YGr^_g3%qr*az_`C8K9xF@pRYx8mPJdI zYaJuO3@%=HNdm#K-E22#u4y{57N@3s#tVAFMXV?>te`vWgeNNuL|z$+G2zn&E0Y~V zB=1e$KP)e_iJt$+n^2VTlp)HPhDTMRyb!(D^b|WWV>VQ=!`IGl$hlzr%pmp{M+!X9 zMiK<5>ODkq9x`TWCWpz^eETr3I^bt-Xp9l8dPD7HI9K48PnTd$#Hrb>?4%rQlto<= z86CoXMH#SHYwlpxXmk@Pl0S2u$CF7C> zEfWpOnJnVZP7!$r#9<)D30bbeHYabmg9|AEV(5ADtH;I$k=OFSA8>Qg-wLP!`T}Wo zuM=CRx~>xh9Q(?H>(C_00CR8JBU6!~`E*9DnVA#a1Ad+YKC4rlzypemQf$IzTA097 zJ{k&L-Hl6Mnx3B4-zEaNdM^lYEZ9t4p!96oyS9eLVSzbpl&tGrJARS8X|rqkuJtyPq59C)a31An7(}KUs}tVC z`+6?wr^*P9jF{nSlSq$KtI5uX&RZP98^~j#lk-GZ@+6zg{1T3|vST<>kq_no9nD6# zE48}Mj&5UnLVjbvqoE@W*|_^f^0}}IF0(nfSO7>6z_$=TJqX_rbjcXY(b7HUCcd&3 zzc5UaGC3T=W`5YajupGa`cJpV-x7nTAKd^~qvlz_1WJnzZm&(@oK@UJYbR(JU?;3XxP~!w!v|mVXyBA+_cKB_DPcvb?Ne2UIWUvd*Yb#5oOUO5WAlI>_VtMt~MfIWg ztsPYV(ntCmfwu_h0?YurU8dV-36xHFc0;)Zt#j|#RwMlFkkKKDyX#3Nu1<_NG6$z4 zz~ro)9h2fKtbD}BL!dAD6vOK=-Im>*98XDUVRJ7X@h9L8G8|9k*3jmx1LHpdq1;vU zHQ7U_qN)zBz{DXdPb-OgS6glXfR=?tEN)k-ULPv0i7k5-JG06C^>2HEEz< zKQ1LWwK&qb7op&m@Yq}TmG37A4IZO^gxzv|9kYKO33#;A9;h5eq!2T(BUd*=dkoS;gLc4BuSi1>Z%0L{GW`?0 z`_6f`+@aY4+Wont4r)1L+Y956liDM7e&7h32^%S-Bn3{1F;Z17(2z z4+BVbg?%J0)ot6lZrQ3=hRJV<&II%gT0XJZvyhHXTm=nBz-GyTDv*mj2QnjM5rw5_ zI`1WwFoh<)3iTixpQgAD`$_C65uc_Y9311YQ}q*1vmVJVc3~+10`H(DWGVnb!yOTx zEtBfygLBlD5#NVRlB7CQA)q{@bJ)J%p06G;ush##d=UKJ!qitCZ#@Z1H+L|&E}Q@T zItwFg`h(S$bJ3YnLI65Bl1DW3EIJ^(!ksS%X|f8st?*`RQ>?RUD67AZ!5axqHFOLw zB{~|(w9S@S>57xzVcs@ASHLw=ZlEW`N1Uh!LSRlUN0Cu30ROR@eD~2Y?*s@3pSqlF zVcx8Y1B?A3Aa=6)ep2Mo{Uj+}${V;33oE(Pwk{#<3Upk0MT1uzmiy?bW8NUyr0YBn zKoL*UZXvNJac3RCd~pKc1d31WNz1y!B&=lK1Z zNsL?rlWl!{%S`tzm#u@r51H?^I3#(Hm4t=gbK{(owiFu}ZQAaP|wq@@o7$qmCalQIc8M&6S= zfK8I7I`c?8DlAV8WFGctW6n>de@I=coS^rWQ>YY@{M6PLO^#)$qPX2F3SGf=KXUx$+A`zuqWJ&H>6d^zcdF)3qCw%v2S

S^5r!V_<&1(uR#SCvkL&1@qNb{oxx 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 C@Br2T diff --git a/docs/images/styling-elements-2.png b/docs/images/styling-elements-2.png deleted file mode 100644 index 27abd955dab8b7893a5c41c5a8d43e77e0b6866b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 175074 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

ej-rlX_eZ3C_wz4gXx0tHA$lQ>{d-gIKZuF{ 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 z z4V5RR4C-b;U6{l9Zv8f|-r#x!;~(#8Wla3uw$x2|Go#TVUmbwCd6M)qz;Js^c+|FC zi9|wgnj#H&$TwVx5X&QbOGZ2(N9umKx$);RXU$p;AWClW0BMhfds@`T=`Ta_Yh+=J zzJ=xiHF%RqY$5O)PRp2Cv_evx9YBdMWOW)_$-?^UO#vGOYGf;(abEJ5Plf zIeR1q<xdy5pxKK%!TD&W+d*h^K~BGgc;b$%o;ViO$!4k7SbxgFHs# z(iL(-FcElM`8TSyiYd=OmQ5ESBTxCnd%=ap-SQd(eIPVFFnoLo7r8!p2)q#ld%95H zmReeT-4wwcBwe6M?Y1&6U|Hd}nc$Z)gi$K&``DMe+V28Ao&=K&L0$lA1bm+f6K0eZ z55gtxGa_ckJRJGZao0uMUMzXE1?}F%-FNenFX#X>q|F#rTdRf{RYj|A1!L$*SZ`aBqTuWIRrfftkyCTjj&h*4eA9c|r4IjBzg#q1VD3IN6KPSDIVRz4nK2 z&;aXa_@)iIXQOTpWr)2Qs!cKuIf^;Lp7KDJKC|$A+0F?2jpaCkS+Arjc`tFE?`)US zkoieu_@vTuI8Z$13;hvJcOK8%=NPuKQ;<$%X3@nZISiZ%X-qHl0L+rF<-Llyktcym zs%SwrwP(EmslbQ~5`E56MIH5x-pZ2f@OgSHP7H2v`68)AzO-cpkJ!>39|J~l@r5Gd zIno6Q3F%8$NI%I`9lJy-SbglQ;HCVob}^4puR^)rfdLd!yJ;uhoYcv*ou3#P$+o)c z9z-LW^V;IElrH+4v*jlL|Mv}=|K{a?0gt}NpULALLbV$z>K1tOt)4z2ZGBSfAxt*< zBZf4kQ-w#pM#N*EZ|B)7I$K=U^juwFQLmOME}W{u;6Y@b!mCF+tmeiqpT;x~`V+{8>WC?d$;r??pP-CS)=jnW;eNfyu?3`fSd>9G(o zqY@nYHA2sGiLOp?tB41xo3>BBmhew^#$n>nYq7dDLw!YygY?G+dH_F)yQj7kEhGhQ z;Ns+HahS>cY|ho5a>=m``P2P&M8;z-T0>QrTU@3WrG_eB^z!eF$Y#w}Wu*mGXWr9DCuVy((F_~Dp5Y2{&=vr2CQiI@)Lf7z-~-Po$TimkxnFT zDLqDg#SC}CK8nR-c3V@Hon+?ANU31;zD|8N{hZqUw+IA6YeK=Yw&q}#qYq-Y z-6426MMe{WPBGM6YR)d4f1*8Avp({gRAJRB6q#ObqwhCD3)Nq>?ubbZ;jFpSh@2QWwslp9ywM@0U>^ zoC}h9-V&$S_hPKpGxw2m#^Cn;&YD0`q4Z47@*s$UQ%y1auFTTCajR+T-)!lN|MI#1 zXU^oYB~I&+kcP2;AQ%GPJClXgQJqV7w_fK|tKKsBkdM}enJpmfGq>W!%DAsQCmS+e z$XO+*vZm%q3UBJ`|GXMV!8P0yKYqM~cFB+b(w*lI?x?ri7J8vnx-8d1+>bVLb*-$h zn(!SIS*gf`I5_Na>-H_;_-M33}#KdN%+10(e6gpc`SXa<5X+p zM49nCuZa7u+T!xEqKF{y9yow#qq)_|7Di<1*~|a(hyVLWkzS>|XrQb1ZHM#>tHk%F zAetT^>c|53P800yTmWqrhuR+D9;)Ejaec5&*%yn$9_LB^&7b<`?@{;RBx(Pf zD#0nqzu0zv{w1$VXV2sXzQC{r{`GVDb-Ny2y=ZV_bt&S;f85mnxK2sdJ9WtMwql;Y zzV=^NK%xEMrS;cJqi6s46aV}T|K&Qb<-a^O^zMFN=YRaEe>{)@Y1*r1+-#ENcIPnPaK{nS5h_WwPAf4+bJxHEq~xc_?qf3b7^_dB4@^LSKV!m|#l RGsnPxCZzG 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 z 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≪OM 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 z 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ª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�(NqP}W}cwIW{2vsNc;}S~KAQ35| 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

PE8RJTqr*@wP7l+B1-+(~+o?y`TWG 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

ZkR>{l`ij{2 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 z%0D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%0DD4{0 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 diff --git a/docs/images/styling-relationships-4.png b/docs/images/styling-relationships-4.png deleted file mode 100644 index 1bd7f7a6ba4938e553a5a881a2cf08b5fb2098b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90321 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 G;Qs-$@c4HC diff --git a/docs/implied-relationships.md b/docs/implied-relationships.md deleted file mode 100644 index baff56730..000000000 --- a/docs/implied-relationships.md +++ /dev/null @@ -1,93 +0,0 @@ -# Implied relationships - -By default, the Structurizr for Java client library will not create implied relationships. For example, let's say that you have two components in different containers, and you create a relationship between them. - -``` -SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); -Container container1 = softwareSystem.addContainer("Container 1", "", ""); -Component component1 = container1.addComponent("Component 1", "", ""); - -Container container2 = softwareSystem.addContainer("Container 2", "", ""); -Component component2 = container2.addComponent("Component 2", "", ""); - -component1.uses(component2, "Sends data X to"); -``` - -At this point, the model contains a single relationship between the two components, but there are three other implied relationships that could be added: - -![Implied relationships](images/implied-relationships-1.png) - -- Container 1 Sends data X to Component 2 -- Component 1 Sends data X to Container 2 -- Container 1 Sends data X to Container 2 - -To have the client library create these for you, set an ```ImpliedRelationshipsStrategy``` implementation on your model. Possible implementations are as follows. - -## DefaultImpliedRelationshipsStrategy - -This strategy does not create any implied relationships. - -``` -SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); -Container container1 = softwareSystem.addContainer("Container 1", "", ""); -Component component1 = container1.addComponent("Component 1", "", ""); - -Container container2 = softwareSystem.addContainer("Container 2", "", ""); -Component component2 = container2.addComponent("Component 2", "", ""); - -model.setImpliedRelationshipsStrategy(new DefaultImpliedRelationshipsStrategy()); // default -component1.uses(component2, "Sends data X to"); -``` - -Relationships that exist in the model: - -- Component 1 Sends data X to Component 2 - -## CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy - -This strategy creates implied relationships between all valid combinations of the parent elements, unless the same relationship already exists between them. - -``` -SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); -Container container1 = softwareSystem.addContainer("Container 1", "", ""); -Component component1 = container1.addComponent("Component 1", "", ""); - -Container container2 = softwareSystem.addContainer("Container 2", "", ""); -Component component2 = container2.addComponent("Component 2", "", ""); - -model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); -component1.uses(component2, "Sends data X to"); -``` - -Relationships that exist in the model: - -- Component 1 Sends data X to Component 2 -- Component 1 Sends data X to Container 2 -- Container 1 Sends data X to Component 2 -- Container 1 Sends data X to Container 2 - -## CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy - -This strategy creates implied relationships between all valid combinations of the parent elements, unless *any* relationship already exists between them. - -``` -SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); -Container container1 = softwareSystem.addContainer("Container 1", "", ""); -Component component1 = container1.addComponent("Component 1", "", ""); - -Container container2 = softwareSystem.addContainer("Container 2", "", ""); -Component component2 = container2.addComponent("Component 2", "", ""); - -model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); -container1.uses(container2, "Sends data to"); -component1.uses(component2, "Sends data X to"); -``` - -Relationships that exist in the model: - -- Component 1 Sends data X to Component 2 -- Component 1 Sends data X to Container 2 -- Container 1 Sends data X to Component 2 -- Container 1 Sends data to Container 2 - -This strategy is useful when you want to show a summary relationship at higher levels in the model, especially when multiple implied relationships could be created between elements. \ No newline at end of file diff --git a/docs/model.md b/docs/model.md deleted file mode 100644 index 969755d6d..000000000 --- a/docs/model.md +++ /dev/null @@ -1,20 +0,0 @@ -# Model - -This is the definition of the software architecture model, consisting of people, software systems, containers, components, deployment nodes, etc plus the relationships between them. - -All of the Java classes representing people, software systems, containers, components, etc, and the functionality related to creating a software architecture model can be found in the [com.structurizr.model](https://github.com/structurizr/java/tree/master/structurizr-core/src/com/structurizr/model) package. - -An empty model is created for you when you create a workspace. - -```java -Workspace workspace = new Workspace("Name", "Description"); -Model model = workspace.getModel(); -``` - -Once you have a reference to a ```Model``` instance, you can add elements to it via the various public `add*` methods that you'll find on ```Model```, ```SoftwareSystem```, ```Container```, etc. - -## Automatic model generation - -Although there is nothing included in the Structurizr for Java library to support automatic model generation, -you could choose to parse an external definition of your software architecture (e.g. an AWS infrastructure topology, Terraform definition, another Architecture Description Language, your source code, etc) -and create model elements accordingly. \ No newline at end of file diff --git a/docs/styling-elements.md b/docs/styling-elements.md deleted file mode 100644 index c0ae0967f..000000000 --- a/docs/styling-elements.md +++ /dev/null @@ -1,78 +0,0 @@ -# Styling elements - -By default, all model elements are rendered as grey boxes, as illustrated by the example diagram below. - -![Default styling](images/styling-elements-1.png) - -However, the following characteristics of the elements can be customized: - -- Width (pixels) -- Height (pixels) -- Background colour (HTML hex value) -- Text colour (HTML hex value) -- Font size (pixels) -- Shape (see the [Shape](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/Shape.java) enum) -- Border (Solid or Dashed; see the [Border](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/Border.java) enum) -- Opacity (an integer between 0 and 100) - -## Tagging elements - -All elements within a software architecture model can have one or more tags associated with them. A tag is simply a free-format string. By default, the Java client library adds the following tags to elements. - -Element | Tags -------- | ---- -Software System | "Element", "Software System" -Person | "Element", "Person" -Container | "Element", "Container" -Component | "Element", "Component" - -All of these tags are defined as constants in the [Tags](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/model/Tags.java) class. As we'll see shortly, you can also add your own custom tags to elements using the ```addTags()``` method on the element. - -## Colour - -To style an element, simply create an [ElementStyle](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ElementStyle.java) for a particular tag and specify the characteristics that you would like to change. For example, you can change the colour of all elements as follows. - -```java -Styles styles = workspace.getViews().getConfiguration().getStyles(); -styles.addElementStyle(Tags.ELEMENT).background("#438dd5").color("#ffffff"); -``` - - ![Colouring all elements](images/styling-elements-2.png) - -You can also change the colour of specific elements, for example based upon their type, as follows. - -```java -styles.addElementStyle(Tags.ELEMENT).color("#ffffff"); -styles.addElementStyle(Tags.PERSON).background("#08427b"); -styles.addElementStyle(Tags.CONTAINER).background("#438dd5"); -``` - -![Colouring elements based upon type](images/styling-elements-3.png) - -> If you're looking for a colour scheme for your diagrams, try the [Adobe Color Wheel](https://color.adobe.com/create/color-wheel/) or [Paletton](http://paletton.com). - -## Shapes - -You can also style elements using different shapes as follows. - -```java -styles.addElementStyle(Tags.ELEMENT).color("#ffffff"); -styles.addElementStyle(Tags.PERSON).background("#08427b").shape(Shape.Person); -styles.addElementStyle(Tags.CONTAINER).background("#438dd5"); -database.addTags("Database"); -styles.addElementStyle("Database").shape(Shape.Cylinder); -``` - -![Adding some shapes](images/styling-elements-4.png) - -As with CSS, styles cascade according to the order in which they are added. In the example above, the database element is coloured using the "Container" style, the shape of which is overriden by the "Database" style. - -The set of available shapes is as follows: - -![The shapes available in Structurizr](images/styling-elements-5.png) - -## Diagram key - -Structurizr will automatically add all element styles to a diagram key, showing you which styles are associated with which tags. - -![The diagram key](images/styling-elements-6.png) \ No newline at end of file diff --git a/docs/styling-relationships.md b/docs/styling-relationships.md deleted file mode 100644 index ae696bd34..000000000 --- a/docs/styling-relationships.md +++ /dev/null @@ -1,48 +0,0 @@ -# Styling relationships - -By default, all relationships are rendered as dashed grey lines as shown in the example diagram below. - -![Default styling](images/styling-relationships-1.png) - -However, the following characteristics of the relationships can be customized: - -- Line thickness (pixels) -- Colour (HTML hex value) -- Dashed (true or false) -- Routing (Direct or Orthogonal; see the [Routing](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/Routing.java) enum) -- Font size (pixels) -- Width (of the description, in pixels) -- Position (of the description along the line, as a percentage from start to end) -- Opacity (an integer between 0 and 100) - -## Tagging relationships - -All relationships within a software architecture model can have one or more tags associated with them. A tag is simply a free-format string. By default, the Java client library adds the ```"Relationship"``` tag to relationships. As we'll see shortly, you can add your own custom tags to relationships using the ```addTags()``` method on the relationship. - -## Colour - -To style a relationship, simply create a [RelationshipStyle](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/RelationshipStyle.java) for a particular tag and specify the characteristics that you would like to change. For example, you can change the colour of all relationships as follows. - -```java -Styles styles = workspace.getViews().getConfiguration().getStyles(); -styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); -``` - -![Colouring all relationships](images/styling-relationships-2.png) - -You can also change the colour of specific relationships, based upon their tag, as follows. - -```java -model.getRelationships().stream().filter(r -> "HTTPS".equals(r.getTechnology())).forEach(r -> r.addTags("HTTPS")); -model.getRelationships().stream().filter(r -> "JDBC".equals(r.getTechnology())).forEach(r -> r.addTags("JDBC")); -styles.addRelationshipStyle("HTTPS").color("#ff0000"); -styles.addRelationshipStyle("JDBC").color("#0000ff"); -``` - -![Colouring relationships based upon tag](images/styling-relationships-3.png) - -## Diagram key - -Structurizr will automatically add all relationship styles to a diagram key. - -![The diagram key](images/styling-relationships-4.png) \ No newline at end of file diff --git a/docs/system-context-diagram.md b/docs/system-context-diagram.md deleted file mode 100644 index cc4bcfd58..000000000 --- a/docs/system-context-diagram.md +++ /dev/null @@ -1,13 +0,0 @@ -# System Context diagram - -A System Context diagram is a good starting point for diagramming and documenting a software system, allowing you to step back and see the big picture. Draw a diagram showing your system as a box in the centre, surrounded by its users and the other systems that it interacts with. - -Detail isn't important here as this is your zoomed out view showing a big picture of the system landscape. The focus should be on people (actors, roles, personas, etc) and software systems rather than technologies, protocols and other low-level details. It's the sort of diagram that you could show to non-technical people. - -## Example - -This is an example System Context diagram for a fictional Internet Banking System. It shows the people who use it, and the other software systems that the Internet Banking System has a relationship with. Personal Customers of the bank use the Internet Banking System to view information about their bank accounts, and to make payments. The Internet Banking System itself uses the bank's existing Mainframe Banking System to do this, and uses the bank's existing E-mail System to send e-mails to customers. - -![An example System Context diagram](https://static.structurizr.com/workspace/36141/diagrams/SystemContext.png) - -See [InternetBankingSystem.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/InternetBankingSystem.java) for the code, and [https://structurizr.com/share/36141/diagrams#SystemContext](https://structurizr.com/share/36141/diagrams#SystemContext) for the diagram. \ No newline at end of file diff --git a/docs/system-landscape-diagram.md b/docs/system-landscape-diagram.md deleted file mode 100644 index 457998bf7..000000000 --- a/docs/system-landscape-diagram.md +++ /dev/null @@ -1,13 +0,0 @@ -# System Landscape diagram - -The C4 model provides a static view of a single software system but, in the real-world, software systems never live in isolation. For this reason, and particularly if you are responsible for a collection of software systems, it's often useful to understand how all of these software systems fit together within the bounds of an enterprise. To do this, simply add another diagram that sits "on top" of the C4 diagrams, to show the system landscape from an IT perspective. Like the System Context diagram, this diagram can show the organisational boundary, internal/external users and internal/external systems. - -Essentially this is a high-level map of the software systems at the enterprise level, with a C4 drill-down for each software system of interest. From a practical perspective, a system landscape diagram is really just a system context diagram without a specific focus on a particular software system. - -## Example - -As an example, a System Landscape diagram for a simplified, fictional bank might look something like this. - -![An example System Landscape diagram](https://static.structurizr.com/workspace/28201/diagrams/SystemLandscape.png) - -See [SystemLandscape.java](https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/SystemLandscape.java) for the code, and [https://structurizr.com/share/28201/diagrams#SystemLandscape](https://structurizr.com/share/28201/diagrams#SystemLandscape) for the diagram. \ No newline at end of file diff --git a/docs/views.md b/docs/views.md deleted file mode 100644 index b53c9e22f..000000000 --- a/docs/views.md +++ /dev/null @@ -1,29 +0,0 @@ -# Views - -Once you've [added elements to a model](model.md), you can create one or more views to visualise parts of the model, which can subsequently be rendered as diagrams by a number of different tools. - -Structurizr for Java supports the following view types described in the [C4 model](https://c4model.com), and the Java classes implementing these views can be found in the [com.structurizr.view](https://github.com/structurizr/java/tree/master/structurizr-core/src/com/structurizr/view) package as follows: - -* Core diagrams - * [SystemContextView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/SystemContextView.java) - * [ContainerView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ContainerView.java) - * [ComponentView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ComponentView.java) - -* Supplementary diagrams - * [SystemLandscapeView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java) - * [DynamicView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/DynamicView.java) - * [DeploymentView](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/DeploymentView.java) - -Please note that code diagrams (level 4 of the C4 model) are not supported. - -## Creating views - -All views are associated with a [ViewSet](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/view/ViewSet.java), which is created for you when you create a workspace. - -```java -Workspace workspace = new Workspace("Getting Started", "This is a model of my software system."); -ViewSet views = workspace.getViews(); -``` - -Use the various ```create*View``` methods on the ```ViewSet``` class to create views. - From 245e573e24a8b4ea1ab32925ec6ae8ecbb17ef3f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Dec 2023 15:30:54 +0000 Subject: [PATCH 135/418] . --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index e02d80837..fa0f33d35 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,7 @@ - Adds `com.structurizr.api.AdminApiClient` as a client for the cloud service/on-premises admin APIs. - Adds support for inter-workspace URLs of the form `{workspace:123456}/diagrams`. +- Deprecates `StructurizrClient` in favour of `WorkspaceApiClient`. ## 1.28.1 (11th December 2023) From b703ddaad8d1bc97ff9dc1e51359ace485cb8b2b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Dec 2023 15:32:20 +0000 Subject: [PATCH 136/418] Updated to reflect release. --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index fa0f33d35..301b56efe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.29.0 (unreleased) +## 1.29.0 (28th December 2023) - Adds `com.structurizr.api.AdminApiClient` as a client for the cloud service/on-premises admin APIs. - Adds support for inter-workspace URLs of the form `{workspace:123456}/diagrams`. From d830b157ace65d9b54bef97facf06507a4ce6c50 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 5 Jan 2024 11:25:11 +0000 Subject: [PATCH 137/418] Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). --- docs/changelog.md | 4 + .../src/com/structurizr/Workspace.java | 194 +++++++++++++- .../src/com/structurizr/model/Container.java | 14 ++ .../com/structurizr/model/DeploymentNode.java | 37 +++ .../src/com/structurizr/model/Element.java | 6 +- .../src/com/structurizr/model/Model.java | 134 +++++++++- .../com/structurizr/model/SoftwareSystem.java | 14 ++ .../unit/com/structurizr/WorkspaceTests.java | 237 +++++++++++++++++- 8 files changed, 623 insertions(+), 17 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 301b56efe..90451c105 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +- Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). + ## 1.29.0 (28th December 2023) - Adds `com.structurizr.api.AdminApiClient` as a client for the cloud service/on-premises admin APIs. diff --git a/structurizr-core/src/com/structurizr/Workspace.java b/structurizr-core/src/com/structurizr/Workspace.java index 3c2dc0334..8dd320eb7 100644 --- a/structurizr-core/src/com/structurizr/Workspace.java +++ b/structurizr-core/src/com/structurizr/Workspace.java @@ -4,7 +4,7 @@ import com.structurizr.documentation.Documentable; import com.structurizr.documentation.Documentation; import com.structurizr.model.*; -import com.structurizr.view.ViewSet; +import com.structurizr.view.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -12,8 +12,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; /** * Represents a Structurizr workspace, which is a wrapper for a @@ -231,12 +231,190 @@ public int countAndLogWarnings() { return warnings.size(); } - private String typeof(Element element) { - if (element instanceof SoftwareSystem) { - return "software system"; - } else { - return element.getClass().getSimpleName().toLowerCase(); + /** + * Trims the workspace by removing all unused elements. + */ + public void trim() { + for (CustomElement element : model.getCustomElements()) { + remove(element); + } + + for (Person person : model.getPeople()) { + remove(person); + } + + for (SoftwareSystem softwareSystem : model.getSoftwareSystems()) { + remove(softwareSystem); + } + + for (DeploymentNode deploymentNode : model.getDeploymentNodes()) { + remove(deploymentNode); + } + } + + void remove(CustomElement element) { + if (!isElementAssociatedWithAnyViews(element)) { + try { + Method method = Model.class.getDeclaredMethod("remove", CustomElement.class); + method.setAccessible(true); + method.invoke(model, element); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + void remove(Person person) { + if (!isElementAssociatedWithAnyViews(person)) { + try { + Method method = Model.class.getDeclaredMethod("remove", Person.class); + method.setAccessible(true); + method.invoke(model, person); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + void remove(SoftwareSystem softwareSystem) { + Set softwareSystemInstances = model.getElements().stream().filter(e -> e instanceof SoftwareSystemInstance && ((SoftwareSystemInstance)e).getSoftwareSystem() == softwareSystem).map(e -> (SoftwareSystemInstance)e).collect(Collectors.toSet()); + for (SoftwareSystemInstance softwareSystemInstance : softwareSystemInstances) { + remove(softwareSystemInstance); + } + + for (Container container : softwareSystem.getContainers()) { + remove(container); + } + + boolean hasContainers = softwareSystem.hasContainers(); + boolean hasSoftwareSystemInstances = model.getElements().stream().anyMatch(e -> e instanceof SoftwareSystemInstance && ((SoftwareSystemInstance)e).getSoftwareSystem() == softwareSystem); + if (!hasContainers && !hasSoftwareSystemInstances && !isElementAssociatedWithAnyViews(softwareSystem)) { + try { + Method method = Model.class.getDeclaredMethod("remove", SoftwareSystem.class); + method.setAccessible(true); + method.invoke(model, softwareSystem); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + void remove(Container container) { + for (Component component : container.getComponents()) { + remove(component); + } + + if (!isElementAssociatedWithAnyViews(container)) { + Set containerInstances = model.getElements().stream().filter(e -> e instanceof ContainerInstance && ((ContainerInstance)e).getContainer() == container).map(e -> (ContainerInstance)e).collect(Collectors.toSet()); + for (ContainerInstance containerInstance : containerInstances) { + remove(containerInstance); + } + + boolean hasComponents = container.hasComponents(); + boolean hasContainerInstances = model.getElements().stream().anyMatch(e -> e instanceof ContainerInstance && ((ContainerInstance)e).getContainer() == container); + if (!hasComponents && !hasContainerInstances && !isElementAssociatedWithAnyViews(container)) { + try { + Method method = Model.class.getDeclaredMethod("remove", Container.class); + method.setAccessible(true); + method.invoke(model, container); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + + void remove(Component component) { + if (!isElementAssociatedWithAnyViews(component)) { + try { + Method method = Model.class.getDeclaredMethod("remove", Component.class); + method.setAccessible(true); + method.invoke(model, component); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + void remove(SoftwareSystemInstance softwareSystemInstance) { + if (!isElementAssociatedWithAnyViews(softwareSystemInstance)) { + try { + Method method = Model.class.getDeclaredMethod("remove", SoftwareSystemInstance.class); + method.setAccessible(true); + method.invoke(model, softwareSystemInstance); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + void remove(ContainerInstance containerInstance) { + if (!isElementAssociatedWithAnyViews(containerInstance)) { + try { + Method method = Model.class.getDeclaredMethod("remove", ContainerInstance.class); + method.setAccessible(true); + method.invoke(model, containerInstance); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + void remove(DeploymentNode deploymentNode) { + if (deploymentNode.hasChildren()) { + for (DeploymentNode child : deploymentNode.getChildren()) { + remove(child); + } } + + if (!deploymentNode.hasChildren() && !deploymentNode.hasSoftwareSystemInstances() && !deploymentNode.hasContainerInstances()) { + try { + Method method = Model.class.getDeclaredMethod("remove", DeploymentNode.class); + method.setAccessible(true); + method.invoke(model, deploymentNode); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + private boolean isElementAssociatedWithAnyViews(Element element) { + boolean result = false; + + // is the element used in any views + for (View view : viewSet.getViews()) { + if (view instanceof ModelView) { + ModelView modelView = (ModelView)view; + result = result | modelView.isElementInView(element); + } + } + + // is the element the scope of any views? + for (SystemContextView view : viewSet.getSystemContextViews()) { + result = result | view.getSoftwareSystem() == element; + } + + for (ContainerView view : viewSet.getContainerViews()) { + result = result | view.getSoftwareSystem() == element; + } + + for (ComponentView view : viewSet.getComponentViews()) { + result = result | view.getContainer() == element; + } + + for (DynamicView view : viewSet.getDynamicViews()) { + result = result | view.getElement() == element; + } + + for (DeploymentView view : viewSet.getDeploymentViews()) { + result = result | view.getSoftwareSystem() == element; + } + + for (ImageView view : viewSet.getImageViews()) { + result = result | view.getElement() == element; + } + + return result; } } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/Container.java b/structurizr-core/src/com/structurizr/model/Container.java index a4c9fe29a..d6aff51b8 100644 --- a/structurizr-core/src/com/structurizr/model/Container.java +++ b/structurizr-core/src/com/structurizr/model/Container.java @@ -108,6 +108,10 @@ void add(Component component) { } } + void remove(Component component) { + components.remove(component); + } + /** * Gets the set of components within this software system. * @@ -123,6 +127,16 @@ void setComponents(Set components) { } } + /** + * Determines whether this container has any components. + * + * @return true if it has components, false otherwise + */ + @JsonIgnore + public boolean hasComponents() { + return !components.isEmpty(); + } + /** * Gets the component with the specified name. * diff --git a/structurizr-core/src/com/structurizr/model/DeploymentNode.java b/structurizr-core/src/com/structurizr/model/DeploymentNode.java index faa9c613a..abdd8308f 100644 --- a/structurizr-core/src/com/structurizr/model/DeploymentNode.java +++ b/structurizr-core/src/com/structurizr/model/DeploymentNode.java @@ -54,6 +54,14 @@ public SoftwareSystemInstance add(SoftwareSystem softwareSystem, String... deplo return softwareSystemInstance; } + void remove(SoftwareSystemInstance softwareSystemInstance) { + this.softwareSystemInstances.remove(softwareSystemInstance); + } + + void remove(ContainerInstance containerInstance) { + this.containerInstances.remove(containerInstance); + } + /** * Adds a container instance to this deployment node, replicating relationships. * @@ -131,6 +139,10 @@ public DeploymentNode addDeploymentNode(String name, String description, String return deploymentNode; } + void remove(DeploymentNode deploymentNode) { + children.remove(deploymentNode); + } + /** * Gets the DeploymentNode with the specified name. * @@ -318,11 +330,36 @@ void setInfrastructureNodes(Set infrastructureNodes) { } } + /** + * Determines whether this deployment node has any child deployment nodes. + * + * @return true if it has child deployment nodes, false otherwise + */ @JsonIgnore public boolean hasChildren() { return !children.isEmpty(); } + /** + * Determines whether this deployment node has any software system instances. + * + * @return true if it has software system instances, false otherwise + */ + @JsonIgnore + public boolean hasSoftwareSystemInstances() { + return !softwareSystemInstances.isEmpty(); + } + + /** + * Determines whether this deployment node has any container instances. + * + * @return true if it has container instances, false otherwise + */ + @JsonIgnore + public boolean hasContainerInstances() { + return !containerInstances.isEmpty(); + } + /** * Gets the set of software system instances associated with this deployment node. * diff --git a/structurizr-core/src/com/structurizr/model/Element.java b/structurizr-core/src/com/structurizr/model/Element.java index a22b8f895..a80ad2a75 100644 --- a/structurizr-core/src/com/structurizr/model/Element.java +++ b/structurizr-core/src/com/structurizr/model/Element.java @@ -195,10 +195,14 @@ boolean has(Relationship relationship) { return relationships.stream().anyMatch(r -> r.getDestination().equals(relationship.getDestination()) && r.getDescription().equals(relationship.getDescription())); } - void addRelationship(Relationship relationship) { + void add(Relationship relationship) { relationships.add(relationship); } + void remove(Relationship relationship) { + relationships.remove(relationship); + } + /** * Adds a unidirectional "uses" style relationship between this element and the specified custom element. * diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/com/structurizr/model/Model.java index 04479e9a9..73ac3a65d 100644 --- a/structurizr-core/src/com/structurizr/model/Model.java +++ b/structurizr-core/src/com/structurizr/model/Model.java @@ -227,8 +227,6 @@ public CustomElement addCustomElement(@Nonnull String name, @Nullable String met } } - - @Nonnull Container addContainer(SoftwareSystem parent, @Nonnull String name, @Nullable String description, @Nullable String technology) { if (parent.getContainerWithName(name) == null) { @@ -333,7 +331,7 @@ private boolean isChildOf(Element e1, Element e2) { private boolean addRelationship(Relationship relationship) { if (!relationship.getSource().has(relationship)) { relationship.setId(idGenerator.generateId(relationship)); - relationship.getSource().addRelationship(relationship); + relationship.getSource().add(relationship); addRelationshipToInternalStructures(relationship); return true; @@ -364,6 +362,10 @@ private void addRelationshipToInternalStructures(Relationship relationship) { idGenerator.found(relationship.getId()); } + private void removeRelationshipFromInternalStructures(Relationship relationship) { + relationshipsById.remove(relationship.getId()); + } + /** * Gets the set of all elements in this model. * @@ -618,11 +620,21 @@ private void hydrateRelationships(Element element) { /** * Determines whether this model contains the specified element. * - * @param element any element + * @param element an element * @return true, if the element is contained in this model */ public boolean contains(Element element) { - return elementsById.values().contains(element); + return elementsById.containsValue(element); + } + + /** + * Determines whether this model contains the specified relationship. + * + * @param relationship a relationship + * @return true, if the relationship is contained in this model + */ + public boolean contains(Relationship relationship) { + return relationshipsById.containsValue(relationship); } /** @@ -1101,4 +1113,116 @@ void setProperties(Map properties) { } } + /** + * Removes a custom element from the model. + * + * @param element the CustomElement object to remove + */ + void remove(CustomElement element) { + removeElement(element); + customElements.remove(element); + } + + /** + * Removes a person from the model. + * + * @param person the Person object to remove + */ + void remove(Person person) { + removeElement(person); + people.remove(person); + } + + /** + * Removes a software system from the model. + * + * @param softwareSystem the SoftwareSystem object to remove + */ + void remove(SoftwareSystem softwareSystem) { + removeElement(softwareSystem); + softwareSystems.remove(softwareSystem); + } + + /** + * Removes a container from the model. + * + * @param container the Container object to remove + */ + void remove(Container container) { + removeElement(container); + container.getSoftwareSystem().remove(container); + } + + /** + * Removes a component from the model. + * + * @param component the Component object to remove + */ + void remove(Component component) { + removeElement(component); + component.getContainer().remove(component); + } + + /** + * Removes a software system instance from the model. + * + * @param softwareSystemInstance the SoftwareSystemInstance object to remove + */ + void remove(SoftwareSystemInstance softwareSystemInstance) { + removeElement(softwareSystemInstance); + + Set deploymentNodes = getElements().stream().filter(e -> e instanceof DeploymentNode).map(e -> (DeploymentNode)e).collect(Collectors.toSet()); + for (DeploymentNode deploymentNode : deploymentNodes) { + deploymentNode.remove(softwareSystemInstance); + } + } + + /** + * Removes a container instance from the model. + * + * @param containerInstance the ContainerInstance object to remove + */ + void remove(ContainerInstance containerInstance) { + removeElement(containerInstance); + + Set deploymentNodes = getElements().stream().filter(e -> e instanceof DeploymentNode).map(e -> (DeploymentNode)e).collect(Collectors.toSet()); + for (DeploymentNode deploymentNode : deploymentNodes) { + deploymentNode.remove(containerInstance); + } + } + + /** + * Removes a deployment node from the model. + * + * @param deploymentNode the DeploymentNode object to remove + */ + void remove(DeploymentNode deploymentNode) { + removeElement(deploymentNode); + + if (deploymentNode.getParent() == null) { + deploymentNodes.remove(deploymentNode); + } else { + ((DeploymentNode)deploymentNode.getParent()).remove(deploymentNode); + } + } + + private void removeElement(Element element) { + if (element == null) { + throw new IllegalArgumentException("An element must be specified."); + } + + // remove any relationships to/from the element + for (Relationship relationship : getRelationships()) { + if (relationship.getSource() == element) { + removeRelationshipFromInternalStructures(relationship); + relationship.getSource().remove(relationship); + } else if (relationship.getDestination() == element) { + removeRelationshipFromInternalStructures(relationship); + relationship.getDestination().remove(relationship); + } + } + + elementsById.remove(element.getId()); + } + } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/SoftwareSystem.java b/structurizr-core/src/com/structurizr/model/SoftwareSystem.java index b700c5b55..6b0135397 100644 --- a/structurizr-core/src/com/structurizr/model/SoftwareSystem.java +++ b/structurizr-core/src/com/structurizr/model/SoftwareSystem.java @@ -79,6 +79,16 @@ void setContainers(Set containers) { } } + /** + * Determines whether this software system has any containers. + * + * @return true if it has containers, false otherwise + */ + @JsonIgnore + public boolean hasContainers() { + return !containers.isEmpty(); + } + /** * Adds a container with the specified name. * @@ -118,6 +128,10 @@ public Container addContainer(@Nonnull String name, String description, String t return getModel().addContainer(this, name, description, technology); } + void remove(Container container) { + containers.remove(container); + } + /** * Gets the container with the specified name. * diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java index 651057bbe..4a5230338 100644 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java @@ -1,10 +1,11 @@ package com.structurizr; +import com.structurizr.configuration.WorkspaceScope; import com.structurizr.documentation.Decision; import com.structurizr.documentation.Format; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.*; +import com.structurizr.view.SystemContextView; +import com.structurizr.view.SystemLandscapeView; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -73,4 +74,234 @@ void hydrate_DoesNotCrash() { workspace.hydrate(); } + @Test + void remove_WhenACustomElementIsUsedInAView() { + Workspace workspace = new Workspace("Name", "Description"); + + CustomElement element = workspace.getModel().addCustomElement("Name"); + workspace.getViews().createCustomView("key", "Title", "Description").addDefaultElements(); + assertEquals(1, workspace.getModel().getCustomElements().size()); + + workspace.remove(element); + assertEquals(1, workspace.getModel().getCustomElements().size()); + } + + @Test + void remove_WhenAPersonIsUsedInAView() { + Workspace workspace = new Workspace("Name", "Description"); + + Person person = workspace.getModel().addPerson("User"); + workspace.getViews().createSystemLandscapeView("key", "Description").addDefaultElements(); + assertEquals(1, workspace.getModel().getPeople().size()); + + workspace.remove(person); + assertEquals(1, workspace.getModel().getPeople().size()); + } + + @Test + void remove_WhenASoftwareSystemIsUsedInAView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + workspace.getViews().createSystemContextView(softwareSystem, "key", "Description").addDefaultElements(); + assertEquals(1, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(1, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAContainerIsUsedInAView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + workspace.getViews().createContainerView(softwareSystem, "key", "Description").addDefaultElements(); + assertEquals(2, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(2, workspace.getModel().getElements().size()); + + workspace.remove(container); + assertEquals(2, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAComponentIsUsedInAView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + workspace.getViews().createComponentView(container, "key", "Description").addDefaultElements(); + assertEquals(3, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(3, workspace.getModel().getElements().size()); + + workspace.remove(container); + assertEquals(3, workspace.getModel().getElements().size()); + + workspace.remove(component); + assertEquals(3, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenASoftwareSystemInstanceIsUsedInAView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + workspace.getViews().createDeploymentView("key", "Description").addDefaultElements(); + assertEquals(3, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(3, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystemInstance); + assertEquals(3, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAContainerInstanceIsUsedInAView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node"); + ContainerInstance containerInstance = deploymentNode.add(container); + workspace.getViews().createDeploymentView("key", "Description").addDefaultElements(); + assertEquals(4, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(4, workspace.getModel().getElements().size()); + + workspace.remove(container); + assertEquals(4, workspace.getModel().getElements().size()); + + workspace.remove(containerInstance); + assertEquals(4, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAnElementIsTheScopeOfAContainerView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + workspace.getViews().createContainerView(softwareSystem, "key", "Description"); + assertEquals(1, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(1, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAnElementIsTheScopeOfAComponentView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + workspace.getViews().createComponentView(container, "key", "Description"); + assertEquals(2, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(2, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAnElementIsTheScopeOfADynamicView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + workspace.getViews().createDynamicView(softwareSystem, "key", "Description"); + assertEquals(1, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(1, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAnElementIsTheScopeOfADeploymentView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + workspace.getViews().createDeploymentView(softwareSystem, "key", "Description"); + assertEquals(1, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(1, workspace.getModel().getElements().size()); + } + + @Test + void remove_WhenAnElementIsTheScopeOfAnImageView() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + workspace.getViews().createImageView(softwareSystem, "key"); + assertEquals(1, workspace.getModel().getElements().size()); + + workspace.remove(softwareSystem); + assertEquals(1, workspace.getModel().getElements().size()); + } + + @Test + void trim_WhenAllElementsAreUnused() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy()); + + CustomElement element = workspace.getModel().addCustomElement("Custom Element"); + Person user = workspace.getModel().addPerson("User"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container webapp = softwareSystem.addContainer("Web Application"); + Container database = softwareSystem.addContainer("Database"); + Component component = webapp.addComponent("Component"); + user.uses(component, "uses"); + webapp.uses(database, "uses"); + + DeploymentNode live = workspace.getModel().addDeploymentNode("Live"); + DeploymentNode server1 = live.addDeploymentNode("Server 1"); + DeploymentNode server2 = live.addDeploymentNode("Server 2"); + ContainerInstance webappInstance = server1.add(webapp); + ContainerInstance databaseInstance = server2.add(database); + + DeploymentNode dev = workspace.getModel().addDeploymentNode("Dev"); + SoftwareSystemInstance softwareSystemInstance = dev.add(softwareSystem); + + assertEquals(13, workspace.getModel().getElements().size()); + assertEquals(5, workspace.getModel().getRelationships().size()); + + workspace.trim(); + assertEquals(0, workspace.getModel().getElements().size()); + } + + @Test + void trim_WhenSomeElementsAreUnused() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + SoftwareSystem c = workspace.getModel().addSoftwareSystem("C"); + SoftwareSystem d = workspace.getModel().addSoftwareSystem("D"); + + // a -> b -> c -> d + Relationship ab = a.uses(b, "uses"); + Relationship bc = b.uses(c, "uses"); + Relationship cd = c.uses(d, "uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.add(b); + view.add(c); + + assertEquals(4, workspace.getModel().getElements().size()); + assertEquals(3, workspace.getModel().getRelationships().size()); + + workspace.trim(); + assertEquals(2, workspace.getModel().getElements().size()); + assertEquals(1, workspace.getModel().getRelationships().size()); + assertTrue(workspace.getModel().contains(b)); + assertTrue(workspace.getModel().contains(c)); + assertTrue(workspace.getModel().contains(bc)); + } + } \ No newline at end of file From 3e63fb8c5704bc1060ced934f83952a4e3100f81 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 9 Jan 2024 14:53:20 +0000 Subject: [PATCH 138/418] Combines source from dsl/export/import/graphviz repos. --- README.md | 14 +- build.gradle | 15 +- changelog.md | 6 + docs/changelog.md | 402 ---- settings.gradle | 6 +- structurizr-client/README.md | 5 + structurizr-client/build.gradle | 4 +- .../api/BackwardsCompatibilityTests.java | 2 +- .../StructurizrClientIntegrationTests.java | 0 .../api/WorkspaceRulesValidationTests.java | 2 +- .../structurizr-31-workspace.json | 0 .../structurizr-36141-workspace.json | 0 .../structurizr-39459-workspace.json | 0 .../ChildDeploymentNodeNamesAreNotUnique.json | 0 .../ComponentNamesAreNotUnique.json | 0 ...ithComponentViewIsMissingFromTheModel.json | 0 .../ContainerNamesAreNotUnique.json | 0 ...dWithDynamicViewIsMissingFromTheModel.json | 0 .../ElementIdsAreNotUnique.json | 0 ...ReferencedByViewIsMissingFromTheModel.json | 0 ...pleAndSoftwareSystemNamesAreNotUnique.json | 0 .../RelationshipDescriptionsAreNotUnique.json | 0 .../RelationshipIdsAreNotUnique.json | 0 ...ReferencedByViewIsMissingFromTheModel.json | 0 ...ithContainerViewIsMissingFromTheModel.json | 0 ...thDeploymentViewIsMissingFromTheModel.json | 0 ...ystemContextViewIsMissingFromTheModel.json | 0 ...pLevelDeploymentNodeNamesAreNotUnique.json | 0 ...ueButTheyExistInDifferentEnvironments.json | 0 ...FilteredViewIsMissingFromTheWorkspace.json | 0 .../ViewKeysAreNotUnique.json | 0 .../structurizr/api/AbstractApiClient.java | 0 .../com/structurizr/api/AdminApiClient.java | 0 .../com/structurizr/api/ApiResponse.java | 0 .../HashBasedMessageAuthenticationCode.java | 0 .../api/HmacAuthorizationHeader.java | 0 .../com/structurizr/api/HmacContent.java | 0 .../com/structurizr/api/HttpHeaders.java | 0 .../java}/com/structurizr/api/Md5Digest.java | 0 .../structurizr/api/StructurizrClient.java | 0 .../api/StructurizrClientException.java | 0 .../structurizr/api/WorkspaceApiClient.java | 0 .../structurizr/api/WorkspaceMetadata.java | 0 .../java}/com/structurizr/api/Workspaces.java | 0 .../encryption/AesEncryptionStrategy.java | 0 .../encryption/EncryptedWorkspace.java | 0 .../encryption/EncryptionLocation.java | 0 .../encryption/EncryptionStrategy.java | 0 .../com/structurizr/io/WorkspaceReader.java | 0 .../io/WorkspaceReaderException.java | 0 .../com/structurizr/io/WorkspaceWriter.java | 0 .../io/WorkspaceWriterException.java | 0 .../io/json/AbstractJsonReader.java | 0 .../io/json/AbstractJsonWriter.java | 0 .../io/json/EncryptedJsonReader.java | 0 .../io/json/EncryptedJsonWriter.java | 0 .../com/structurizr/io/json/JsonReader.java | 0 .../com/structurizr/io/json/JsonWriter.java | 0 .../com/structurizr/util/WorkspaceUtils.java | 0 .../com/structurizr/view/ThemeUtils.java | 0 .../com/structurizr/api/ApiResponseTests.java | 0 ...shBasedMessageAuthenticationCodeTests.java | 0 .../api/HmacAuthorizationHeaderTests.java | 0 .../com/structurizr/api/HmacContentTests.java | 0 .../com/structurizr/api/Md5DigestTests.java | 0 .../api/StructurizrClientTests.java | 0 .../AesEncryptionStrategyTests.java | 0 .../encryption/EncryptedWorkspaceTests.java | 0 .../encryption/MockEncryptionStrategy.java | 0 .../io/json/EncryptedJsonTests.java | 0 .../io/json/EncryptedJsonWriterTests.java | 0 .../com/structurizr/io/json/JsonTests.java | 0 .../structurizr/io/json/JsonWriterTests.java | 0 .../structurizr/util/WorkspaceUtilsTests.java | 0 .../com/structurizr/view/ThemeUtilsTests.java | 0 structurizr-core/README.md | 6 + .../com/structurizr/AbstractWorkspace.java | 0 .../java}/com/structurizr/PropertyHolder.java | 0 .../java}/com/structurizr/Workspace.java | 0 .../WorkspaceValidationException.java | 0 .../com/structurizr/configuration/Role.java | 0 .../com/structurizr/configuration/User.java | 0 .../structurizr/configuration/Visibility.java | 0 .../configuration/WorkspaceConfiguration.java | 0 .../configuration/WorkspaceScope.java | 0 .../structurizr/documentation/Decision.java | 0 .../documentation/Documentable.java | 0 .../documentation/Documentation.java | 0 .../documentation/DocumentationContent.java | 0 .../com/structurizr/documentation/Format.java | 0 .../com/structurizr/documentation/Image.java | 0 .../structurizr/documentation/Section.java | 0 .../AbstractImpliedRelationshipsStrategy.java | 0 .../model/CanonicalNameGenerator.java | 0 .../com/structurizr/model/Component.java | 0 .../com/structurizr/model/Constants.java | 0 .../com/structurizr/model/Container.java | 0 .../structurizr/model/ContainerInstance.java | 0 ...psUnlessAnyRelationshipExistsStrategy.java | 0 ...sUnlessSameRelationshipExistsStrategy.java | 0 .../com/structurizr/model/CustomElement.java | 0 .../DefaultImpliedRelationshipsStrategy.java | 0 .../structurizr/model/DeploymentElement.java | 0 .../com/structurizr/model/DeploymentNode.java | 10 + .../java}/com/structurizr/model/Element.java | 0 .../com/structurizr/model/Enterprise.java | 0 .../structurizr/model/GroupableElement.java | 0 .../structurizr/model/HttpHealthCheck.java | 0 .../com/structurizr/model/IdGenerator.java | 0 .../model/ImpliedRelationshipsStrategy.java | 0 .../structurizr/model/InfrastructureNode.java | 0 .../structurizr/model/InteractionStyle.java | 0 .../java}/com/structurizr/model/Location.java | 0 .../java}/com/structurizr/model/Model.java | 0 .../com/structurizr/model/ModelItem.java | 0 .../java}/com/structurizr/model/Person.java | 0 .../com/structurizr/model/Perspective.java | 0 .../com/structurizr/model/Relationship.java | 0 .../SequentialIntegerIdGeneratorStrategy.java | 0 .../com/structurizr/model/SoftwareSystem.java | 0 .../model/SoftwareSystemInstance.java | 0 .../model/StaticStructureElement.java | 0 .../model/StaticStructureElementInstance.java | 0 .../java}/com/structurizr/model/Tags.java | 0 .../com/structurizr/util/ImageUtils.java | 0 .../java}/com/structurizr/util/MapUtils.java | 0 .../com/structurizr/util/StringUtils.java | 0 .../java}/com/structurizr/util/TagUtils.java | 0 .../java}/com/structurizr/util/Url.java | 0 .../LandscapeWorkspaceScopeValidator.java | 0 ...SoftwareSystemWorkspaceScopeValidator.java | 0 .../UndefinedWorkspaceScopeValidator.java | 0 .../WorkspaceScopeValidationException.java | 0 .../validation/WorkspaceScopeValidator.java | 0 .../WorkspaceScopeValidatorFactory.java | 0 .../com/structurizr/view/AbstractStyle.java | 0 .../com/structurizr/view/AnimatedView.java | 0 .../java}/com/structurizr/view/Animation.java | 0 .../com/structurizr/view/AutomaticLayout.java | 0 .../java}/com/structurizr/view/Border.java | 0 .../java}/com/structurizr/view/Branding.java | 0 .../java}/com/structurizr/view/Color.java | 0 .../com/structurizr/view/ComponentView.java | 0 .../com/structurizr/view/Configuration.java | 0 .../com/structurizr/view/ContainerView.java | 0 .../com/structurizr/view/CustomView.java | 0 .../view/DefaultLayoutMergeStrategy.java | 0 .../com/structurizr/view/DeploymentView.java | 0 .../com/structurizr/view/Dimensions.java | 0 .../com/structurizr/view/DynamicView.java | 0 .../ElementNotPermittedInViewException.java | 0 .../com/structurizr/view/ElementStyle.java | 0 .../com/structurizr/view/ElementView.java | 0 .../com/structurizr/view/FilterMode.java | 0 .../com/structurizr/view/FilteredView.java | 0 .../java}/com/structurizr/view/Font.java | 0 .../java}/com/structurizr/view/ImageView.java | 0 .../structurizr/view/LayoutMergeStrategy.java | 0 .../java}/com/structurizr/view/LineStyle.java | 0 .../com/structurizr/view/MetadataSymbols.java | 0 .../java}/com/structurizr/view/ModelView.java | 0 .../java}/com/structurizr/view/PaperSize.java | 0 .../view/ParallelSequenceCounter.java | 0 .../structurizr/view/RelationshipStyle.java | 0 .../structurizr/view/RelationshipView.java | 0 .../java}/com/structurizr/view/Routing.java | 0 .../com/structurizr/view/SequenceCounter.java | 0 .../com/structurizr/view/SequenceNumber.java | 0 .../java}/com/structurizr/view/Shape.java | 0 .../com/structurizr/view/StaticView.java | 0 .../java}/com/structurizr/view/Styles.java | 0 .../structurizr/view/SystemContextView.java | 0 .../structurizr/view/SystemLandscapeView.java | 0 .../com/structurizr/view/Terminology.java | 0 .../java}/com/structurizr/view/Theme.java | 0 .../java}/com/structurizr/view/Vertex.java | 0 .../java}/com/structurizr/view/View.java | 0 .../java}/com/structurizr/view/ViewSet.java | 0 .../com/structurizr/view/ViewSortOrder.java | 0 .../AbstractWorkspaceTestBase.java | 0 .../java}/com/structurizr/WorkspaceTests.java | 0 .../structurizr/configuration/UserTests.java | 0 .../WorkspaceConfigurationTests.java | 0 .../documentation/DecisionTests.java | 0 .../documentation/DocumentationTests.java | 0 .../documentation/SectionTests.java | 0 .../com/structurizr/model/ComponentTests.java | 0 .../model/ContainerInstanceTests.java | 0 .../com/structurizr/model/ContainerTests.java | 0 ...essAnyRelationshipExistsStrategyTests.java | 0 ...ssSameRelationshipExistsStrategyTests.java | 0 .../structurizr/model/CustomElementTests.java | 0 ...aultImpliedRelationshipsStrategyTests.java | 0 .../model/DeploymentNodeTests.java | 0 .../com/structurizr/model/ElementTests.java | 0 .../model/GroupableElementTests.java | 0 .../model/HttpHealthCheckTests.java | 0 .../model/InfrastructureNodeTests.java | 0 .../com/structurizr/model/ModelItemTests.java | 0 .../com/structurizr/model/ModelTests.java | 0 .../com/structurizr/model/PersonTests.java | 0 .../structurizr/model/RelationshipTests.java | 0 .../model/SoftwareSystemInstanceTests.java | 0 .../model/SoftwareSystemTests.java | 0 .../com/structurizr/util/ImageUtilsTests.java | 8 +- .../structurizr/util/StringUtilsTests.java | 0 .../java}/com/structurizr/util/UrlTests.java | 0 ...LandscapeWorkspaceScopeValidatorTests.java | 0 ...areSystemWorkspaceScopeValidatorTests.java | 0 .../WorkspaceScopeValidatorFactoryTests.java | 0 .../view/AutomaticLayoutTests.java | 0 .../com/structurizr/view/BrandingTests.java | 0 .../com/structurizr/view/ColorTests.java | 0 .../structurizr/view/ComponentViewTests.java | 0 .../structurizr/view/ConfigurationTests.java | 0 .../structurizr/view/ContainerViewTests.java | 0 .../view/DefaultLayoutMergeStrategyTests.java | 0 .../structurizr/view/DeploymentViewTests.java | 0 .../com/structurizr/view/DimensionsTests.java | 0 .../structurizr/view/DynamicViewTests.java | 0 .../structurizr/view/ElementStyleTests.java | 0 .../structurizr/view/ElementViewTests.java | 0 .../structurizr/view/FilteredViewTests.java | 0 .../java}/com/structurizr/view/FontTests.java | 0 .../com/structurizr/view/ImageViewTests.java | 0 .../com/structurizr/view/PaperSizeTests.java | 0 .../view/RelationshipStyleTests.java | 0 .../view/SequenceCounterTests.java | 0 .../structurizr/view/SequenceNumberTests.java | 0 .../com/structurizr/view/StaticViewTests.java | 0 .../com/structurizr/view/StylesTests.java | 0 .../view/SystemContextViewTests.java | 0 .../view/SystemLandscapeViewTests.java | 0 .../structurizr/view/TerminologyTests.java | 0 .../com/structurizr/view/VertexTests.java | 0 .../com/structurizr/view/ViewSetTests.java | 0 .../java}/com/structurizr/view/ViewTests.java | 0 .../test/resources}/structurizr-logo.png | Bin structurizr-dsl/README.md | 11 + structurizr-dsl/build.gradle | 14 + .../dsl/AbstractExpressionParser.java | 344 +++ .../com/structurizr/dsl/AbstractParser.java | 42 + .../dsl/AbstractRelationshipParser.java | 37 + .../structurizr/dsl/AbstractViewParser.java | 4 + .../java/com/structurizr/dsl/AdrsParser.java | 80 + .../com/structurizr/dsl/AutoLayoutParser.java | 70 + .../structurizr/dsl/BrandingDslContext.java | 25 + .../com/structurizr/dsl/BrandingParser.java | 71 + .../structurizr/dsl/CommentDslContext.java | 10 + .../structurizr/dsl/ComponentDslContext.java | 42 + .../com/structurizr/dsl/ComponentParser.java | 77 + .../dsl/ComponentViewDslContext.java | 11 + .../structurizr/dsl/ComponentViewParser.java | 62 + .../dsl/ConfigurationDslContext.java | 15 + .../structurizr/dsl/ConfigurationParser.java | 61 + .../java/com/structurizr/dsl/Constant.java | 21 + .../com/structurizr/dsl/ConstantParser.java | 33 + .../structurizr/dsl/ContainerDslContext.java | 52 + .../dsl/ContainerInstanceDslContext.java | 42 + .../dsl/ContainerInstanceParser.java | 64 + .../com/structurizr/dsl/ContainerParser.java | 77 + .../dsl/ContainerViewDslContext.java | 11 + .../structurizr/dsl/ContainerViewParser.java | 62 + .../dsl/CustomElementDslContext.java | 41 + .../structurizr/dsl/CustomElementParser.java | 53 + .../dsl/CustomViewAnimationDslContext.java | 26 + .../dsl/CustomViewAnimationStepParser.java | 51 + .../dsl/CustomViewContentParser.java | 97 + .../structurizr/dsl/CustomViewDslContext.java | 29 + .../dsl/CustomViewExpressionParser.java | 45 + .../com/structurizr/dsl/CustomViewParser.java | 48 + .../structurizr/dsl/DefaultViewParser.java | 12 + .../dsl/DeploymentEnvironment.java | 49 + .../dsl/DeploymentEnvironmentDslContext.java | 31 + .../dsl/DeploymentEnvironmentParser.java | 21 + .../com/structurizr/dsl/DeploymentGroup.java | 35 + .../dsl/DeploymentGroupParser.java | 21 + .../dsl/DeploymentNodeDslContext.java | 54 + .../structurizr/dsl/DeploymentNodeParser.java | 140 ++ .../DeploymentViewAnimationDslContext.java | 26 + .../DeploymentViewAnimationStepParser.java | 60 + .../dsl/DeploymentViewContentParser.java | 154 ++ .../dsl/DeploymentViewDslContext.java | 29 + .../dsl/DeploymentViewExpressionParser.java | 70 + .../structurizr/dsl/DeploymentViewParser.java | 85 + .../java/com/structurizr/dsl/DocsParser.java | 80 + .../java/com/structurizr/dsl/DslContext.java | 126 ++ .../java/com/structurizr/dsl/DslLine.java | 24 + .../com/structurizr/dsl/DslParserContext.java | 38 + .../java/com/structurizr/dsl/DslUtils.java | 48 + .../dsl/DynamicViewContentParser.java | 100 + .../dsl/DynamicViewDslContext.java | 27 + ...DynamicViewParallelSequenceDslContext.java | 21 + .../structurizr/dsl/DynamicViewParser.java | 89 + .../dsl/DynamicViewRelationshipContext.java | 26 + .../dsl/DynamicViewRelationshipParser.java | 21 + .../com/structurizr/dsl/ElementGroup.java | 74 + .../dsl/ElementStyleDslContext.java | 46 + .../structurizr/dsl/ElementStyleParser.java | 324 +++ .../structurizr/dsl/EnterpriseDslContext.java | 23 + .../com/structurizr/dsl/EnterpriseParser.java | 30 + .../dsl/ExplicitRelationshipParser.java | 72 + .../dsl/ExternalScriptDslContext.java | 41 + .../java/com/structurizr/dsl/FileUtils.java | 46 + .../dsl/FilteredViewDslContext.java | 25 + .../structurizr/dsl/FilteredViewParser.java | 88 + .../java/com/structurizr/dsl/GroupParser.java | 35 + .../structurizr/dsl/GroupableDslContext.java | 23 + .../dsl/GroupableElementDslContext.java | 17 + .../structurizr/dsl/HealthCheckParser.java | 61 + .../java/com/structurizr/dsl/IconUtils.java | 38 + .../com/structurizr/dsl/IdentifierScope.java | 8 + .../dsl/IdentifierScopeParser.java | 30 + .../structurizr/dsl/IdentifiersRegister.java | 222 ++ .../dsl/ImageViewContentParser.java | 163 ++ .../structurizr/dsl/ImageViewDslContext.java | 50 + .../com/structurizr/dsl/ImageViewParser.java | 56 + .../dsl/ImplicitRelationshipParser.java | 52 + .../dsl/ImpliedRelationshipsParser.java | 34 + .../com/structurizr/dsl/IncludeParser.java | 73 + .../structurizr/dsl/IncludedDslContext.java | 33 + .../com/structurizr/dsl/IncludedFile.java | 24 + .../dsl/InfrastructureNodeDslContext.java | 36 + .../dsl/InfrastructureNodeParser.java | 71 + .../dsl/InlineScriptDslContext.java | 55 + .../com/structurizr/dsl/ModelDslContext.java | 37 + .../structurizr/dsl/ModelItemDslContext.java | 17 + .../com/structurizr/dsl/ModelItemParser.java | 79 + .../dsl/ModelItemPerspectivesDslContext.java | 22 + .../dsl/ModelViewContentParser.java | 19 + .../structurizr/dsl/ModelViewDslContext.java | 15 + .../com/structurizr/dsl/PersonDslContext.java | 41 + .../com/structurizr/dsl/PersonParser.java | 59 + .../com/structurizr/dsl/PluginDslContext.java | 42 + .../com/structurizr/dsl/PluginParser.java | 43 + .../structurizr/dsl/PropertiesDslContext.java | 22 + .../com/structurizr/dsl/PropertyParser.java | 25 + .../java/com/structurizr/dsl/RefParser.java | 52 + .../dsl/RelationshipDslContext.java | 33 + .../dsl/RelationshipStyleDslContext.java | 33 + .../dsl/RelationshipStyleParser.java | 239 ++ .../com/structurizr/dsl/ScriptDslContext.java | 77 + .../com/structurizr/dsl/ScriptParser.java | 66 + .../dsl/SoftwareSystemDslContext.java | 51 + .../dsl/SoftwareSystemInstanceDslContext.java | 42 + .../dsl/SoftwareSystemInstanceParser.java | 65 + .../structurizr/dsl/SoftwareSystemParser.java | 59 + ...ticStructureElementInstanceDslContext.java | 9 + .../dsl/StaticViewAnimationDslContext.java | 26 + .../dsl/StaticViewAnimationStepParser.java | 50 + .../dsl/StaticViewContentParser.java | 117 + .../structurizr/dsl/StaticViewDslContext.java | 29 + .../dsl/StaticViewExpressionParser.java | 64 + .../dsl/StructurizrDslExpressions.java | 22 + .../structurizr/dsl/StructurizrDslParser.java | 1033 +++++++++ .../dsl/StructurizrDslParserException.java | 49 + .../structurizr/dsl/StructurizrDslPlugin.java | 15 + .../dsl/StructurizrDslPluginContext.java | 88 + .../dsl/StructurizrDslScriptContext.java | 76 + .../structurizr/dsl/StructurizrDslTokens.java | 109 + .../com/structurizr/dsl/StylesDslContext.java | 13 + .../dsl/SystemContextViewDslContext.java | 11 + .../dsl/SystemContextViewParser.java | 62 + .../dsl/SystemLandscapeViewDslContext.java | 11 + .../dsl/SystemLandscapeViewParser.java | 43 + .../dsl/TerminologyDslContext.java | 19 + .../structurizr/dsl/TerminologyParser.java | 79 + .../java/com/structurizr/dsl/ThemeParser.java | 41 + .../java/com/structurizr/dsl/Tokenizer.java | 58 + .../main/java/com/structurizr/dsl/Tokens.java | 41 + .../com/structurizr/dsl/UserRoleParser.java | 39 + .../com/structurizr/dsl/UsersDslContext.java | 10 + .../com/structurizr/dsl/ViewDslContext.java | 17 + .../java/com/structurizr/dsl/ViewParser.java | 51 + .../com/structurizr/dsl/ViewsDslContext.java | 25 + .../structurizr/dsl/WorkspaceDslContext.java | 21 + .../com/structurizr/dsl/WorkspaceParser.java | 138 ++ .../dsl/AbstractExpressionParserTests.java | 506 +++++ .../com/structurizr/dsl/AbstractTests.java | 30 + .../com/structurizr/dsl/AdrsParserTests.java | 22 + .../dsl/AutoLayoutParserTests.java | 111 + .../structurizr/dsl/BrandingParserTests.java | 140 ++ .../structurizr/dsl/ComponentParserTests.java | 127 ++ .../dsl/ComponentViewParserTests.java | 111 + .../dsl/ConfigurationParserTests.java | 86 + .../structurizr/dsl/ConstantParserTests.java | 59 + .../dsl/ContainerInstanceParserTests.java | 147 ++ .../structurizr/dsl/ContainerParserTests.java | 122 + .../dsl/ContainerViewParserTests.java | 111 + .../com/structurizr/dsl/CookbookTests.java | 33 + .../dsl/CustomElementParserTests.java | 79 + .../CustomViewAnimationStepParserTests.java | 32 + .../dsl/CustomViewContentParserTests.java | 357 +++ .../dsl/CustomViewParserTests.java | 75 + .../dsl/DefaultViewParserTests.java | 24 + .../dsl/DeploymentEnvironmentParserTests.java | 38 + .../dsl/DeploymentGroupParserTests.java | 38 + .../dsl/DeploymentNodeParserTests.java | 222 ++ ...eploymentViewAnimationStepParserTests.java | 32 + .../dsl/DeploymentViewContentParserTests.java | 576 +++++ .../DeploymentViewExpressionParserTests.java | 220 ++ .../dsl/DeploymentViewParserTests.java | 209 ++ .../com/structurizr/dsl/DocsParserTests.java | 22 + .../java/com/structurizr/dsl/DslTests.java | 1049 +++++++++ .../dsl/DynamicViewContentParserTests.java | 187 ++ .../dsl/DynamicViewParserTests.java | 197 ++ .../DynamicViewRelationshipParserTests.java | 52 + .../dsl/ElementStyleParserTests.java | 546 +++++ .../dsl/EnterpriseParserTests.java | 63 + .../dsl/ExplicitRelationshipParserTests.java | 232 ++ .../dsl/ExternalScriptDslContextTests.java | 21 + .../dsl/FilteredViewParserTests.java | 139 ++ .../com/structurizr/dsl/GroupParserTests.java | 72 + .../dsl/HealthCheckParserTests.java | 127 ++ .../dsl/IdentifierRegisterTests.java | 63 + .../dsl/IdentifierScopeParserTests.java | 46 + .../structurizr/dsl/ImageViewParserTests.java | 62 + .../dsl/ImplicitRelationshipParserTests.java | 179 ++ .../dsl/ImpliedRelationshipsParserTests.java | 46 + .../structurizr/dsl/IncludeParserTests.java | 32 + .../dsl/InfrastructureNodeParserTests.java | 134 ++ .../dsl/InlineScriptDslContextTests.java | 23 + .../structurizr/dsl/ModelDslContextTests.java | 77 + .../structurizr/dsl/ModelItemParserTests.java | 165 ++ .../structurizr/dsl/PersonParserTests.java | 83 + .../dsl/PluginDslContextTests.java | 23 + .../structurizr/dsl/PluginParserTests.java | 38 + .../structurizr/dsl/PropertyParserTests.java | 31 + .../com/structurizr/dsl/RefParserTests.java | 79 + .../dsl/RelationshipStyleParserTests.java | 392 ++++ .../structurizr/dsl/ScriptParserTests.java | 34 + .../SoftwareSystemInstanceParserTests.java | 143 ++ .../dsl/SoftwareSystemParserTests.java | 83 + .../StaticViewAnimationStepParserTests.java | 32 + .../dsl/StaticViewContentParserTests.java | 790 +++++++ .../dsl/StaticViewExpressionParserTests.java | 199 ++ .../dsl/SystemContextViewParserTests.java | 109 + .../dsl/SystemLandscapeViewParserTests.java | 60 + .../dsl/TerminologyParserTests.java | 148 ++ .../com/structurizr/dsl/ThemeParserTests.java | 84 + .../com/structurizr/dsl/TokenizerTests.java | 71 + .../java/com/structurizr/dsl/TokensTests.java | 23 + .../structurizr/dsl/UserRoleParserTests.java | 80 + .../com/structurizr/dsl/ViewParserTests.java | 156 ++ .../structurizr/dsl/WorkspaceParserTests.java | 105 + .../example/ExampleDecisionImporter.java | 6 + .../example/ExampleDocumentationImporter.java | 6 + .../0001-record-architecture-decisions.md | 19 + .../adrs/0002-implement-as-shell-scripts.md | 28 + .../0003-single-command-with-subcommands.md | 45 + .../dsl/adrs/adrs/0004-markdown-format.md | 40 + .../dsl/adrs/adrs/0005-help-comments.md | 42 + ...n-in-other-version-control-repositories.md | 41 + ...-config-executable-to-get-configuration.md | 31 + .../0008-use-iso-8601-format-for-dates.md | 43 + .../dsl/adrs/adrs/0009-help-scripts.md | 28 + .../dsl/adrs/adrs/0010-asciidoc-format.md | 21 + .../src/test/resources/dsl/adrs/workspace.dsl | 19 + .../dsl/amazon-web-services-local.dsl | 65 + .../resources/dsl/amazon-web-services.dsl | 84 + .../src/test/resources/dsl/big-bank-plc.dsl | 249 ++ .../big-bank-plc/internet-banking-system.dsl | 189 ++ .../0001-record-architecture-decisions.md | 19 + .../model/internet-banking-system/details.dsl | 15 + .../docs/01-context.md | 11 + .../docs/02-containers.md | 21 + .../docs/03-development-environment.adoc | 5 + .../docs/04-deployment.adoc | 5 + .../model/internet-banking-system/summary.dsl | 1 + .../model/people-and-software-systems.dsl | 25 + .../dsl/big-bank-plc/system-landscape.dsl | 23 + .../dsl/big-bank-plc/views/styles-people.dsl | 11 + .../dsl/deployment-environment-empty.dsl | 9 + .../test/resources/dsl/deployment-groups.dsl | 48 + .../resources/dsl/docs/docs/01-context.md | 5 + .../src/test/resources/dsl/docs/workspace.dsl | 31 + .../dsl/dynamic-view-with-custom-elements.dsl | 19 + ...namic-view-with-explicit-relationships.dsl | 37 + .../src/test/resources/dsl/dynamic.dsl | 24 + .../dsl/exclude-implied-relationship.dsl | 22 + .../resources/dsl/exclude-relationships.dsl | 20 + .../src/test/resources/dsl/extend/1.dsl | 7 + .../src/test/resources/dsl/extend/2.dsl | 7 + .../src/test/resources/dsl/extend/3.dsl | 7 + .../src/test/resources/dsl/extend/4.dsl | 9 + .../extend/extend-workspace-from-dsl-file.dsl | 12 + .../extend/extend-workspace-from-dsl-url.dsl | 12 + .../extend-workspace-from-json-file.dsl | 18 + .../extend/extend-workspace-from-json-url.dsl | 18 + .../test/resources/dsl/extend/workspace.dsl | 18 + .../test/resources/dsl/extend/workspace.json | 73 + .../src/test/resources/dsl/filteredviews.dsl | 41 + .../resources/dsl/financial-risk-system.dsl | 67 + .../resources/dsl/getting-started-short.dsl | 17 + .../test/resources/dsl/getting-started.dsl | 19 + .../src/test/resources/dsl/group-url.dsl | 11 + .../resources/dsl/group-without-brace.dsl | 7 + .../src/test/resources/dsl/groups-nested.dsl | 47 + .../src/test/resources/dsl/groups.dsl | 51 + ...cal-identifiers-and-deployment-nodes-1.dsl | 40 + ...cal-identifiers-and-deployment-nodes-2.dsl | 18 + ...cal-identifiers-and-deployment-nodes-3.dsl | 22 + ...erarchical-identifiers-when-unassigned.dsl | 19 + .../dsl/hierarchical-identifiers.dsl | 16 + .../src/test/resources/dsl/identifiers.dsl | 14 + .../resources/dsl/image-views/diagram.dot | 1 + .../resources/dsl/image-views/diagram.mmd | 2 + .../resources/dsl/image-views/diagram.puml | 3 + .../test/resources/dsl/image-views/logo.png | Bin 0 -> 9262 bytes .../dsl/image-views/workspace-via-file.dsl | 27 + .../dsl/image-views/workspace-via-url.dsl | 30 + .../test/resources/dsl/include-directory.dsl | 14 + .../src/test/resources/dsl/include-file.dsl | 7 + .../dsl/include-implied-relationship.dsl | 23 + .../src/test/resources/dsl/include-url.dsl | 7 + .../resources/dsl/include/docs/section.md | 3 + .../src/test/resources/dsl/include/model.dsl | 3 + .../include/model/software-system/model.dsl | 3 + .../src/test/resources/dsl/iso-8859.dsl | 5 + .../src/test/resources/dsl/logo.png | Bin 0 -> 9262 bytes .../resources/dsl/multi-line-with-error.dsl | 12 + .../src/test/resources/dsl/multi-line.dsl | 10 + .../resources/dsl/multiple-model-tokens.dsl | 11 + .../resources/dsl/multiple-view-tokens.dsl | 19 + .../dsl/multiple-workspace-tokens.dsl | 15 + .../src/test/resources/dsl/parallel1.dsl | 33 + .../src/test/resources/dsl/parallel2.dsl | 34 + .../resources/dsl/plugin-with-parameters.dsl | 7 + .../dsl/plugin-without-parameters.dsl | 5 + .../plugins/structurizr-dsl-plugin-1.0.0.jar | Bin 0 -> 1214 bytes .../src/test/resources/dsl/ref.dsl | 45 + .../dsl/relationship-already-exists.dsl | 13 + .../dsl/script-external-with-parameters.dsl | 7 + .../test/resources/dsl/script-external.dsl | 7 + .../resources/dsl/script-in-dynamic-view.dsl | 16 + .../src/test/resources/dsl/script-inline.dsl | 42 + .../src/test/resources/dsl/shapes.dsl | 89 + .../resources/dsl/test-with-parameters.groovy | 4 + .../src/test/resources/dsl/test.dsl | 345 +++ .../src/test/resources/dsl/test.groovy | 4 + .../src/test/resources/dsl/test.js | 2 + .../src/test/resources/dsl/test.kts | 2 + .../src/test/resources/dsl/test.rb | 2 + .../src/test/resources/dsl/this.dsl | 71 + .../dsl/unexpected-tokens-after-workspace.dsl | 4 + .../unexpected-tokens-before-workspace.dsl | 4 + .../dsl/unexpected-tokens-in-workspace.dsl | 5 + .../src/test/resources/dsl/utf8.dsl | 7 + .../test/resources/dsl/views-without-keys.dsl | 18 + .../resources/dsl/workspace-properties.dsl | 7 + .../test/resources/dsl/workspace-with-bom.dsl | 29 + structurizr-export/README.md | 9 + structurizr-export/build.gradle | 10 + .../export/AbstractDiagramExporter.java | 748 ++++++ .../structurizr/export/AbstractExporter.java | 119 + .../export/AbstractWorkspaceExporter.java | 4 + .../java/com/structurizr/export/Diagram.java | 51 + .../structurizr/export/DiagramExporter.java | 17 + .../java/com/structurizr/export/Exporter.java | 4 + .../com/structurizr/export/IndentType.java | 8 + .../structurizr/export/IndentingWriter.java | 62 + .../java/com/structurizr/export/Legend.java | 15 + .../structurizr/export/WorkspaceExport.java | 17 + .../structurizr/export/WorkspaceExporter.java | 15 + .../structurizr/export/dot/DOTDiagram.java | 17 + .../structurizr/export/dot/DOTExporter.java | 434 ++++ .../java/com/structurizr/export/dot/README.md | 6 + .../structurizr/export/dot/RankDirection.java | 23 + .../export/ilograph/IlographExporter.java | 391 ++++ .../ilograph/IlographWorkspaceExport.java | 16 + .../com/structurizr/export/ilograph/README.md | 7 + .../export/mermaid/MermaidDiagram.java | 17 + .../mermaid/MermaidDiagramExporter.java | 412 ++++ .../export/mermaid/MermaidEncoder.java | 18 + .../com/structurizr/export/mermaid/README.md | 6 + .../plantuml/AbstractPlantUMLExporter.java | 251 +++ .../export/plantuml/C4PlantUMLExporter.java | 682 ++++++ .../export/plantuml/PlantUMLDiagram.java | 17 + .../export/plantuml/PlantUMLEncoder.java | 73 + .../com/structurizr/export/plantuml/README.md | 7 + .../plantuml/StructurizrPlantUMLExporter.java | 626 ++++++ .../export/websequencediagrams/README.md | 23 + .../WebSequenceDiagramsDiagram.java | 17 + .../WebSequenceDiagramsExporter.java | 176 ++ .../export/AbstractExporterTests.java | 29 + .../export/IndentingWriterTests.java | 76 + .../export/dot/36141-Components.dot | 43 + .../export/dot/36141-Containers.dot | 37 + .../dot/36141-DevelopmentDeployment.dot | 97 + .../export/dot/36141-LiveDeployment.dot | 152 ++ .../structurizr/export/dot/36141-SignIn.dot | 29 + .../export/dot/36141-SystemContext.dot | 17 + .../export/dot/36141-SystemLandscape.dot | 35 + .../dot/54915-AmazonWebServicesDeployment.dot | 75 + .../export/dot/DOTDiagramExporterTests.java | 425 ++++ .../export/dot/groups-Components.dot | 35 + .../export/dot/groups-Containers.dot | 35 + .../export/dot/groups-SystemLandscape.dot | 48 + .../structurizr/export/dot/nested-groups.dot | 69 + .../export/ilograph/36141.ilograph | 631 ++++++ .../export/ilograph/54915.ilograph | 120 + .../export/ilograph/IlographWriterTests.java | 76 + .../export/mermaid/36141-Components.mmd | 48 + .../export/mermaid/36141-Containers.mmd | 39 + .../mermaid/36141-DevelopmentDeployment.mmd | 61 + .../export/mermaid/36141-LiveDeployment.mmd | 92 + .../export/mermaid/36141-SignIn-sequence.mmd | 13 + .../export/mermaid/36141-SignIn.mmd | 27 + .../export/mermaid/36141-SystemContext.mmd | 20 + .../export/mermaid/36141-SystemLandscape.mmd | 36 + .../54915-AmazonWebServicesDeployment.mmd | 48 + .../mermaid/MermaidDiagramExporterTests.java | 317 +++ .../export/mermaid/groups-Components.mmd | 26 + .../export/mermaid/groups-Containers.mmd | 26 + .../export/mermaid/groups-SystemLandscape.mmd | 34 + .../export/mermaid/nested-groups.mmd | 43 + .../C4PlantUMLDiagramExporterTests.java | 755 +++++++ ...ructurizrPlantUMLDiagramExporterTests.java | 1031 +++++++++ .../plantuml/c4plantuml/36141-Components.puml | 52 + .../plantuml/c4plantuml/36141-Containers.puml | 46 + .../36141-DevelopmentDeployment.puml | 55 + .../c4plantuml/36141-LiveDeployment.puml | 79 + .../c4plantuml/36141-SignIn-sequence.puml | 26 + .../plantuml/c4plantuml/36141-SignIn.puml | 36 + .../c4plantuml/36141-SystemContext.puml | 27 + .../c4plantuml/36141-SystemLandscape.puml | 39 + ...-AmazonWebServicesDeployment-WithTags.puml | 52 + ...azonWebServicesDeployment-WithoutTags.puml | 39 + .../plantuml/c4plantuml/group-styles-1.puml | 28 + .../plantuml/c4plantuml/group-styles-2.puml | 28 + .../c4plantuml/groups-Components.puml | 26 + .../c4plantuml/groups-Containers.puml | 26 + .../c4plantuml/groups-SystemLandscape.puml | 31 + .../nested-groups-with-dot-separator.puml | 26 + .../plantuml/c4plantuml/nested-groups.puml | 38 + .../printProperties-containerView.puml | 28 + .../printProperties-deploymentView.puml | 21 + .../structurizr/36141-Components.puml | 116 + .../structurizr/36141-Containers.puml | 92 + .../36141-DevelopmentDeployment.puml | 128 ++ .../structurizr/36141-LiveDeployment.puml | 190 ++ .../structurizr/36141-SignIn-sequence.puml | 49 + .../plantuml/structurizr/36141-SignIn.puml | 60 + .../structurizr/36141-SystemContext.puml | 50 + .../structurizr/36141-SystemLandscape.puml | 82 + ...15-AmazonWebServicesDeployment-Legend.puml | 102 + .../54915-AmazonWebServicesDeployment.puml | 111 + ...onent-view-with-external-components-1.puml | 56 + ...onent-view-with-external-components-2.puml | 62 + ...mic-view-container-scoped-with-groups.puml | 62 + ...ew-software-system-scoped-with-groups.puml | 62 + .../dynamic-view-unscoped-with-groups.puml | 46 + ...namic-view-with-external-components-1.puml | 56 + ...namic-view-with-external-components-2.puml | 62 + .../plantuml/structurizr/group-styles-1.puml | 60 + .../plantuml/structurizr/group-styles-2.puml | 60 + .../structurizr/groups-Components.puml | 56 + .../structurizr/groups-Containers.puml | 56 + .../structurizr/groups-SystemLandscape.puml | 69 + .../plantuml/structurizr/nested-groups.puml | 88 + .../websequencediagrams/36141-SignIn.wsd | 13 + .../WebSequenceDiagramsExporterTests.java | 53 + .../src/test/resources/groups.json | 203 ++ .../structurizr-36141-workspace.json | 1999 +++++++++++++++++ .../structurizr-54915-workspace.json | 353 +++ structurizr-graphviz/README.md | 26 + structurizr-graphviz/build.gradle | 10 + .../com/structurizr/graphviz/Constants.java | 17 + .../com/structurizr/graphviz/DOTDiagram.java | 17 + .../com/structurizr/graphviz/DOTExporter.java | 620 +++++ .../graphviz/GraphvizAutomaticLayout.java | 217 ++ .../structurizr/graphviz/RankDirection.java | 23 + .../com/structurizr/graphviz/SVGReader.java | 203 ++ .../graphviz/DOTExporterTests.java | 616 +++++ .../GraphvizAutomaticLayoutTests.java | 49 + .../structurizr/graphviz/SVGReaderTests.java | 66 + .../test/resources/graphviz/SystemContext.dot | 14 + .../resources/graphviz/SystemContext.dot.svg | 35 + .../structurizr-54915-workspace.json | 353 +++ structurizr-import/README.md | 11 + structurizr-import/build.gradle | 9 + .../diagrams/AbstractDiagramImporter.java | 33 + .../importer/diagrams/kroki/KrokiEncoder.java | 30 + .../diagrams/kroki/KrokiImporter.java | 45 + .../diagrams/mermaid/MermaidEncoder.java | 15 + .../diagrams/mermaid/MermaidImporter.java | 50 + .../diagrams/plantuml/PlantUMLEncoder.java | 73 + .../diagrams/plantuml/PlantUMLImporter.java | 54 + .../AdrToolsDecisionImporter.java | 237 ++ .../DefaultDocumentationImporter.java | 81 + .../documentation/DefaultImageImporter.java | 91 + .../DocumentationImportException.java | 13 + .../documentation/DocumentationImporter.java | 20 + .../importer/documentation/FormatFinder.java | 38 + ...RecursiveDefaultDocumentationImporter.java | 70 + .../diagrams/kroki/KrokiEncoderTests.java | 68 + .../diagrams/kroki/KrokiImporterTests.java | 88 + .../diagrams/mermaid/MermaidEncoderTests.java | 28 + .../mermaid/MermaidImporterTests.java | 88 + .../plantuml/PlantUMLEncoderTests.java | 20 + .../plantuml/PlantUMLImporterTests.java | 95 + .../AdrToolsDecisionImporterTests.java | 128 ++ .../DefaultDocumentImporterTests.java | 81 + .../DefaultImageImporterTests.java | 181 ++ .../documentation/FormatFinderTests.java | 38 + ...RecursiveDefaultDocumentImporterTests.java | 51 + .../0001-record-architecture-decisions.md | 19 + .../adrs/0002-implement-as-shell-scripts.md | 28 + .../0003-single-command-with-subcommands.md | 45 + .../resources/adrs/0004-markdown-format.md | 40 + .../test/resources/adrs/0005-help-comments.md | 42 + ...n-in-other-version-control-repositories.md | 41 + ...-config-executable-to-get-configuration.md | 31 + .../0008-use-iso-8601-format-for-dates.md | 43 + .../test/resources/adrs/0009-help-scripts.md | 28 + .../test/resources/diagrams/kroki/diagram.dot | 1 + .../test/resources/diagrams/mermaid/class.mmd | 21 + .../resources/diagrams/mermaid/flowchart.mmd | 6 + .../diagrams/plantuml/with-title.puml | 4 + .../diagrams/plantuml/without-title.puml | 3 + .../test/resources/docs/docs/01-section-1.md | 1 + .../resources/docs/docs/02-section-2.markdown | 1 + .../resources/docs/docs/03-section-3.text | 1 + .../resources/docs/docs/04-section-4.adoc | 1 + .../resources/docs/docs/05-section-5.asciidoc | 1 + .../test/resources/docs/docs/06-section-6.asc | 1 + .../docs/docs/07-subdirectory/01-section-1.md | 1 + .../test/resources/docs/docs/images/image.gif | Bin 0 -> 4682 bytes .../resources/docs/docs/images/image.jpeg | Bin 0 -> 1467 bytes .../test/resources/docs/docs/images/image.jpg | Bin 0 -> 1467 bytes .../test/resources/docs/docs/images/image.png | Bin 0 -> 1563 bytes .../src/test/resources/docs/images/image.gif | Bin 0 -> 4682 bytes .../src/test/resources/docs/images/image.jpeg | Bin 0 -> 1467 bytes .../src/test/resources/docs/images/image.jpg | Bin 0 -> 1467 bytes .../src/test/resources/docs/images/image.png | Bin 0 -> 1563 bytes .../src/test/resources/docs/images/image.svg | 3 + .../resources/docs/images/images/image.gif | Bin 0 -> 4682 bytes .../resources/docs/images/images/image.jpeg | Bin 0 -> 1467 bytes .../resources/docs/images/images/image.jpg | Bin 0 -> 1467 bytes .../resources/docs/images/images/image.png | Bin 0 -> 1563 bytes .../resources/docs/images/noimages/readme.md | 1 + 741 files changed, 39604 insertions(+), 430 deletions(-) create mode 100644 changelog.md delete mode 100644 docs/changelog.md create mode 100644 structurizr-client/README.md rename structurizr-client/{test/integration => src/integrationTest/java}/com/structurizr/api/BackwardsCompatibilityTests.java (97%) rename structurizr-client/{test/integration => src/integrationTest/java}/com/structurizr/api/StructurizrClientIntegrationTests.java (100%) rename structurizr-client/{test/integration => src/integrationTest/java}/com/structurizr/api/WorkspaceRulesValidationTests.java (99%) rename structurizr-client/{test/integration => src/integrationTest/resources}/backwardsCompatibility/structurizr-31-workspace.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/backwardsCompatibility/structurizr-36141-workspace.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/backwardsCompatibility/structurizr-39459-workspace.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ComponentNamesAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ContainerNamesAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ElementIdsAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/RelationshipDescriptionsAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/RelationshipIdsAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json (100%) rename structurizr-client/{test/integration => src/integrationTest/resources}/workspaceValidation/ViewKeysAreNotUnique.json (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/AbstractApiClient.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/AdminApiClient.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/ApiResponse.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/HashBasedMessageAuthenticationCode.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/HmacAuthorizationHeader.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/HmacContent.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/HttpHeaders.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/Md5Digest.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/StructurizrClient.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/StructurizrClientException.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/WorkspaceApiClient.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/WorkspaceMetadata.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/api/Workspaces.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/encryption/AesEncryptionStrategy.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/encryption/EncryptedWorkspace.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/encryption/EncryptionLocation.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/encryption/EncryptionStrategy.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/WorkspaceReader.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/WorkspaceReaderException.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/WorkspaceWriter.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/WorkspaceWriterException.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/json/AbstractJsonReader.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/json/AbstractJsonWriter.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/json/EncryptedJsonReader.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/json/EncryptedJsonWriter.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/json/JsonReader.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/io/json/JsonWriter.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/util/WorkspaceUtils.java (100%) rename structurizr-client/src/{ => main/java}/com/structurizr/view/ThemeUtils.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/api/ApiResponseTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/api/HmacAuthorizationHeaderTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/api/HmacContentTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/api/Md5DigestTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/api/StructurizrClientTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/encryption/AesEncryptionStrategyTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/encryption/EncryptedWorkspaceTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/encryption/MockEncryptionStrategy.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/io/json/EncryptedJsonTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/io/json/EncryptedJsonWriterTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/io/json/JsonTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/io/json/JsonWriterTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/util/WorkspaceUtilsTests.java (100%) rename structurizr-client/{test/unit => src/test/java}/com/structurizr/view/ThemeUtilsTests.java (100%) create mode 100644 structurizr-core/README.md rename structurizr-core/src/{ => main/java}/com/structurizr/AbstractWorkspace.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/PropertyHolder.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/Workspace.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/WorkspaceValidationException.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/configuration/Role.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/configuration/User.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/configuration/Visibility.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/configuration/WorkspaceConfiguration.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/configuration/WorkspaceScope.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/documentation/Decision.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/documentation/Documentable.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/documentation/Documentation.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/documentation/DocumentationContent.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/documentation/Format.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/documentation/Image.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/documentation/Section.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/CanonicalNameGenerator.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Component.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Constants.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Container.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/ContainerInstance.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/CustomElement.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/DeploymentElement.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/DeploymentNode.java (98%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Element.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Enterprise.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/GroupableElement.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/HttpHealthCheck.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/IdGenerator.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/ImpliedRelationshipsStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/InfrastructureNode.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/InteractionStyle.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Location.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Model.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/ModelItem.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Person.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Perspective.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Relationship.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/SoftwareSystem.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/SoftwareSystemInstance.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/StaticStructureElement.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/StaticStructureElementInstance.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/model/Tags.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/util/ImageUtils.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/util/MapUtils.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/util/StringUtils.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/util/TagUtils.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/util/Url.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/validation/WorkspaceScopeValidationException.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/validation/WorkspaceScopeValidator.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/validation/WorkspaceScopeValidatorFactory.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/AbstractStyle.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/AnimatedView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Animation.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/AutomaticLayout.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Border.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Branding.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Color.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ComponentView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Configuration.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ContainerView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/CustomView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/DefaultLayoutMergeStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/DeploymentView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Dimensions.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/DynamicView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ElementNotPermittedInViewException.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ElementStyle.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ElementView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/FilterMode.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/FilteredView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Font.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ImageView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/LayoutMergeStrategy.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/LineStyle.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/MetadataSymbols.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ModelView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/PaperSize.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ParallelSequenceCounter.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/RelationshipStyle.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/RelationshipView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Routing.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/SequenceCounter.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/SequenceNumber.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Shape.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/StaticView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Styles.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/SystemContextView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/SystemLandscapeView.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Terminology.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Theme.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/Vertex.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/View.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ViewSet.java (100%) rename structurizr-core/src/{ => main/java}/com/structurizr/view/ViewSortOrder.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/AbstractWorkspaceTestBase.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/WorkspaceTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/configuration/UserTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/configuration/WorkspaceConfigurationTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/documentation/DecisionTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/documentation/DocumentationTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/documentation/SectionTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/ComponentTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/ContainerInstanceTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/ContainerTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/CustomElementTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/DeploymentNodeTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/ElementTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/GroupableElementTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/HttpHealthCheckTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/InfrastructureNodeTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/ModelItemTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/ModelTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/PersonTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/RelationshipTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/SoftwareSystemInstanceTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/model/SoftwareSystemTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/util/ImageUtilsTests.java (96%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/util/StringUtilsTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/util/UrlTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/AutomaticLayoutTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/BrandingTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ColorTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ComponentViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ConfigurationTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ContainerViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/DefaultLayoutMergeStrategyTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/DeploymentViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/DimensionsTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/DynamicViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ElementStyleTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ElementViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/FilteredViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/FontTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ImageViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/PaperSizeTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/RelationshipStyleTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/SequenceCounterTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/SequenceNumberTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/StaticViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/StylesTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/SystemContextViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/SystemLandscapeViewTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/TerminologyTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/VertexTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ViewSetTests.java (100%) rename structurizr-core/{test/unit => src/test/java}/com/structurizr/view/ViewTests.java (100%) rename structurizr-core/{test/unit/com/structurizr/util => src/test/resources}/structurizr-logo.png (100%) create mode 100644 structurizr-dsl/README.md create mode 100644 structurizr-dsl/build.gradle create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/AdrsParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/AutoLayoutParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CommentDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/Constant.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ConstantParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewContentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DefaultViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironment.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroupParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParallelSequenceDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/FileUtils.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/HealthCheckParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScope.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScopeParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedFile.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/InlineScriptDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PersonDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PluginParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPlugin.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPluginContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslScriptContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/Tokenizer.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/UserRoleParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/UsersDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ViewDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ViewParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ViewsDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/AdrsParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/AutoLayoutParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ConfigurationParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ConstantParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/CookbookTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewContentParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DefaultViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentEnvironmentParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentGroupParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DocsParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewRelationshipParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ExternalScriptDslContextTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/HealthCheckParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierScopeParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/InlineScriptDslContextTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/PluginDslContextTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/PluginParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/PropertyParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ScriptParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewExpressionParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/SystemContextViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/SystemLandscapeViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/TokenizerTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/TokensTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/UserRoleParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ViewParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/example/ExampleDecisionImporter.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/example/ExampleDocumentationImporter.java create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0001-record-architecture-decisions.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0002-implement-as-shell-scripts.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0003-single-command-with-subcommands.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0004-markdown-format.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0005-help-comments.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0007-invoke-adr-config-executable-to-get-configuration.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0008-use-iso-8601-format-for-dates.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0009-help-scripts.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/adrs/0010-asciidoc-format.md create mode 100644 structurizr-dsl/src/test/resources/dsl/adrs/workspace.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/amazon-web-services-local.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/amazon-web-services.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/adrs/0001-record-architecture-decisions.md create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/details.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/01-context.md create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/02-containers.md create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/03-development-environment.adoc create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/04-deployment.adoc create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/summary.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/big-bank-plc/views/styles-people.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/deployment-groups.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md create mode 100644 structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/dynamic-view-with-custom-elements.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-relationships.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/dynamic.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/exclude-implied-relationship.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/exclude-relationships.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/1.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/2.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/3.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/4.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/workspace.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/extend/workspace.json create mode 100644 structurizr-dsl/src/test/resources/dsl/filteredviews.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/financial-risk-system.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/getting-started-short.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/getting-started.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/group-url.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/group-without-brace.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/groups-nested.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/groups.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-1.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-2.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-3.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-when-unassigned.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/identifiers.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/image-views/diagram.dot create mode 100644 structurizr-dsl/src/test/resources/dsl/image-views/diagram.mmd create mode 100644 structurizr-dsl/src/test/resources/dsl/image-views/diagram.puml create mode 100644 structurizr-dsl/src/test/resources/dsl/image-views/logo.png create mode 100644 structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/include-directory.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/include-file.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/include-implied-relationship.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/include-url.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/include/docs/section.md create mode 100644 structurizr-dsl/src/test/resources/dsl/include/model.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/include/model/software-system/model.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/iso-8859.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/logo.png create mode 100644 structurizr-dsl/src/test/resources/dsl/multi-line-with-error.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/multi-line.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/multiple-model-tokens.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/multiple-view-tokens.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/multiple-workspace-tokens.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/parallel1.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/parallel2.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/plugin-with-parameters.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/plugin-without-parameters.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/plugins/structurizr-dsl-plugin-1.0.0.jar create mode 100644 structurizr-dsl/src/test/resources/dsl/ref.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/relationship-already-exists.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/script-external-with-parameters.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/script-external.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/script-in-dynamic-view.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/script-inline.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/shapes.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/test-with-parameters.groovy create mode 100644 structurizr-dsl/src/test/resources/dsl/test.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/test.groovy create mode 100644 structurizr-dsl/src/test/resources/dsl/test.js create mode 100644 structurizr-dsl/src/test/resources/dsl/test.kts create mode 100644 structurizr-dsl/src/test/resources/dsl/test.rb create mode 100644 structurizr-dsl/src/test/resources/dsl/this.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/unexpected-tokens-after-workspace.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/unexpected-tokens-before-workspace.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/unexpected-tokens-in-workspace.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/utf8.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/views-without-keys.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/workspace-properties.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl create mode 100644 structurizr-export/README.md create mode 100644 structurizr-export/build.gradle create mode 100644 structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/AbstractExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/AbstractWorkspaceExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/Diagram.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/DiagramExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/Exporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/IndentType.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/Legend.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/WorkspaceExport.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/WorkspaceExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/dot/DOTDiagram.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/dot/README.md create mode 100644 structurizr-export/src/main/java/com/structurizr/export/dot/RankDirection.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographWorkspaceExport.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/ilograph/README.md create mode 100644 structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagram.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/mermaid/README.md create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDiagram.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/README.md create mode 100644 structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsDiagram.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/AbstractExporterTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/IndentingWriterTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot create mode 100644 structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph create mode 100644 structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph create mode 100644 structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographWriterTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml create mode 100644 structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java create mode 100644 structurizr-export/src/test/resources/groups.json create mode 100644 structurizr-export/src/test/resources/structurizr-36141-workspace.json create mode 100644 structurizr-export/src/test/resources/structurizr-54915-workspace.json create mode 100644 structurizr-graphviz/README.md create mode 100644 structurizr-graphviz/build.gradle create mode 100644 structurizr-graphviz/src/main/java/com/structurizr/graphviz/Constants.java create mode 100644 structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTDiagram.java create mode 100644 structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTExporter.java create mode 100644 structurizr-graphviz/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java create mode 100644 structurizr-graphviz/src/main/java/com/structurizr/graphviz/RankDirection.java create mode 100644 structurizr-graphviz/src/main/java/com/structurizr/graphviz/SVGReader.java create mode 100644 structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java create mode 100644 structurizr-graphviz/src/test/java/com/structurizr/graphviz/GraphvizAutomaticLayoutTests.java create mode 100644 structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java create mode 100644 structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot create mode 100644 structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot.svg create mode 100644 structurizr-graphviz/src/test/resources/structurizr-54915-workspace.json create mode 100644 structurizr-import/README.md create mode 100644 structurizr-import/build.gradle create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiEncoder.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultDocumentationImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultImageImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImportException.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/FormatFinder.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentationImporter.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiEncoderTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultDocumentImporterTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultImageImporterTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/documentation/FormatFinderTests.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentImporterTests.java create mode 100644 structurizr-import/src/test/resources/adrs/0001-record-architecture-decisions.md create mode 100644 structurizr-import/src/test/resources/adrs/0002-implement-as-shell-scripts.md create mode 100644 structurizr-import/src/test/resources/adrs/0003-single-command-with-subcommands.md create mode 100644 structurizr-import/src/test/resources/adrs/0004-markdown-format.md create mode 100644 structurizr-import/src/test/resources/adrs/0005-help-comments.md create mode 100644 structurizr-import/src/test/resources/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md create mode 100644 structurizr-import/src/test/resources/adrs/0007-invoke-adr-config-executable-to-get-configuration.md create mode 100644 structurizr-import/src/test/resources/adrs/0008-use-iso-8601-format-for-dates.md create mode 100644 structurizr-import/src/test/resources/adrs/0009-help-scripts.md create mode 100644 structurizr-import/src/test/resources/diagrams/kroki/diagram.dot create mode 100644 structurizr-import/src/test/resources/diagrams/mermaid/class.mmd create mode 100644 structurizr-import/src/test/resources/diagrams/mermaid/flowchart.mmd create mode 100644 structurizr-import/src/test/resources/diagrams/plantuml/with-title.puml create mode 100644 structurizr-import/src/test/resources/diagrams/plantuml/without-title.puml create mode 100644 structurizr-import/src/test/resources/docs/docs/01-section-1.md create mode 100644 structurizr-import/src/test/resources/docs/docs/02-section-2.markdown create mode 100644 structurizr-import/src/test/resources/docs/docs/03-section-3.text create mode 100644 structurizr-import/src/test/resources/docs/docs/04-section-4.adoc create mode 100644 structurizr-import/src/test/resources/docs/docs/05-section-5.asciidoc create mode 100644 structurizr-import/src/test/resources/docs/docs/06-section-6.asc create mode 100644 structurizr-import/src/test/resources/docs/docs/07-subdirectory/01-section-1.md create mode 100644 structurizr-import/src/test/resources/docs/docs/images/image.gif create mode 100644 structurizr-import/src/test/resources/docs/docs/images/image.jpeg create mode 100644 structurizr-import/src/test/resources/docs/docs/images/image.jpg create mode 100644 structurizr-import/src/test/resources/docs/docs/images/image.png create mode 100644 structurizr-import/src/test/resources/docs/images/image.gif create mode 100644 structurizr-import/src/test/resources/docs/images/image.jpeg create mode 100644 structurizr-import/src/test/resources/docs/images/image.jpg create mode 100644 structurizr-import/src/test/resources/docs/images/image.png create mode 100644 structurizr-import/src/test/resources/docs/images/image.svg create mode 100644 structurizr-import/src/test/resources/docs/images/images/image.gif create mode 100644 structurizr-import/src/test/resources/docs/images/images/image.jpeg create mode 100644 structurizr-import/src/test/resources/docs/images/images/image.jpg create mode 100644 structurizr-import/src/test/resources/docs/images/images/image.png create mode 100644 structurizr-import/src/test/resources/docs/images/noimages/readme.md diff --git a/README.md b/README.md index 9b45a95ef..41abcb2b6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # Structurizr for Java -This GitHub repository is (1) a client library for the [Structurizr](https://structurizr.com) cloud service and on-premises installation -and (2) a way to create a Structurizr workspace using Java code. +This repository contains the source code for the following libraries: -Looking for the [Structurizr DSL](https://github.com/structurizr/dsl) instead? +- [structurizr-client](structurizr-client) +- [structurizr-core](structurizr-core) +- [structurizr-dsl](structurizr-dsl) +- [structurizr-export](structurizr-export) +- [structurizr-graphviz](structurizr-graphviz) +- [structurizr-import](structurizr-import) -- [Documentation](https://docs.structurizr.com/java) -- [Changelog](docs/changelog.md) \ No newline at end of file +- [Documentation](https://docs.structurizr.com) +- [Changelog](changelog.md) \ No newline at end of file diff --git a/build.gradle b/build.gradle index b1052523f..e939207e8 100644 --- a/build.gradle +++ b/build.gradle @@ -8,25 +8,12 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '1.29.0' + version = '2.0.0' repositories { mavenCentral() } - sourceSets { - main { - java { - srcDir 'src' - } - } - test { - java { - srcDir 'test/unit' - } - } - } - test { useJUnitPlatform() } diff --git a/changelog.md b/changelog.md new file mode 100644 index 000000000..cc90bf20c --- /dev/null +++ b/changelog.md @@ -0,0 +1,6 @@ +# Changelog + +## 2.0.0 (unreleased) + +- structurizr-core: Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). + diff --git a/docs/changelog.md b/docs/changelog.md deleted file mode 100644 index 90451c105..000000000 --- a/docs/changelog.md +++ /dev/null @@ -1,402 +0,0 @@ -# Changelog - -## unreleased - -- Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). - -## 1.29.0 (28th December 2023) - -- Adds `com.structurizr.api.AdminApiClient` as a client for the cloud service/on-premises admin APIs. -- Adds support for inter-workspace URLs of the form `{workspace:123456}/diagrams`. -- Deprecates `StructurizrClient` in favour of `WorkspaceApiClient`. - -## 1.28.1 (11th December 2023) - -- `AbstractWorkspace.clearConfiguration()` creates a new instance rather than nulling it. - -## 1.28.0 (19th November 2023) - -- Adds a flag to determine whether automatic layout has been applied or not. -- Adds support for perspective values. -- Adds the ability to scope and validate a workspace. - -## 1.27.0 (23rd October 2023) - -- Upgrades dependencies, targets Java 17. -- Adds a 'url' property to 'RelationshipView' (see https://github.com/structurizr/java/issues/214). - -## 1.26.1 (28th July 2023) - -- Adds the ability to specify the workspace visibility (private/public) via the workspace configuration. - -## 1.25.1 (26th July 2023) - -- Adds a `clearUsers()` method to clear configured users on the workspace configuration. - -## 1.25.0 (22nd July 2023) - -- Fixes https://github.com/structurizr/java/issues/213 (Views are not created automatically if non-English characters are used in software systems' names) -- Adds a way to load themes with a timeout. - -## 1.24.1 (5th April 2023) - -- Reduces visibility of `setOrder()` and `setDescription()` on `RelationshipView`, as they should not be public. - -## 1.24.0 (30th March 2023) - -- Adds a `group` property to deployment nodes, infrastructure nodes, software system instances, and container instances. - -## 1.23.2 (24th March 2023) - -- `DynamicView.endParallelSequences(true)` will now increment the counter when no relationships have been defined in the parallel sequence. - -## 1.23.1 (17th March 2023) - -- Deprecates the `setExternalBoundariesVisible` methods on `ContainerView`, `ComponentView`, and `DynamicView`. -- Removes the check for empty content when adding a documentation section. - -## 1.23.0 (11th March 2023) - -- Deprecates `Enterprise` and `Location` concepts. -- Adds properties to the model. - -## 1.22.3 (11th March 2023) - -- Adds better backwards compatibility for removal of documentation section titles. - -## 1.22.2 (10th March 2023) - -- Updates Jackson library dependency. - -## 1.22.1 (5th March 2023) - -- Removes unused documentation section title property. - -## 1.22.0 (5th March 2023) - -- Adds documentation to components. - -## 1.21.0 (26th February 2023) - -- __Breaking change__: Removes the concept of "code elements" from `Component`. -- Adds support for element/relationship URLs of the form `{workspace}/...` for linking to diagrams/documentation/decisions in the same workspace. - -## 1.20.1 (16th February 2023) - -- `ViewSet.getViews()` now includes all views. -- `ViewSet.getViewWithKey()` is now public. - -## 1.20.0 (16th February 2023) - -- __Breaking change__: Renamed `com.structurizr.view.View` to `com.structurizr.view.ModelView`. -- Added support for "image views". -- Added a `Window` shape. -- `ThemeUtils.toJson()` now includes the workspace branding logo and font in the resulting theme. - -## 1.19.0 (26th January 2023) - -- Fixes #196 (Named colours are case-sensitive). - -## 1.18.0 (15th January 2023) - -- Fixes #191 (Layout of relationships is reset when changing the description). -- Adds support for using (CSS/HTML) named colors instead of hex color codes (#192). - -## 1.17.0 (5th January 2023) - -- Fixes case-sensitivity inconsistencies related to element names and relationship descriptions (#183). -- Adds support for setting deployment node instances to positive integers or a range (e.g. 0..1, 0..N, 0..*, 1..N, 1..*, 5..10, etc). - -## 1.16.2 (22nd December 2022) - -- Upgraded dependencies. - -## 1.16.1 (27th October 2022) - -- Adds name-value properties to views. - -## 1.16.0 (24th October 2022) - -- Adds support for element style stroke widths. - -## 1.15.3 (11th October 2022) - -- Updates some transitive dependencies. - -## 1.15.2 (3rd October 2022) - -- Adds support for element icons being specified as filenames (rather than full URLs) in themes. - -## 1.15.1 (23rd September 2022) - -- Adds some additional functionality for getting and finding element/relationship styles. - -## 1.15.0 (13th September 2022) - -- Adds documentation section filenames into the model. -- Adds support for custom elements on dynamic views. - -## 1.14.1 (15th August 2022) - -- Enables `structurizr-core` to be used as a transitive dependency by consumers of `structurizr-client`. - -## 1.14.0 (14th August 2022) - -- Adds a helper method (`AbstractImpliedRelationshipsStrategy.createImpliedRelationship`) to create implied relationships, which can then be used by custom implementations. -- Provides a way to add specific relationships to dynamic views. - -## 1.13.0 (25th June 2022) - -- Adds support for name/value properties on element and relationship styles. - -## 1.12.2 (30th March 2022) - -- Adds support for sorting views by the order in which they are created. - -## 1.12.1 (2nd March 2022) - -- Renamed `Decision.Link.type` to `Decision.Link.description`. - -## 1.12.0 (1st March 2022) - -- Breaking API changes to how documentation and decisions are managed. -- Moved documentation importers/templates to [structurizr-documentation](https://github.com/structurizr/documentation). -- Moved examples to [structurizr-examples](https://github.com/structurizr/examples) -- Removal of deprecated `Model.addImplicitRelationships()` method. - -## 1.11.0 (18th February 2022) - -- Fixes #167 (ImpliedRelationship Strategy replication of URL and perspectives). -- Makes the `Decision.setContent()` method public, to allow pre-processing of content before workspace upload/rendering. - -## 1.10.1 (1st February 2022) - -- Makes the `Section.setContent()` method public, to allow pre-processing of content before workspace upload/rendering. - -## 1.10.0 (29th December 2021) - -- Adds support for different relationship line styles (solid, dashed, dotted). -- Adds the ability to indicate that individual views should not merge layout from remotes. -- Adds name/value properties to the view set configuration. - -## 1.9.10 (26th November 2021) - -- Promotes a couple of methods to be public; no functional changes. - -## 1.9.9 (16th October 2021) - -- Adds the implied relationships functionality for custom elements. -- "addDefaultElements" will now also add any connected custom elements. - -## 1.9.8 (1st October 2021) - -- Adds support for relationships from deployment nodes to infrastructure nodes. - -## 1.9.7 (9th September 2021) - -- Adds support for software system/container instances in multiple deployment groups. - -## 1.9.6 (31st August 2021) - -- Added validation logic to reject unsupported image data URIs. -- Fixes #166 (ContainerInstance/SoftwareSystemInstance and auto-generation of deployment diagram). - -## 1.9.5 (7th June 2021) - -- Provides a way to store view dimensions. - -## 1.9.4 (22nd May 2021) - -- Bug fixes to prevent parents and children to both be added to container/component views. - -## 1.9.3 (11th May 2021) - -- Added an `addTheme` method on `Configuration`. -- Removed the `addDefaultStyles` method on `Styles`. - -## 1.9.2 (27th April 2021) - -- Adds a `Diamond` shape. - -## 1.9.1 (28th March 2021) - -- Adds a `findTerminology` method on the `Terminology` class. -- `Styles.findElementStyle` better mirrors how the Structurizr web renderer deals with element styling. - -## 1.9.0 (20th March 2021) - -- Adds support for adding individual infrastructure nodes, software system instances, and container instances to a deployment view. -- Adds support for removing software system instances from deployment views. -- Improved support for themes (e.g. when exporting to PlantUML), which now works the same as described at [Structurizr - Themes](https://structurizr.com/help/themes). -- Adds support for "deployment groups", providing a way to scope how software system/container instance relationships are replicated when added to deployment nodes. __breaking change__ - -## 1.8.0 (20th February 2021) - -- Adds support for custom elements and custom views (experimental). -- Bug fixes and improved workspace validation. - -## 1.7.2 (2nd February 2021) - -- Bug fixes. - -## 1.7.1 (28th January 2021) - -- Bug fixes. - -## 1.7.0 (6th January 2021) - -- Removes the dynamic view restrictions related to adding containers/components outside the scoped software system/container. -- Adds an "externalBoundariesVisible" property to DynamicView, so that external software system/container boundaries can be shown/hidden. -- Enhanced the rules relating to whether elements can be added to a view or not. -- Enhanced the logic to merge layout information of elements on views. - -## 1.6.3 (30th November 2020) - -- When adding a relationship to a dynamic view, the first relationship between the source and destination would be chosen, even if there are multiple relationships with different technologies. This release adds a way to indicate which relationship (based upon technology) should be chosen. -- Suppress description warnings for software system instances. - -## 1.6.2 (10th October 2020) - -- Resolves an issue with the AutomaticDocumentationTemplate, where images were being included as documentation content. - -## 1.6.1 (27th September 2020) - -- Added a "recursive" option to the AutomaticDocumentationTemplate, so that sub-directories can optionally be scanned too. - -## 1.6.0 (18th September 2020) - -- Changed the way that internal canonical element names are generated, to improve layout merging for deployment views. -- getParent() of SoftwareSystemInstance and ContainerInstance now returns the parent deployment node. -- Refactoring and bug fixes. - -## 1.5.0 (4th August 2020) - -- Fixes #151: linked relationship tags were not being taken into account when finding relationship styling. -- Fixes #153: Allow relationships in DynamicView to go both ways without two relationships between Elements in Model. -- Adds support for software system instances on deployment views (#150: how do I provide tech details for an external system to show in Deployment View?) -- The interaction style on relationships no longer defaults to Synchronous. -- Adds support for software system instances on deployment views. - -## 1.4.8 (15th July 2020) - -- Implied relationships now also copy the interaction style and tags. -- Fixes a serialisation problem with themes and styles. - -## 1.4.7 (6th July 2020) - -- Remove default stroke styling. - -## 1.4.6 (6th July 2020) - -- Adds a way to load styles from external themes. - -## 1.4.5 (21st June 2020) - -- Bug fixes. - -## 1.4.4 (21st June 2020) - -- Adds an "addDefaultElements()" method to the static/deployment views. -- Adds an "addDefaultStyles()" method to Styles. -- Adds a "createDefaultViews()" method to Views. - -## 1.4.3 (19th June 2020) - -- Fixes a bug where all deployment nodes would be added to a deployment view, even if that view had an environment set. -- Adds support for removing deployment nodes, infrastructure nodes, and container instances from deployment views. -- Fixes a bug where deployment node instances could set to a non-positive integer. - -## 1.4.2 (18th June 2020) - -- Adds the ability to add container instances and infrastructure nodes to the same animation step on a deployment view. -- Adds the ability to override the Structurizr client agent string. - -## 1.4.1 (14th June 2020) - -- Fixes a bug that defaults the relationship interaction style to Synchronous, when it's specifically set to null. - -## 1.4.0 (5th June 2020) - -- Added a "Component" element shape. -- Added a "Dotted" element border style. -- Components from any container can now be added to a component view. -- Added an externalContainersBoundariesVisible property to ComponentView, to set whether container boundaries should be visible for "external" components (those outside the container in scope). -- Improved the support for creating [implied relationships](docs/implied-relationships.md). -- Added the ability to customize the symbols used when rendering metadata. -- Adds support for infrastructure nodes. -- Adds support for multiple themes. -- Adds support for curved relationship routing. - -## 1.3.5 (26th March 2020) - -- Added an externalSoftwareSystemBoundariesVisible property to ContainerView, to set whether software system boundaries should be visible for "external" containers (those outside the software system in scope). -- Added a 16:10 ratio paper size. - -## 1.3.4 (29th February 2020) - -- Split View.setAutomaticLayout(boolean) to enableAutomaticLayout() and disableAutomaticLayout() (__breaking change__). -- Added A1 and A0 paper sizes. -- Adds support for themes. -- Adds support for tags on deployment nodes. -- Adds support for animations on deployment views. -- Adds support for URLs on relationships. - -## 1.3.3 (24th December 2019) - -- Fixes a deserialization issue with component views. - -## 1.3.2 (22nd November 2019) - -- Added support for element stroke colours. - -## 1.3.1 (29th October 2019) - -- The automatic layout algorithm can now be configured on individual views. -- The structurizr-annotations library can now be more easily used with OSGi applications. -- Fixes a bug with the PlantUML and WebSequenceDiagram writers, where relationships were sorted incorrectly (alphabetically, rather than numerically). -- Fixes a bug that allows relationships to be created between parents and children. -- The way layout information is copied between different versions of a view is now configurable by setting a custom LayoutMergeStrategy on a per view basis. - -## 1.3.0 (3rd March 2019) - -- Added the ability to lock and unlock workspaces, to prevent concurrent updates. - -## 1.2.0 (3rd January 2019) - -- Fixes an issue with Java 11 and SSL handshaking. -- The terminology for relationships can now be customised. -- Added support for icons on element styles. -- Top-level deployment nodes can now be given an environment property, to represent which deployment environment they belong to (e.g. "Development", "Live", etc). -- Relationships can no longer be created between container instances (__breaking change__). -- When adding elements to views, you can now optionally specify whether relationships to/from that element are added. -- Provided a way to customize the sort order when displaying the list of views. - -## 1.1.0 (8th November 2018) - -- Added the ability to specify users who should have read-write or read-only workspace access, via the ```workspace.getConfiguration().addUser(username, role)``` method. - -## 1.0.0 (17th Oct 2018) - -- Added name-value properties to relationships. -- Added the ability to define animations on the static structure diagrams. -- Removed support for colours in the corporate branding feature (__breaking change__). -- The PlantUML writer can now export sequence diagrams. - -## 1.0.0-RC7 - -- HTTP-based health check interval and timeout can be specified via the factory method now (__breaking change__). Also added some documentation and an example. -- Added an ```endParallelSequence(boolean)``` method to the ```DynamicView``` class, which allows sequence numbering to continue. -- Fixed a bug where the software system associated with a SystemContextView could be removed from the view. -- Added support for architecture decision records. - -## 1.0.0-RC6 - -- Component finders are no longer idempotent, and an exception will be thrown if the same component is discovered more than once (__breaking change__). -- Removed the "groups" property of documentation sections (__breaking change__). -- Added some new shapes: web browser, mobile device (portrait and landscape), and robot. -- Addition of @NonNull annotations (JSR 305: Annotations for Software Defect Detection). -- Added the ability to enable/disable the enterprise boundary on system landscape and system context views. -- Added the ability to customise the terminology used when rendering views. -- Added the ability to hide element metadata and/or descriptions. -- The Spring component finder now supports the @Endpoint annotation. -- Bug fixes and performance enhancements. \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 62816092a..3b97826de 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,8 @@ rootProject.name = 'structurizr-java' include 'structurizr-client' -include 'structurizr-core' \ No newline at end of file +include 'structurizr-core' +include 'structurizr-dsl' +include 'structurizr-export' +include 'structurizr-graphviz' +include 'structurizr-import' diff --git a/structurizr-client/README.md b/structurizr-client/README.md new file mode 100644 index 000000000..60a155cf1 --- /dev/null +++ b/structurizr-client/README.md @@ -0,0 +1,5 @@ +# structurizr-client + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-client.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-client) + +- [Documentation](https://docs.structurizr.com/java/workspace-api) diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index def7a58e6..f6a2c657e 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -13,8 +13,8 @@ dependencies { sourceSets { test { java { - srcDir 'test/unit' - srcDir 'test/integration' + srcDir 'src/test/java' + srcDir 'src/integrationTest/java' } } } \ No newline at end of file diff --git a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java similarity index 97% rename from structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java rename to structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java index 7f30cc635..ca11f7cf7 100644 --- a/structurizr-client/test/integration/com/structurizr/api/BackwardsCompatibilityTests.java +++ b/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java @@ -13,7 +13,7 @@ class BackwardsCompatibilityTests { - private static final File PATH_TO_WORKSPACE_FILES = new File("test/integration/backwardsCompatibility"); + private static final File PATH_TO_WORKSPACE_FILES = new File("./src/integrationTest/resources/backwardsCompatibility"); @Test void test() throws Exception { diff --git a/structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java b/structurizr-client/src/integrationTest/java/com/structurizr/api/StructurizrClientIntegrationTests.java similarity index 100% rename from structurizr-client/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java rename to structurizr-client/src/integrationTest/java/com/structurizr/api/StructurizrClientIntegrationTests.java diff --git a/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java b/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceRulesValidationTests.java similarity index 99% rename from structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java rename to structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceRulesValidationTests.java index efd07a640..5e74670ed 100644 --- a/structurizr-client/test/integration/com/structurizr/api/WorkspaceRulesValidationTests.java +++ b/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceRulesValidationTests.java @@ -10,7 +10,7 @@ public class WorkspaceRulesValidationTests { - private static final File PATH_TO_WORKSPACE_FILES = new File("test/integration/workspaceValidation"); + private static final File PATH_TO_WORKSPACE_FILES = new File("./src/integrationTest/resources/workspaceValidation"); @Test void exceptionThrown_WhenElementIdsAreNotUnique() throws Exception { diff --git a/structurizr-client/test/integration/backwardsCompatibility/structurizr-31-workspace.json b/structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-31-workspace.json similarity index 100% rename from structurizr-client/test/integration/backwardsCompatibility/structurizr-31-workspace.json rename to structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-31-workspace.json diff --git a/structurizr-client/test/integration/backwardsCompatibility/structurizr-36141-workspace.json b/structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-36141-workspace.json similarity index 100% rename from structurizr-client/test/integration/backwardsCompatibility/structurizr-36141-workspace.json rename to structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-36141-workspace.json diff --git a/structurizr-client/test/integration/backwardsCompatibility/structurizr-39459-workspace.json b/structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-39459-workspace.json similarity index 100% rename from structurizr-client/test/integration/backwardsCompatibility/structurizr-39459-workspace.json rename to structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-39459-workspace.json diff --git a/structurizr-client/test/integration/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/ComponentNamesAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ComponentNamesAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ComponentNamesAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ComponentNamesAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json diff --git a/structurizr-client/test/integration/workspaceValidation/ContainerNamesAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerNamesAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ContainerNamesAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerNamesAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json diff --git a/structurizr-client/test/integration/workspaceValidation/ElementIdsAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ElementIdsAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ElementIdsAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ElementIdsAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json diff --git a/structurizr-client/test/integration/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/RelationshipDescriptionsAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/RelationshipDescriptionsAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/RelationshipIdsAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipIdsAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/RelationshipIdsAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipIdsAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json diff --git a/structurizr-client/test/integration/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json diff --git a/structurizr-client/test/integration/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json diff --git a/structurizr-client/test/integration/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json diff --git a/structurizr-client/test/integration/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json diff --git a/structurizr-client/test/integration/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json diff --git a/structurizr-client/test/integration/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json diff --git a/structurizr-client/test/integration/workspaceValidation/ViewKeysAreNotUnique.json b/structurizr-client/src/integrationTest/resources/workspaceValidation/ViewKeysAreNotUnique.json similarity index 100% rename from structurizr-client/test/integration/workspaceValidation/ViewKeysAreNotUnique.json rename to structurizr-client/src/integrationTest/resources/workspaceValidation/ViewKeysAreNotUnique.json diff --git a/structurizr-client/src/com/structurizr/api/AbstractApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/AbstractApiClient.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/AbstractApiClient.java rename to structurizr-client/src/main/java/com/structurizr/api/AbstractApiClient.java diff --git a/structurizr-client/src/com/structurizr/api/AdminApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/AdminApiClient.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/AdminApiClient.java rename to structurizr-client/src/main/java/com/structurizr/api/AdminApiClient.java diff --git a/structurizr-client/src/com/structurizr/api/ApiResponse.java b/structurizr-client/src/main/java/com/structurizr/api/ApiResponse.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/ApiResponse.java rename to structurizr-client/src/main/java/com/structurizr/api/ApiResponse.java diff --git a/structurizr-client/src/com/structurizr/api/HashBasedMessageAuthenticationCode.java b/structurizr-client/src/main/java/com/structurizr/api/HashBasedMessageAuthenticationCode.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/HashBasedMessageAuthenticationCode.java rename to structurizr-client/src/main/java/com/structurizr/api/HashBasedMessageAuthenticationCode.java diff --git a/structurizr-client/src/com/structurizr/api/HmacAuthorizationHeader.java b/structurizr-client/src/main/java/com/structurizr/api/HmacAuthorizationHeader.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/HmacAuthorizationHeader.java rename to structurizr-client/src/main/java/com/structurizr/api/HmacAuthorizationHeader.java diff --git a/structurizr-client/src/com/structurizr/api/HmacContent.java b/structurizr-client/src/main/java/com/structurizr/api/HmacContent.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/HmacContent.java rename to structurizr-client/src/main/java/com/structurizr/api/HmacContent.java diff --git a/structurizr-client/src/com/structurizr/api/HttpHeaders.java b/structurizr-client/src/main/java/com/structurizr/api/HttpHeaders.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/HttpHeaders.java rename to structurizr-client/src/main/java/com/structurizr/api/HttpHeaders.java diff --git a/structurizr-client/src/com/structurizr/api/Md5Digest.java b/structurizr-client/src/main/java/com/structurizr/api/Md5Digest.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/Md5Digest.java rename to structurizr-client/src/main/java/com/structurizr/api/Md5Digest.java diff --git a/structurizr-client/src/com/structurizr/api/StructurizrClient.java b/structurizr-client/src/main/java/com/structurizr/api/StructurizrClient.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/StructurizrClient.java rename to structurizr-client/src/main/java/com/structurizr/api/StructurizrClient.java diff --git a/structurizr-client/src/com/structurizr/api/StructurizrClientException.java b/structurizr-client/src/main/java/com/structurizr/api/StructurizrClientException.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/StructurizrClientException.java rename to structurizr-client/src/main/java/com/structurizr/api/StructurizrClientException.java diff --git a/structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/WorkspaceApiClient.java rename to structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java diff --git a/structurizr-client/src/com/structurizr/api/WorkspaceMetadata.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/WorkspaceMetadata.java rename to structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java diff --git a/structurizr-client/src/com/structurizr/api/Workspaces.java b/structurizr-client/src/main/java/com/structurizr/api/Workspaces.java similarity index 100% rename from structurizr-client/src/com/structurizr/api/Workspaces.java rename to structurizr-client/src/main/java/com/structurizr/api/Workspaces.java diff --git a/structurizr-client/src/com/structurizr/encryption/AesEncryptionStrategy.java b/structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java similarity index 100% rename from structurizr-client/src/com/structurizr/encryption/AesEncryptionStrategy.java rename to structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java diff --git a/structurizr-client/src/com/structurizr/encryption/EncryptedWorkspace.java b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java similarity index 100% rename from structurizr-client/src/com/structurizr/encryption/EncryptedWorkspace.java rename to structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java diff --git a/structurizr-client/src/com/structurizr/encryption/EncryptionLocation.java b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptionLocation.java similarity index 100% rename from structurizr-client/src/com/structurizr/encryption/EncryptionLocation.java rename to structurizr-client/src/main/java/com/structurizr/encryption/EncryptionLocation.java diff --git a/structurizr-client/src/com/structurizr/encryption/EncryptionStrategy.java b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptionStrategy.java similarity index 100% rename from structurizr-client/src/com/structurizr/encryption/EncryptionStrategy.java rename to structurizr-client/src/main/java/com/structurizr/encryption/EncryptionStrategy.java diff --git a/structurizr-client/src/com/structurizr/io/WorkspaceReader.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceReader.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/WorkspaceReader.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceReader.java diff --git a/structurizr-client/src/com/structurizr/io/WorkspaceReaderException.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceReaderException.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/WorkspaceReaderException.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceReaderException.java diff --git a/structurizr-client/src/com/structurizr/io/WorkspaceWriter.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriter.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/WorkspaceWriter.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriter.java diff --git a/structurizr-client/src/com/structurizr/io/WorkspaceWriterException.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriterException.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/WorkspaceWriterException.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriterException.java diff --git a/structurizr-client/src/com/structurizr/io/json/AbstractJsonReader.java b/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonReader.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/json/AbstractJsonReader.java rename to structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonReader.java diff --git a/structurizr-client/src/com/structurizr/io/json/AbstractJsonWriter.java b/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonWriter.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/json/AbstractJsonWriter.java rename to structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonWriter.java diff --git a/structurizr-client/src/com/structurizr/io/json/EncryptedJsonReader.java b/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonReader.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/json/EncryptedJsonReader.java rename to structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonReader.java diff --git a/structurizr-client/src/com/structurizr/io/json/EncryptedJsonWriter.java b/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonWriter.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/json/EncryptedJsonWriter.java rename to structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonWriter.java diff --git a/structurizr-client/src/com/structurizr/io/json/JsonReader.java b/structurizr-client/src/main/java/com/structurizr/io/json/JsonReader.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/json/JsonReader.java rename to structurizr-client/src/main/java/com/structurizr/io/json/JsonReader.java diff --git a/structurizr-client/src/com/structurizr/io/json/JsonWriter.java b/structurizr-client/src/main/java/com/structurizr/io/json/JsonWriter.java similarity index 100% rename from structurizr-client/src/com/structurizr/io/json/JsonWriter.java rename to structurizr-client/src/main/java/com/structurizr/io/json/JsonWriter.java diff --git a/structurizr-client/src/com/structurizr/util/WorkspaceUtils.java b/structurizr-client/src/main/java/com/structurizr/util/WorkspaceUtils.java similarity index 100% rename from structurizr-client/src/com/structurizr/util/WorkspaceUtils.java rename to structurizr-client/src/main/java/com/structurizr/util/WorkspaceUtils.java diff --git a/structurizr-client/src/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java similarity index 100% rename from structurizr-client/src/com/structurizr/view/ThemeUtils.java rename to structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java diff --git a/structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java b/structurizr-client/src/test/java/com/structurizr/api/ApiResponseTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/api/ApiResponseTests.java rename to structurizr-client/src/test/java/com/structurizr/api/ApiResponseTests.java diff --git a/structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java b/structurizr-client/src/test/java/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java rename to structurizr-client/src/test/java/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java diff --git a/structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java b/structurizr-client/src/test/java/com/structurizr/api/HmacAuthorizationHeaderTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java rename to structurizr-client/src/test/java/com/structurizr/api/HmacAuthorizationHeaderTests.java diff --git a/structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java b/structurizr-client/src/test/java/com/structurizr/api/HmacContentTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/api/HmacContentTests.java rename to structurizr-client/src/test/java/com/structurizr/api/HmacContentTests.java diff --git a/structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java b/structurizr-client/src/test/java/com/structurizr/api/Md5DigestTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/api/Md5DigestTests.java rename to structurizr-client/src/test/java/com/structurizr/api/Md5DigestTests.java diff --git a/structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java b/structurizr-client/src/test/java/com/structurizr/api/StructurizrClientTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/api/StructurizrClientTests.java rename to structurizr-client/src/test/java/com/structurizr/api/StructurizrClientTests.java diff --git a/structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java b/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java rename to structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java diff --git a/structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java b/structurizr-client/src/test/java/com/structurizr/encryption/EncryptedWorkspaceTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java rename to structurizr-client/src/test/java/com/structurizr/encryption/EncryptedWorkspaceTests.java diff --git a/structurizr-client/test/unit/com/structurizr/encryption/MockEncryptionStrategy.java b/structurizr-client/src/test/java/com/structurizr/encryption/MockEncryptionStrategy.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/encryption/MockEncryptionStrategy.java rename to structurizr-client/src/test/java/com/structurizr/encryption/MockEncryptionStrategy.java diff --git a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonTests.java rename to structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonTests.java diff --git a/structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonWriterTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/io/json/EncryptedJsonWriterTests.java rename to structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonWriterTests.java diff --git a/structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/JsonTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/io/json/JsonTests.java rename to structurizr-client/src/test/java/com/structurizr/io/json/JsonTests.java diff --git a/structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/JsonWriterTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/io/json/JsonWriterTests.java rename to structurizr-client/src/test/java/com/structurizr/io/json/JsonWriterTests.java diff --git a/structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/util/WorkspaceUtilsTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/util/WorkspaceUtilsTests.java rename to structurizr-client/src/test/java/com/structurizr/util/WorkspaceUtilsTests.java diff --git a/structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java similarity index 100% rename from structurizr-client/test/unit/com/structurizr/view/ThemeUtilsTests.java rename to structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java diff --git a/structurizr-core/README.md b/structurizr-core/README.md new file mode 100644 index 000000000..23e5b133d --- /dev/null +++ b/structurizr-core/README.md @@ -0,0 +1,6 @@ +# structurizr-core + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-core.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-core) + +- [Documentation](https://docs.structurizr.com/java) + diff --git a/structurizr-core/src/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java similarity index 100% rename from structurizr-core/src/com/structurizr/AbstractWorkspace.java rename to structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java diff --git a/structurizr-core/src/com/structurizr/PropertyHolder.java b/structurizr-core/src/main/java/com/structurizr/PropertyHolder.java similarity index 100% rename from structurizr-core/src/com/structurizr/PropertyHolder.java rename to structurizr-core/src/main/java/com/structurizr/PropertyHolder.java diff --git a/structurizr-core/src/com/structurizr/Workspace.java b/structurizr-core/src/main/java/com/structurizr/Workspace.java similarity index 100% rename from structurizr-core/src/com/structurizr/Workspace.java rename to structurizr-core/src/main/java/com/structurizr/Workspace.java diff --git a/structurizr-core/src/com/structurizr/WorkspaceValidationException.java b/structurizr-core/src/main/java/com/structurizr/WorkspaceValidationException.java similarity index 100% rename from structurizr-core/src/com/structurizr/WorkspaceValidationException.java rename to structurizr-core/src/main/java/com/structurizr/WorkspaceValidationException.java diff --git a/structurizr-core/src/com/structurizr/configuration/Role.java b/structurizr-core/src/main/java/com/structurizr/configuration/Role.java similarity index 100% rename from structurizr-core/src/com/structurizr/configuration/Role.java rename to structurizr-core/src/main/java/com/structurizr/configuration/Role.java diff --git a/structurizr-core/src/com/structurizr/configuration/User.java b/structurizr-core/src/main/java/com/structurizr/configuration/User.java similarity index 100% rename from structurizr-core/src/com/structurizr/configuration/User.java rename to structurizr-core/src/main/java/com/structurizr/configuration/User.java diff --git a/structurizr-core/src/com/structurizr/configuration/Visibility.java b/structurizr-core/src/main/java/com/structurizr/configuration/Visibility.java similarity index 100% rename from structurizr-core/src/com/structurizr/configuration/Visibility.java rename to structurizr-core/src/main/java/com/structurizr/configuration/Visibility.java diff --git a/structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java b/structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceConfiguration.java similarity index 100% rename from structurizr-core/src/com/structurizr/configuration/WorkspaceConfiguration.java rename to structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceConfiguration.java diff --git a/structurizr-core/src/com/structurizr/configuration/WorkspaceScope.java b/structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceScope.java similarity index 100% rename from structurizr-core/src/com/structurizr/configuration/WorkspaceScope.java rename to structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceScope.java diff --git a/structurizr-core/src/com/structurizr/documentation/Decision.java b/structurizr-core/src/main/java/com/structurizr/documentation/Decision.java similarity index 100% rename from structurizr-core/src/com/structurizr/documentation/Decision.java rename to structurizr-core/src/main/java/com/structurizr/documentation/Decision.java diff --git a/structurizr-core/src/com/structurizr/documentation/Documentable.java b/structurizr-core/src/main/java/com/structurizr/documentation/Documentable.java similarity index 100% rename from structurizr-core/src/com/structurizr/documentation/Documentable.java rename to structurizr-core/src/main/java/com/structurizr/documentation/Documentable.java diff --git a/structurizr-core/src/com/structurizr/documentation/Documentation.java b/structurizr-core/src/main/java/com/structurizr/documentation/Documentation.java similarity index 100% rename from structurizr-core/src/com/structurizr/documentation/Documentation.java rename to structurizr-core/src/main/java/com/structurizr/documentation/Documentation.java diff --git a/structurizr-core/src/com/structurizr/documentation/DocumentationContent.java b/structurizr-core/src/main/java/com/structurizr/documentation/DocumentationContent.java similarity index 100% rename from structurizr-core/src/com/structurizr/documentation/DocumentationContent.java rename to structurizr-core/src/main/java/com/structurizr/documentation/DocumentationContent.java diff --git a/structurizr-core/src/com/structurizr/documentation/Format.java b/structurizr-core/src/main/java/com/structurizr/documentation/Format.java similarity index 100% rename from structurizr-core/src/com/structurizr/documentation/Format.java rename to structurizr-core/src/main/java/com/structurizr/documentation/Format.java diff --git a/structurizr-core/src/com/structurizr/documentation/Image.java b/structurizr-core/src/main/java/com/structurizr/documentation/Image.java similarity index 100% rename from structurizr-core/src/com/structurizr/documentation/Image.java rename to structurizr-core/src/main/java/com/structurizr/documentation/Image.java diff --git a/structurizr-core/src/com/structurizr/documentation/Section.java b/structurizr-core/src/main/java/com/structurizr/documentation/Section.java similarity index 100% rename from structurizr-core/src/com/structurizr/documentation/Section.java rename to structurizr-core/src/main/java/com/structurizr/documentation/Section.java diff --git a/structurizr-core/src/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java rename to structurizr-core/src/main/java/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java diff --git a/structurizr-core/src/com/structurizr/model/CanonicalNameGenerator.java b/structurizr-core/src/main/java/com/structurizr/model/CanonicalNameGenerator.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/CanonicalNameGenerator.java rename to structurizr-core/src/main/java/com/structurizr/model/CanonicalNameGenerator.java diff --git a/structurizr-core/src/com/structurizr/model/Component.java b/structurizr-core/src/main/java/com/structurizr/model/Component.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Component.java rename to structurizr-core/src/main/java/com/structurizr/model/Component.java diff --git a/structurizr-core/src/com/structurizr/model/Constants.java b/structurizr-core/src/main/java/com/structurizr/model/Constants.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Constants.java rename to structurizr-core/src/main/java/com/structurizr/model/Constants.java diff --git a/structurizr-core/src/com/structurizr/model/Container.java b/structurizr-core/src/main/java/com/structurizr/model/Container.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Container.java rename to structurizr-core/src/main/java/com/structurizr/model/Container.java diff --git a/structurizr-core/src/com/structurizr/model/ContainerInstance.java b/structurizr-core/src/main/java/com/structurizr/model/ContainerInstance.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/ContainerInstance.java rename to structurizr-core/src/main/java/com/structurizr/model/ContainerInstance.java diff --git a/structurizr-core/src/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java rename to structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java diff --git a/structurizr-core/src/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java rename to structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java diff --git a/structurizr-core/src/com/structurizr/model/CustomElement.java b/structurizr-core/src/main/java/com/structurizr/model/CustomElement.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/CustomElement.java rename to structurizr-core/src/main/java/com/structurizr/model/CustomElement.java diff --git a/structurizr-core/src/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java rename to structurizr-core/src/main/java/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java diff --git a/structurizr-core/src/com/structurizr/model/DeploymentElement.java b/structurizr-core/src/main/java/com/structurizr/model/DeploymentElement.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/DeploymentElement.java rename to structurizr-core/src/main/java/com/structurizr/model/DeploymentElement.java diff --git a/structurizr-core/src/com/structurizr/model/DeploymentNode.java b/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java similarity index 98% rename from structurizr-core/src/com/structurizr/model/DeploymentNode.java rename to structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java index abdd8308f..e40aaeac5 100644 --- a/structurizr-core/src/com/structurizr/model/DeploymentNode.java +++ b/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java @@ -330,6 +330,16 @@ void setInfrastructureNodes(Set infrastructureNodes) { } } + /** + * Determines whether this deployment node has any infrastructure nodes. + * + * @return true if it has infrastructure nodes, false otherwise + */ + @JsonIgnore + public boolean hasInfrastructureNodes() { + return !infrastructureNodes.isEmpty(); + } + /** * Determines whether this deployment node has any child deployment nodes. * diff --git a/structurizr-core/src/com/structurizr/model/Element.java b/structurizr-core/src/main/java/com/structurizr/model/Element.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Element.java rename to structurizr-core/src/main/java/com/structurizr/model/Element.java diff --git a/structurizr-core/src/com/structurizr/model/Enterprise.java b/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Enterprise.java rename to structurizr-core/src/main/java/com/structurizr/model/Enterprise.java diff --git a/structurizr-core/src/com/structurizr/model/GroupableElement.java b/structurizr-core/src/main/java/com/structurizr/model/GroupableElement.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/GroupableElement.java rename to structurizr-core/src/main/java/com/structurizr/model/GroupableElement.java diff --git a/structurizr-core/src/com/structurizr/model/HttpHealthCheck.java b/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/HttpHealthCheck.java rename to structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java diff --git a/structurizr-core/src/com/structurizr/model/IdGenerator.java b/structurizr-core/src/main/java/com/structurizr/model/IdGenerator.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/IdGenerator.java rename to structurizr-core/src/main/java/com/structurizr/model/IdGenerator.java diff --git a/structurizr-core/src/com/structurizr/model/ImpliedRelationshipsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/ImpliedRelationshipsStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/ImpliedRelationshipsStrategy.java rename to structurizr-core/src/main/java/com/structurizr/model/ImpliedRelationshipsStrategy.java diff --git a/structurizr-core/src/com/structurizr/model/InfrastructureNode.java b/structurizr-core/src/main/java/com/structurizr/model/InfrastructureNode.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/InfrastructureNode.java rename to structurizr-core/src/main/java/com/structurizr/model/InfrastructureNode.java diff --git a/structurizr-core/src/com/structurizr/model/InteractionStyle.java b/structurizr-core/src/main/java/com/structurizr/model/InteractionStyle.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/InteractionStyle.java rename to structurizr-core/src/main/java/com/structurizr/model/InteractionStyle.java diff --git a/structurizr-core/src/com/structurizr/model/Location.java b/structurizr-core/src/main/java/com/structurizr/model/Location.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Location.java rename to structurizr-core/src/main/java/com/structurizr/model/Location.java diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Model.java rename to structurizr-core/src/main/java/com/structurizr/model/Model.java diff --git a/structurizr-core/src/com/structurizr/model/ModelItem.java b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/ModelItem.java rename to structurizr-core/src/main/java/com/structurizr/model/ModelItem.java diff --git a/structurizr-core/src/com/structurizr/model/Person.java b/structurizr-core/src/main/java/com/structurizr/model/Person.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Person.java rename to structurizr-core/src/main/java/com/structurizr/model/Person.java diff --git a/structurizr-core/src/com/structurizr/model/Perspective.java b/structurizr-core/src/main/java/com/structurizr/model/Perspective.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Perspective.java rename to structurizr-core/src/main/java/com/structurizr/model/Perspective.java diff --git a/structurizr-core/src/com/structurizr/model/Relationship.java b/structurizr-core/src/main/java/com/structurizr/model/Relationship.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Relationship.java rename to structurizr-core/src/main/java/com/structurizr/model/Relationship.java diff --git a/structurizr-core/src/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java rename to structurizr-core/src/main/java/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java diff --git a/structurizr-core/src/com/structurizr/model/SoftwareSystem.java b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/SoftwareSystem.java rename to structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java diff --git a/structurizr-core/src/com/structurizr/model/SoftwareSystemInstance.java b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystemInstance.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/SoftwareSystemInstance.java rename to structurizr-core/src/main/java/com/structurizr/model/SoftwareSystemInstance.java diff --git a/structurizr-core/src/com/structurizr/model/StaticStructureElement.java b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElement.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/StaticStructureElement.java rename to structurizr-core/src/main/java/com/structurizr/model/StaticStructureElement.java diff --git a/structurizr-core/src/com/structurizr/model/StaticStructureElementInstance.java b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/StaticStructureElementInstance.java rename to structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java diff --git a/structurizr-core/src/com/structurizr/model/Tags.java b/structurizr-core/src/main/java/com/structurizr/model/Tags.java similarity index 100% rename from structurizr-core/src/com/structurizr/model/Tags.java rename to structurizr-core/src/main/java/com/structurizr/model/Tags.java diff --git a/structurizr-core/src/com/structurizr/util/ImageUtils.java b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java similarity index 100% rename from structurizr-core/src/com/structurizr/util/ImageUtils.java rename to structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java diff --git a/structurizr-core/src/com/structurizr/util/MapUtils.java b/structurizr-core/src/main/java/com/structurizr/util/MapUtils.java similarity index 100% rename from structurizr-core/src/com/structurizr/util/MapUtils.java rename to structurizr-core/src/main/java/com/structurizr/util/MapUtils.java diff --git a/structurizr-core/src/com/structurizr/util/StringUtils.java b/structurizr-core/src/main/java/com/structurizr/util/StringUtils.java similarity index 100% rename from structurizr-core/src/com/structurizr/util/StringUtils.java rename to structurizr-core/src/main/java/com/structurizr/util/StringUtils.java diff --git a/structurizr-core/src/com/structurizr/util/TagUtils.java b/structurizr-core/src/main/java/com/structurizr/util/TagUtils.java similarity index 100% rename from structurizr-core/src/com/structurizr/util/TagUtils.java rename to structurizr-core/src/main/java/com/structurizr/util/TagUtils.java diff --git a/structurizr-core/src/com/structurizr/util/Url.java b/structurizr-core/src/main/java/com/structurizr/util/Url.java similarity index 100% rename from structurizr-core/src/com/structurizr/util/Url.java rename to structurizr-core/src/main/java/com/structurizr/util/Url.java diff --git a/structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java similarity index 100% rename from structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java rename to structurizr-core/src/main/java/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java diff --git a/structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java similarity index 100% rename from structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java rename to structurizr-core/src/main/java/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java diff --git a/structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java similarity index 100% rename from structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java rename to structurizr-core/src/main/java/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java b/structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidationException.java similarity index 100% rename from structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java rename to structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidationException.java diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidator.java similarity index 100% rename from structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java rename to structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidator.java diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java b/structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidatorFactory.java similarity index 100% rename from structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java rename to structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidatorFactory.java diff --git a/structurizr-core/src/com/structurizr/view/AbstractStyle.java b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/AbstractStyle.java rename to structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java diff --git a/structurizr-core/src/com/structurizr/view/AnimatedView.java b/structurizr-core/src/main/java/com/structurizr/view/AnimatedView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/AnimatedView.java rename to structurizr-core/src/main/java/com/structurizr/view/AnimatedView.java diff --git a/structurizr-core/src/com/structurizr/view/Animation.java b/structurizr-core/src/main/java/com/structurizr/view/Animation.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Animation.java rename to structurizr-core/src/main/java/com/structurizr/view/Animation.java diff --git a/structurizr-core/src/com/structurizr/view/AutomaticLayout.java b/structurizr-core/src/main/java/com/structurizr/view/AutomaticLayout.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/AutomaticLayout.java rename to structurizr-core/src/main/java/com/structurizr/view/AutomaticLayout.java diff --git a/structurizr-core/src/com/structurizr/view/Border.java b/structurizr-core/src/main/java/com/structurizr/view/Border.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Border.java rename to structurizr-core/src/main/java/com/structurizr/view/Border.java diff --git a/structurizr-core/src/com/structurizr/view/Branding.java b/structurizr-core/src/main/java/com/structurizr/view/Branding.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Branding.java rename to structurizr-core/src/main/java/com/structurizr/view/Branding.java diff --git a/structurizr-core/src/com/structurizr/view/Color.java b/structurizr-core/src/main/java/com/structurizr/view/Color.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Color.java rename to structurizr-core/src/main/java/com/structurizr/view/Color.java diff --git a/structurizr-core/src/com/structurizr/view/ComponentView.java b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ComponentView.java rename to structurizr-core/src/main/java/com/structurizr/view/ComponentView.java diff --git a/structurizr-core/src/com/structurizr/view/Configuration.java b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Configuration.java rename to structurizr-core/src/main/java/com/structurizr/view/Configuration.java diff --git a/structurizr-core/src/com/structurizr/view/ContainerView.java b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ContainerView.java rename to structurizr-core/src/main/java/com/structurizr/view/ContainerView.java diff --git a/structurizr-core/src/com/structurizr/view/CustomView.java b/structurizr-core/src/main/java/com/structurizr/view/CustomView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/CustomView.java rename to structurizr-core/src/main/java/com/structurizr/view/CustomView.java diff --git a/structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java b/structurizr-core/src/main/java/com/structurizr/view/DefaultLayoutMergeStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/DefaultLayoutMergeStrategy.java rename to structurizr-core/src/main/java/com/structurizr/view/DefaultLayoutMergeStrategy.java diff --git a/structurizr-core/src/com/structurizr/view/DeploymentView.java b/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/DeploymentView.java rename to structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java diff --git a/structurizr-core/src/com/structurizr/view/Dimensions.java b/structurizr-core/src/main/java/com/structurizr/view/Dimensions.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Dimensions.java rename to structurizr-core/src/main/java/com/structurizr/view/Dimensions.java diff --git a/structurizr-core/src/com/structurizr/view/DynamicView.java b/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/DynamicView.java rename to structurizr-core/src/main/java/com/structurizr/view/DynamicView.java diff --git a/structurizr-core/src/com/structurizr/view/ElementNotPermittedInViewException.java b/structurizr-core/src/main/java/com/structurizr/view/ElementNotPermittedInViewException.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ElementNotPermittedInViewException.java rename to structurizr-core/src/main/java/com/structurizr/view/ElementNotPermittedInViewException.java diff --git a/structurizr-core/src/com/structurizr/view/ElementStyle.java b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ElementStyle.java rename to structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java diff --git a/structurizr-core/src/com/structurizr/view/ElementView.java b/structurizr-core/src/main/java/com/structurizr/view/ElementView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ElementView.java rename to structurizr-core/src/main/java/com/structurizr/view/ElementView.java diff --git a/structurizr-core/src/com/structurizr/view/FilterMode.java b/structurizr-core/src/main/java/com/structurizr/view/FilterMode.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/FilterMode.java rename to structurizr-core/src/main/java/com/structurizr/view/FilterMode.java diff --git a/structurizr-core/src/com/structurizr/view/FilteredView.java b/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/FilteredView.java rename to structurizr-core/src/main/java/com/structurizr/view/FilteredView.java diff --git a/structurizr-core/src/com/structurizr/view/Font.java b/structurizr-core/src/main/java/com/structurizr/view/Font.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Font.java rename to structurizr-core/src/main/java/com/structurizr/view/Font.java diff --git a/structurizr-core/src/com/structurizr/view/ImageView.java b/structurizr-core/src/main/java/com/structurizr/view/ImageView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ImageView.java rename to structurizr-core/src/main/java/com/structurizr/view/ImageView.java diff --git a/structurizr-core/src/com/structurizr/view/LayoutMergeStrategy.java b/structurizr-core/src/main/java/com/structurizr/view/LayoutMergeStrategy.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/LayoutMergeStrategy.java rename to structurizr-core/src/main/java/com/structurizr/view/LayoutMergeStrategy.java diff --git a/structurizr-core/src/com/structurizr/view/LineStyle.java b/structurizr-core/src/main/java/com/structurizr/view/LineStyle.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/LineStyle.java rename to structurizr-core/src/main/java/com/structurizr/view/LineStyle.java diff --git a/structurizr-core/src/com/structurizr/view/MetadataSymbols.java b/structurizr-core/src/main/java/com/structurizr/view/MetadataSymbols.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/MetadataSymbols.java rename to structurizr-core/src/main/java/com/structurizr/view/MetadataSymbols.java diff --git a/structurizr-core/src/com/structurizr/view/ModelView.java b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ModelView.java rename to structurizr-core/src/main/java/com/structurizr/view/ModelView.java diff --git a/structurizr-core/src/com/structurizr/view/PaperSize.java b/structurizr-core/src/main/java/com/structurizr/view/PaperSize.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/PaperSize.java rename to structurizr-core/src/main/java/com/structurizr/view/PaperSize.java diff --git a/structurizr-core/src/com/structurizr/view/ParallelSequenceCounter.java b/structurizr-core/src/main/java/com/structurizr/view/ParallelSequenceCounter.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ParallelSequenceCounter.java rename to structurizr-core/src/main/java/com/structurizr/view/ParallelSequenceCounter.java diff --git a/structurizr-core/src/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/RelationshipStyle.java rename to structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java diff --git a/structurizr-core/src/com/structurizr/view/RelationshipView.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/RelationshipView.java rename to structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java diff --git a/structurizr-core/src/com/structurizr/view/Routing.java b/structurizr-core/src/main/java/com/structurizr/view/Routing.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Routing.java rename to structurizr-core/src/main/java/com/structurizr/view/Routing.java diff --git a/structurizr-core/src/com/structurizr/view/SequenceCounter.java b/structurizr-core/src/main/java/com/structurizr/view/SequenceCounter.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/SequenceCounter.java rename to structurizr-core/src/main/java/com/structurizr/view/SequenceCounter.java diff --git a/structurizr-core/src/com/structurizr/view/SequenceNumber.java b/structurizr-core/src/main/java/com/structurizr/view/SequenceNumber.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/SequenceNumber.java rename to structurizr-core/src/main/java/com/structurizr/view/SequenceNumber.java diff --git a/structurizr-core/src/com/structurizr/view/Shape.java b/structurizr-core/src/main/java/com/structurizr/view/Shape.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Shape.java rename to structurizr-core/src/main/java/com/structurizr/view/Shape.java diff --git a/structurizr-core/src/com/structurizr/view/StaticView.java b/structurizr-core/src/main/java/com/structurizr/view/StaticView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/StaticView.java rename to structurizr-core/src/main/java/com/structurizr/view/StaticView.java diff --git a/structurizr-core/src/com/structurizr/view/Styles.java b/structurizr-core/src/main/java/com/structurizr/view/Styles.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Styles.java rename to structurizr-core/src/main/java/com/structurizr/view/Styles.java diff --git a/structurizr-core/src/com/structurizr/view/SystemContextView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/SystemContextView.java rename to structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java diff --git a/structurizr-core/src/com/structurizr/view/SystemLandscapeView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/SystemLandscapeView.java rename to structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java diff --git a/structurizr-core/src/com/structurizr/view/Terminology.java b/structurizr-core/src/main/java/com/structurizr/view/Terminology.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Terminology.java rename to structurizr-core/src/main/java/com/structurizr/view/Terminology.java diff --git a/structurizr-core/src/com/structurizr/view/Theme.java b/structurizr-core/src/main/java/com/structurizr/view/Theme.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Theme.java rename to structurizr-core/src/main/java/com/structurizr/view/Theme.java diff --git a/structurizr-core/src/com/structurizr/view/Vertex.java b/structurizr-core/src/main/java/com/structurizr/view/Vertex.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/Vertex.java rename to structurizr-core/src/main/java/com/structurizr/view/Vertex.java diff --git a/structurizr-core/src/com/structurizr/view/View.java b/structurizr-core/src/main/java/com/structurizr/view/View.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/View.java rename to structurizr-core/src/main/java/com/structurizr/view/View.java diff --git a/structurizr-core/src/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ViewSet.java rename to structurizr-core/src/main/java/com/structurizr/view/ViewSet.java diff --git a/structurizr-core/src/com/structurizr/view/ViewSortOrder.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSortOrder.java similarity index 100% rename from structurizr-core/src/com/structurizr/view/ViewSortOrder.java rename to structurizr-core/src/main/java/com/structurizr/view/ViewSortOrder.java diff --git a/structurizr-core/test/unit/com/structurizr/AbstractWorkspaceTestBase.java b/structurizr-core/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/AbstractWorkspaceTestBase.java rename to structurizr-core/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/WorkspaceTests.java rename to structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java diff --git a/structurizr-core/test/unit/com/structurizr/configuration/UserTests.java b/structurizr-core/src/test/java/com/structurizr/configuration/UserTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/configuration/UserTests.java rename to structurizr-core/src/test/java/com/structurizr/configuration/UserTests.java diff --git a/structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/src/test/java/com/structurizr/configuration/WorkspaceConfigurationTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/configuration/WorkspaceConfigurationTests.java rename to structurizr-core/src/test/java/com/structurizr/configuration/WorkspaceConfigurationTests.java diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java b/structurizr-core/src/test/java/com/structurizr/documentation/DecisionTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/DecisionTests.java rename to structurizr-core/src/test/java/com/structurizr/documentation/DecisionTests.java diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java b/structurizr-core/src/test/java/com/structurizr/documentation/DocumentationTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/DocumentationTests.java rename to structurizr-core/src/test/java/com/structurizr/documentation/DocumentationTests.java diff --git a/structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java b/structurizr-core/src/test/java/com/structurizr/documentation/SectionTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/SectionTests.java rename to structurizr-core/src/test/java/com/structurizr/documentation/SectionTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java b/structurizr-core/src/test/java/com/structurizr/model/ComponentTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/ComponentTests.java rename to structurizr-core/src/test/java/com/structurizr/model/ComponentTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java b/structurizr-core/src/test/java/com/structurizr/model/ContainerInstanceTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java rename to structurizr-core/src/test/java/com/structurizr/model/ContainerInstanceTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java b/structurizr-core/src/test/java/com/structurizr/model/ContainerTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/ContainerTests.java rename to structurizr-core/src/test/java/com/structurizr/model/ContainerTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java rename to structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java rename to structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java b/structurizr-core/src/test/java/com/structurizr/model/CustomElementTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/CustomElementTests.java rename to structurizr-core/src/test/java/com/structurizr/model/CustomElementTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java rename to structurizr-core/src/test/java/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java b/structurizr-core/src/test/java/com/structurizr/model/DeploymentNodeTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java rename to structurizr-core/src/test/java/com/structurizr/model/DeploymentNodeTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java b/structurizr-core/src/test/java/com/structurizr/model/ElementTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/ElementTests.java rename to structurizr-core/src/test/java/com/structurizr/model/ElementTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java b/structurizr-core/src/test/java/com/structurizr/model/GroupableElementTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/GroupableElementTests.java rename to structurizr-core/src/test/java/com/structurizr/model/GroupableElementTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java b/structurizr-core/src/test/java/com/structurizr/model/HttpHealthCheckTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/HttpHealthCheckTests.java rename to structurizr-core/src/test/java/com/structurizr/model/HttpHealthCheckTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java b/structurizr-core/src/test/java/com/structurizr/model/InfrastructureNodeTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/InfrastructureNodeTests.java rename to structurizr-core/src/test/java/com/structurizr/model/InfrastructureNodeTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java b/structurizr-core/src/test/java/com/structurizr/model/ModelItemTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/ModelItemTests.java rename to structurizr-core/src/test/java/com/structurizr/model/ModelItemTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java b/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/ModelTests.java rename to structurizr-core/src/test/java/com/structurizr/model/ModelTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/PersonTests.java b/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/PersonTests.java rename to structurizr-core/src/test/java/com/structurizr/model/PersonTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java b/structurizr-core/src/test/java/com/structurizr/model/RelationshipTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java rename to structurizr-core/src/test/java/com/structurizr/model/RelationshipTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java b/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemInstanceTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/SoftwareSystemInstanceTests.java rename to structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemInstanceTests.java diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java b/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java rename to structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemTests.java diff --git a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java similarity index 96% rename from structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java rename to structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java index 81892cd78..03f9c23dd 100644 --- a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java @@ -52,7 +52,7 @@ void getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() th @Test void getContentType_ReturnsTheContentType_WhenAFileIsSpecified() throws Exception { - String contentType = ImageUtils.getContentType(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); + String contentType = ImageUtils.getContentType(new File("./src/test/resources/structurizr-logo.png")); assertEquals("image/png", contentType); } @@ -134,8 +134,8 @@ void getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() @Test void getImageAsBase64_ReturnsTheImageAsABase64EncodedString_WhenAFileIsSpecified() throws Exception { - String imageAsBase64 = ImageUtils.getImageAsBase64(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); - assertTrue(imageAsBase64.startsWith("iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADApo5rAAA")); // the actual base64 encoded string varies between Java 8 and 9 + String imageAsBase64 = ImageUtils.getImageAsBase64(new File("./src/test/resources/structurizr-logo.png")); + assertTrue(imageAsBase64.startsWith("iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADApo5rAAA")); } @Test @@ -182,7 +182,7 @@ void getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() @Test void getImageAsDataUri_ReturnsTheImageAsADataUri_WhenAFileIsSpecified() throws Exception { - String imageAsDataUri = ImageUtils.getImageAsDataUri(new File("../structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png")); + String imageAsDataUri = ImageUtils.getImageAsDataUri(new File("./src/test/resources/structurizr-logo.png")); System.out.println(imageAsDataUri); assertTrue(imageAsDataUri.startsWith("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADA")); // the actual base64 encoded string varies between Java 8 and 9 } diff --git a/structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java b/structurizr-core/src/test/java/com/structurizr/util/StringUtilsTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/util/StringUtilsTests.java rename to structurizr-core/src/test/java/com/structurizr/util/StringUtilsTests.java diff --git a/structurizr-core/test/unit/com/structurizr/util/UrlTests.java b/structurizr-core/src/test/java/com/structurizr/util/UrlTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/util/UrlTests.java rename to structurizr-core/src/test/java/com/structurizr/util/UrlTests.java diff --git a/structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java b/structurizr-core/src/test/java/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java rename to structurizr-core/src/test/java/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java diff --git a/structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java b/structurizr-core/src/test/java/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java rename to structurizr-core/src/test/java/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java diff --git a/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java b/structurizr-core/src/test/java/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java rename to structurizr-core/src/test/java/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java b/structurizr-core/src/test/java/com/structurizr/view/AutomaticLayoutTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/AutomaticLayoutTests.java rename to structurizr-core/src/test/java/com/structurizr/view/AutomaticLayoutTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/BrandingTests.java b/structurizr-core/src/test/java/com/structurizr/view/BrandingTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/BrandingTests.java rename to structurizr-core/src/test/java/com/structurizr/view/BrandingTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java b/structurizr-core/src/test/java/com/structurizr/view/ColorTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ColorTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ColorTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java b/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/view/DefaultLayoutMergeStrategyTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/DefaultLayoutMergeStrategyTests.java rename to structurizr-core/src/test/java/com/structurizr/view/DefaultLayoutMergeStrategyTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/DeploymentViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java b/structurizr-core/src/test/java/com/structurizr/view/DimensionsTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/DimensionsTests.java rename to structurizr-core/src/test/java/com/structurizr/view/DimensionsTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java b/structurizr-core/src/test/java/com/structurizr/view/ElementStyleTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ElementStyleTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ElementViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ElementViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/FilteredViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/FilteredViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/FontTests.java b/structurizr-core/src/test/java/com/structurizr/view/FontTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/FontTests.java rename to structurizr-core/src/test/java/com/structurizr/view/FontTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ImageViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ImageViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java b/structurizr-core/src/test/java/com/structurizr/view/PaperSizeTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/PaperSizeTests.java rename to structurizr-core/src/test/java/com/structurizr/view/PaperSizeTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java b/structurizr-core/src/test/java/com/structurizr/view/RelationshipStyleTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java rename to structurizr-core/src/test/java/com/structurizr/view/RelationshipStyleTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java b/structurizr-core/src/test/java/com/structurizr/view/SequenceCounterTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java rename to structurizr-core/src/test/java/com/structurizr/view/SequenceCounterTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java b/structurizr-core/src/test/java/com/structurizr/view/SequenceNumberTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java rename to structurizr-core/src/test/java/com/structurizr/view/SequenceNumberTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/StaticViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/StaticViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/StaticViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/StylesTests.java b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/StylesTests.java rename to structurizr-core/src/test/java/com/structurizr/view/StylesTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/SystemLandscapeViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java b/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/TerminologyTests.java rename to structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/VertexTests.java b/structurizr-core/src/test/java/com/structurizr/view/VertexTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/VertexTests.java rename to structurizr-core/src/test/java/com/structurizr/view/VertexTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/view/ViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/ViewTests.java diff --git a/structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png b/structurizr-core/src/test/resources/structurizr-logo.png similarity index 100% rename from structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png rename to structurizr-core/src/test/resources/structurizr-logo.png diff --git a/structurizr-dsl/README.md b/structurizr-dsl/README.md new file mode 100644 index 000000000..ae9e61d84 --- /dev/null +++ b/structurizr-dsl/README.md @@ -0,0 +1,11 @@ +# Structurizr DSL + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-dsl.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-dsl) + +This GitHub repository contains an implementation of the Structurizr DSL - a way to create Structurizr software +architecture models based upon the [C4 model](https://c4model.com) using a textual domain specific language (DSL). +The Structurizr DSL has appeared on the +[ThoughtWorks Tech Radar - Techniques - Diagrams as code](https://www.thoughtworks.com/radar/techniques/diagrams-as-code) +and is text-based wrapper around the [Structurizr for Java library](https://github.com/structurizr/java). + +- [Documentation](https://docs.structurizr.com/dsl) diff --git a/structurizr-dsl/build.gradle b/structurizr-dsl/build.gradle new file mode 100644 index 000000000..3416670df --- /dev/null +++ b/structurizr-dsl/build.gradle @@ -0,0 +1,14 @@ +dependencies { + + api project(':structurizr-client') + api project(':structurizr-import') + + testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.19' + testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.8.10' + testImplementation 'org.jruby:jruby-core:9.4.4.0' + + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' +} + +description = 'Structurizr DSL' \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java new file mode 100644 index 000000000..07f7a36c1 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java @@ -0,0 +1,344 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.ModelItem; +import com.structurizr.model.Relationship; +import com.structurizr.model.StaticStructureElementInstance; +import com.structurizr.util.StringUtils; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.structurizr.dsl.StructurizrDslExpressions.*; + +abstract class AbstractExpressionParser { + + private static final String WILDCARD = "*"; + + static boolean isExpression(String token) { + token = token.toLowerCase(); + + return + token.startsWith(ELEMENT_TYPE_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_TAG_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_PARENT_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP) || token.endsWith(RELATIONSHIP) || token.contains(RELATIONSHIP) || + token.startsWith(ELEMENT_EQUALS_EXPRESSION) || + token.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP_EQUALS_EXPRESSION); + } + + + final Set parseExpression(String expr, DslContext context) { + if (expr.contains(" && ")) { + String[] expressions = expr.split(" && "); + Set modelItems1 = evaluateExpression(expressions[0], context); + Set modelItems2 = evaluateExpression(expressions[1], context); + + Set modelItems = new HashSet<>(modelItems1); + modelItems.retainAll(modelItems2); + + return modelItems; + } else if (expr.contains(" || ")) { + String[] expressions = expr.split(" \\|\\| "); + Set modelItems1 = evaluateExpression(expressions[0], context); + Set modelItems2 = evaluateExpression(expressions[1], context); + + Set elements = new HashSet<>(modelItems1); + elements.addAll(modelItems2); + + return elements; + } else { + return evaluateExpression(expr, context); + } + } + + private Set evaluateExpression(String expr, DslContext context) { + Set modelItems = new LinkedHashSet<>(); + + if (expr.startsWith(ELEMENT_EQUALS_EXPRESSION)) { + expr = expr.substring(ELEMENT_EQUALS_EXPRESSION.length()); + + if (isExpression(expr)) { + modelItems.addAll(evaluateExpression(expr, context)); + } else { + modelItems.addAll(parseIdentifier(expr, context)); + } + } else if (expr.startsWith(RELATIONSHIP_EQUALS_EXPRESSION)) { + expr = expr.substring(RELATIONSHIP_EQUALS_EXPRESSION.length()); + + if (WILDCARD.equals(expr)) { + expr = WILDCARD + RELATIONSHIP + WILDCARD; + } + + if (isExpression(expr)) { + modelItems.addAll(evaluateExpression(expr, context)); + } else { + modelItems.addAll(parseIdentifier(expr, context)); + } + } else if (RELATIONSHIP.equals(expr)) { + throw new RuntimeException("Unexpected identifier \"->\""); + } else if (expr.startsWith(RELATIONSHIP) || expr.endsWith(RELATIONSHIP)) { + // this is an element expression: ->identifier identifier-> ->identifier-> + boolean includeAfferentCouplings = false; + boolean includeEfferentCouplings = false; + + String identifier = expr; + + if (identifier.startsWith(RELATIONSHIP)) { + includeAfferentCouplings = true; + identifier = identifier.substring(RELATIONSHIP.length()); + } + if (identifier.endsWith(RELATIONSHIP)) { + includeEfferentCouplings = true; + identifier = identifier.substring(0, identifier.length() - RELATIONSHIP.length()); + } + + identifier = identifier.trim(); + Set elements; + + if (isExpression(identifier)) { + elements = parseExpression(identifier, context).stream().filter(mi -> mi instanceof Element).map(mi -> (Element)mi).collect(Collectors.toSet()); + } else { + elements = getElements(identifier, context); + } + + if (elements.isEmpty()) { + throw new RuntimeException("The element \"" + identifier + "\" does not exist"); + } + + for (Element element : elements) { + modelItems.add(element); + + if (includeAfferentCouplings) { + modelItems.addAll(findAfferentCouplings(element)); + } + + if (includeEfferentCouplings) { + modelItems.addAll(findEfferentCouplings(element)); + } + } + } else if (expr.contains(RELATIONSHIP)) { + String[] identifiers = expr.split(RELATIONSHIP); + String sourceIdentifier = identifiers[0].trim(); + String destinationIdentifier = identifiers[1].trim(); + + String sourceExpression = RELATIONSHIP_SOURCE_EQUALS_EXPRESSION + sourceIdentifier; + String destinationExpression = RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION + destinationIdentifier; + + if (WILDCARD.equals(sourceIdentifier) && WILDCARD.equals(destinationIdentifier)) { + modelItems.addAll(context.getWorkspace().getModel().getRelationships()); + } else if (WILDCARD.equals(destinationIdentifier)) { + modelItems.addAll(parseExpression(sourceExpression, context)); + } else if (WILDCARD.equals(sourceIdentifier)) { + modelItems.addAll(parseExpression(destinationExpression, context)); + } else { + modelItems.addAll(parseExpression(sourceExpression + " && " + destinationExpression, context)); + } + } else if (expr.toLowerCase().startsWith(ELEMENT_PARENT_EQUALS_EXPRESSION)) { + String parentIdentifier = expr.substring(ELEMENT_PARENT_EQUALS_EXPRESSION.length()); + Element parentElement = context.getElement(parentIdentifier); + if (parentElement == null) { + throw new RuntimeException("The parent element \"" + parentIdentifier + "\" does not exist"); + } else { + context.getWorkspace().getModel().getElements().forEach(element -> { + if (element.getParent() == parentElement) { + modelItems.add(element); + } + }); + } + } else if (expr.toLowerCase().startsWith(ELEMENT_TYPE_EQUALS_EXPRESSION)) { + modelItems.addAll(evaluateElementTypeExpression(expr, context)); + } else if (expr.toLowerCase().startsWith(ELEMENT_TAG_EQUALS_EXPRESSION.toLowerCase())) { + String[] tags = expr.substring(ELEMENT_TAG_EQUALS_EXPRESSION.length()).split(","); + context.getWorkspace().getModel().getElements().forEach(element -> { + if (hasAllTags(element, tags)) { + modelItems.add(element); + } + }); + } else if (expr.toLowerCase().startsWith(ELEMENT_TAG_NOT_EQUALS_EXPRESSION)) { + String[] tags = expr.substring(ELEMENT_TAG_NOT_EQUALS_EXPRESSION.length()).split(","); + context.getWorkspace().getModel().getElements().forEach(element -> { + if (!hasAllTags(element, tags)) { + modelItems.add(element); + } + }); + } else if (expr.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION)) { + String[] tags = expr.substring(RELATIONSHIP_TAG_EQUALS_EXPRESSION.length()).split(","); + context.getWorkspace().getModel().getRelationships().forEach(relationship -> { + if (hasAllTags(relationship, tags)) { + modelItems.add(relationship); + } + }); + } else if (expr.startsWith(RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION)) { + String[] tags = expr.substring(RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION.length()).split(","); + context.getWorkspace().getModel().getRelationships().forEach(relationship -> { + if (!hasAllTags(relationship, tags)) { + modelItems.add(relationship); + } + }); + } else if (expr.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION)) { + String identifier = expr.substring(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.length()); + Set sourceElements = new HashSet<>(); + + if (isExpression(identifier)) { + Set set = parseExpression(identifier, context); + for (ModelItem modelItem : set) { + if (modelItem instanceof Element) { + sourceElements.add((Element)modelItem); + } + } + } else { + Element source = context.getElement(identifier); + if (source == null) { + throw new RuntimeException("The element \"" + identifier + "\" does not exist"); + } + + if (source instanceof ElementGroup) { + sourceElements.addAll(((ElementGroup) source).getElements()); + } else { + sourceElements.add(source); + } + } + + context.getWorkspace().getModel().getRelationships().forEach(relationship -> { + if (sourceElements.contains(relationship.getSource())) { + modelItems.add(relationship); + } + }); + } else if (expr.startsWith(RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION)) { + String identifier = expr.substring(RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION.length()); + Set destinationElements = new HashSet<>(); + + if (isExpression(identifier)) { + Set set = parseExpression(identifier, context); + for (ModelItem modelItem : set) { + if (modelItem instanceof Element) { + destinationElements.add((Element)modelItem); + } + } + } else { + Element destination = context.getElement(identifier); + if (destination == null) { + throw new RuntimeException("The element \"" + identifier + "\" does not exist"); + } + + if (destination instanceof ElementGroup) { + destinationElements.addAll(((ElementGroup) destination).getElements()); + } else { + destinationElements.add(destination); + } + } + + context.getWorkspace().getModel().getRelationships().forEach(relationship -> { + if (destinationElements.contains(relationship.getDestination())) { + modelItems.add(relationship); + } + }); + } + + return modelItems; + } + + protected abstract Set evaluateElementTypeExpression(String expr, DslContext context); + + private boolean hasAllTags(ModelItem modelItem, String[] tags) { + boolean result = true; + + for (String tag : tags) { + boolean hasTag = modelItem.hasTag(tag.trim()); + + if (!hasTag) { + // perhaps the tag is instead on a related model item? + if (modelItem instanceof StaticStructureElementInstance) { + StaticStructureElementInstance elementInstance = (StaticStructureElementInstance)modelItem; + hasTag = elementInstance.getElement().hasTag(tag.trim()); + } else if (modelItem instanceof Relationship) { + Relationship relationship = (Relationship)modelItem; + if (!StringUtils.isNullOrEmpty(relationship.getLinkedRelationshipId())) { + Relationship linkedRelationship = relationship.getModel().getRelationship(relationship.getLinkedRelationshipId()); + if (linkedRelationship != null) { + hasTag = linkedRelationship.hasTag(tag.trim()); + } + } + } + } + + result = result && hasTag; + } + + return result; + } + + protected abstract Set findAfferentCouplings(Element element); + + protected Set findAfferentCouplings(Element element, Class typeOfElement) { + Set elements = new LinkedHashSet<>(); + + Set relationships = element.getModel().getRelationships(); + relationships.stream().filter(r -> r.getDestination().equals(element) && typeOfElement.isInstance(r.getSource())) + .map(Relationship::getSource) + .forEach(elements::add); + + return elements; + } + + protected abstract Set findEfferentCouplings(Element element); + + protected Set findEfferentCouplings(Element element, Class typeOfElement) { + Set elements = new LinkedHashSet<>(); + + Set relationships = element.getModel().getRelationships(); + relationships.stream().filter(r -> r.getSource().equals(element) && typeOfElement.isInstance(r.getDestination())) + .map(Relationship::getDestination) + .forEach(elements::add); + + return elements; + } + + protected Set parseIdentifier(String identifier, DslContext context) { + Set modelItems = new LinkedHashSet<>(); + + Element element = context.getElement(identifier); + if (element != null) { + modelItems.addAll(getElements(identifier, context)); + } + + Relationship relationship = context.getRelationship(identifier); + if (relationship != null) { + modelItems.add(relationship); + + // and also find all relationships linked to it (i.e. implied and replicated relationships) + relationship.getModel().getRelationships().stream().filter(r -> relationship.getId().equals(r.getLinkedRelationshipId())).forEach(modelItems::add); + } + + if (modelItems.isEmpty()) { + throw new RuntimeException("The element/relationship \"" + identifier + "\" does not exist"); + } else { + return modelItems; + } + } + + private Set getElements(String identifier, DslContext context) { + Set elements = new HashSet<>(); + + Element element = context.getElement(identifier); + if (element != null) { + if (element instanceof ElementGroup) { + ElementGroup group = (ElementGroup) element; + elements.addAll(group.getElements()); + } else { + elements.add(element); + } + } + + return elements; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java new file mode 100644 index 000000000..01bfcf4c0 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java @@ -0,0 +1,42 @@ +package com.structurizr.dsl; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +import java.util.regex.Pattern; + +abstract class AbstractParser { + + private static final int HTTP_OK_STATUS = 200; + + private static final Pattern VIEW_KEY_PATTERN = Pattern.compile("[\\w-]+"); + + void validateViewKey(String key) { + if (!VIEW_KEY_PATTERN.matcher(key).matches()) { + throw new RuntimeException("View keys can only contain the following characters: a-zA-0-9_-"); + } + } + + String removeNonWordCharacters(String name) { + return name.replaceAll("\\W", ""); + } + + protected String readFromUrl(String url) { + try (CloseableHttpClient httpClient = HttpClients.createSystem()) { + HttpGet httpGet = new HttpGet(url); + CloseableHttpResponse response = httpClient.execute(httpGet); + + if (response.getCode() == HTTP_OK_STATUS) { + return EntityUtils.toString(response.getEntity()); + } + } catch (Exception ioe) { + ioe.printStackTrace(); + } + + return ""; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java new file mode 100644 index 000000000..d8ffb28c6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java @@ -0,0 +1,37 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +abstract class AbstractRelationshipParser extends AbstractParser { + + protected Relationship createRelationship(Element sourceElement, String description, String technology, String[] tags, Element destinationElement) { + Relationship relationship = null; + + if (sourceElement instanceof CustomElement) { + relationship = ((CustomElement)sourceElement).uses(destinationElement, description, technology, null, tags); + } else if (destinationElement instanceof CustomElement) { + relationship = sourceElement.uses((CustomElement)destinationElement, description, technology, null, tags); + } else if (sourceElement instanceof StaticStructureElement && destinationElement instanceof StaticStructureElement) { + relationship = ((StaticStructureElement)sourceElement).uses((StaticStructureElement)destinationElement, description, technology, null, tags); + } else if (sourceElement instanceof DeploymentNode && destinationElement instanceof DeploymentNode) { + relationship = ((DeploymentNode)sourceElement).uses((DeploymentNode)destinationElement, description, technology, null, tags); + } else if (sourceElement instanceof DeploymentNode && destinationElement instanceof InfrastructureNode) { + relationship = ((DeploymentNode)sourceElement).uses((InfrastructureNode) destinationElement, description, technology, null, tags); + } else if (sourceElement instanceof InfrastructureNode && destinationElement instanceof DeploymentElement) { + relationship = ((InfrastructureNode)sourceElement).uses((DeploymentElement)destinationElement, description, technology, null, tags); + } else if (sourceElement instanceof StaticStructureElementInstance && destinationElement instanceof InfrastructureNode) { + relationship = ((StaticStructureElementInstance)sourceElement).uses((InfrastructureNode)destinationElement, description, technology, null, tags); + } else { + throw new RuntimeException("A relationship between \"" + sourceElement.getCanonicalName() + "\" and \"" + destinationElement.getCanonicalName() + "\" is not permitted"); + } + + if (relationship == null) { + if (sourceElement.hasEfferentRelationshipWith(destinationElement, description) || sourceElement.hasEfferentRelationshipWith(destinationElement)) { + throw new RuntimeException("A relationship between \"" + sourceElement.getCanonicalName() + "\" and \"" + destinationElement.getCanonicalName() + "\" already exists"); + } + } + + return relationship; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java new file mode 100644 index 000000000..8f66d3e9b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java @@ -0,0 +1,4 @@ +package com.structurizr.dsl; + +abstract class AbstractViewParser extends AbstractParser { +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AdrsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AdrsParser.java new file mode 100644 index 000000000..110e23462 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AdrsParser.java @@ -0,0 +1,80 @@ +package com.structurizr.dsl; + +import com.structurizr.documentation.Documentable; +import com.structurizr.importer.documentation.DefaultImageImporter; +import com.structurizr.importer.documentation.DocumentationImporter; + +import java.io.File; +import java.lang.reflect.Constructor; + +final class AdrsParser extends AbstractParser { + + private static final String DEFAULT_DECISION_IMPORTER = "com.structurizr.importer.documentation.AdrToolsDecisionImporter"; + + private static final String GRAMMAR = "!adrs "; + + private static final int PATH_INDEX = 1; + private static final int FQN_INDEX = 2; + + void parse(WorkspaceDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getWorkspace(), dslFile, tokens); + } + + void parse(SoftwareSystemDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getSoftwareSystem(), dslFile, tokens); + } + + void parse(ContainerDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getContainer(), dslFile, tokens); + } + + void parse(ComponentDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getComponent(), dslFile, tokens); + } + + private void parse(DslContext context, Documentable documentable, File dslFile, Tokens tokens) { + // !adrs + + if (tokens.hasMoreThan(FQN_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(PATH_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String fullyQualifiedClassName = DEFAULT_DECISION_IMPORTER; + if (tokens.includes(FQN_INDEX)) { + fullyQualifiedClassName = tokens.get(FQN_INDEX); + } + + if (dslFile != null) { + File path = new File(dslFile.getParentFile(), tokens.get(PATH_INDEX)); + + if (!path.exists()) { + throw new RuntimeException("Documentation path " + path + " does not exist"); + } + + if (!path.isDirectory()) { + throw new RuntimeException("Documentation path " + path + " is not a directory"); + } + + try { + Class decisionImporterClass = context.loadClass(fullyQualifiedClassName, dslFile); + Constructor constructor = decisionImporterClass.getDeclaredConstructor(); + DocumentationImporter decisionImporter = (DocumentationImporter)constructor.newInstance(); + decisionImporter.importDocumentation(documentable, path); + + if (!tokens.includes(FQN_INDEX)) { + DefaultImageImporter imageImporter = new DefaultImageImporter(); + imageImporter.importDocumentation(documentable, path); + } + } catch (ClassNotFoundException cnfe) { + throw new RuntimeException("Error importing ADRs from " + path.getAbsolutePath() + ": " + fullyQualifiedClassName + " was not found"); + } catch (Exception e) { + throw new RuntimeException("Error importing ADRs from " + path.getAbsolutePath() + ": " + e.getMessage()); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AutoLayoutParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AutoLayoutParser.java new file mode 100644 index 000000000..7a8be9e4d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AutoLayoutParser.java @@ -0,0 +1,70 @@ +package com.structurizr.dsl; + +import com.structurizr.view.AutomaticLayout; +import com.structurizr.view.ModelView; +import com.structurizr.view.View; + +import java.util.HashMap; +import java.util.Map; + +final class AutoLayoutParser extends AbstractParser { + + private static final int DEFAULT_RANK_SEPARATION = 300; + private static final int DEFAULT_NODE_SEPARATION = 300; + + private static final int RANK_DIRECTION_INDEX = 1; + private static final int RANK_SEPARATION_INDEX = 2; + private static final int NODE_SEPARATION_INDEX = 3; + + private static Map RANK_DIRECTIONS = new HashMap<>(); + + static { + RANK_DIRECTIONS.put("tb", AutomaticLayout.RankDirection.TopBottom); + RANK_DIRECTIONS.put("bt", AutomaticLayout.RankDirection.BottomTop); + RANK_DIRECTIONS.put("lr", AutomaticLayout.RankDirection.LeftRight); + RANK_DIRECTIONS.put("rl", AutomaticLayout.RankDirection.RightLeft); + } + + void parse(ModelViewDslContext context, Tokens tokens) { + // autoLayout [rankDirection] [rankSeparation] [nodeSeparation] + ModelView view = context.getView(); + if (view != null) { + AutomaticLayout.RankDirection rankDirection = AutomaticLayout.RankDirection.TopBottom; + int rankSeparation = DEFAULT_RANK_SEPARATION; + int nodeSeparation = DEFAULT_NODE_SEPARATION; + + if (tokens.includes(RANK_DIRECTION_INDEX)) { + String rankDirectionAsString = tokens.get(RANK_DIRECTION_INDEX); + + if (RANK_DIRECTIONS.containsKey(rankDirectionAsString)) { + rankDirection = RANK_DIRECTIONS.get(rankDirectionAsString); + } else { + throw new RuntimeException("Valid rank directions are: tb|bt|lr|rl"); + } + } + + if (tokens.includes(RANK_SEPARATION_INDEX)) { + String rankSeparationAsString = tokens.get(RANK_SEPARATION_INDEX); + + try { + rankSeparation = Integer.parseInt(rankSeparationAsString); + } catch (NumberFormatException e) { + throw new RuntimeException("Rank separation must be positive integer in pixels"); + } + } + + if (tokens.includes(NODE_SEPARATION_INDEX)) { + String nodeSeparationAsString = tokens.get(NODE_SEPARATION_INDEX); + + try { + nodeSeparation = Integer.parseInt(nodeSeparationAsString); + } catch (NumberFormatException e) { + throw new RuntimeException("Node separation must be positive integer in pixels"); + } + } + + view.enableAutomaticLayout(rankDirection, rankSeparation, nodeSeparation); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingDslContext.java new file mode 100644 index 000000000..e2b730cd4 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingDslContext.java @@ -0,0 +1,25 @@ +package com.structurizr.dsl; + +import java.io.File; + +final class BrandingDslContext extends DslContext { + + private File file; + + BrandingDslContext(File file) { + this.file = file; + } + + File getFile() { + return file; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.BRANDING_LOGO_TOKEN, + StructurizrDslTokens.BRANDING_FONT_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java new file mode 100644 index 000000000..62c08fd4b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java @@ -0,0 +1,71 @@ +package com.structurizr.dsl; + +import com.structurizr.util.ImageUtils; +import com.structurizr.view.Font; + +import java.io.File; + +final class BrandingParser extends AbstractParser { + + private static final String LOGO_GRAMMAR = "logo "; + private static final String FONT_GRAMMAR = "font [url]"; + + private static final int LOGO_FILE_INDEX = 1; + + private static final int FONT_NAME_INDEX = 1; + private static final int FONT_URL_INDEX = 2; + + void parseLogo(BrandingDslContext context, Tokens tokens, boolean restricted) { + // logo + + if (tokens.hasMoreThan(LOGO_FILE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + LOGO_GRAMMAR); + } else if (tokens.includes(LOGO_FILE_INDEX)) { + String path = tokens.get(1); + + if (path.startsWith("data:image/") || path.startsWith("https://") || path.startsWith("http://")) { + if (IconUtils.isSupported(path)) { + context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(path); + } else { + throw new IllegalArgumentException("Only PNG and JPG URLs and data URIs are supported: " + path); + } + } else { + if (!restricted) { + File file = new File(context.getFile().getParent(), path); + if (file.exists() && !file.isDirectory()) { + try { + String dataUri = ImageUtils.getImageAsDataUri(file); + context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(dataUri); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException(path + " does not exist"); + } + } + } + } else { + throw new RuntimeException("Expected: " + LOGO_GRAMMAR); + } + } + + void parseFont(BrandingDslContext context, Tokens tokens) { + // font [url] + + if (tokens.hasMoreThan(FONT_URL_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + FONT_GRAMMAR); + } else if (tokens.includes(FONT_URL_INDEX)) { + String name = tokens.get(FONT_NAME_INDEX); + String url = tokens.get(FONT_URL_INDEX); + + context.getWorkspace().getViews().getConfiguration().getBranding().setFont(new Font(name, url)); + } else if (tokens.includes(FONT_NAME_INDEX)) { + String name = tokens.get(FONT_NAME_INDEX); + + context.getWorkspace().getViews().getConfiguration().getBranding().setFont(new Font(name)); + } else { + throw new RuntimeException("Expected: " + FONT_GRAMMAR); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CommentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CommentDslContext.java new file mode 100644 index 000000000..24f802627 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CommentDslContext.java @@ -0,0 +1,10 @@ +package com.structurizr.dsl; + +final class CommentDslContext extends DslContext { + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java new file mode 100644 index 000000000..39e079084 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java @@ -0,0 +1,42 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Component; +import com.structurizr.model.GroupableElement; +import com.structurizr.model.ModelItem; + +final class ComponentDslContext extends GroupableElementDslContext { + + private Component component; + + ComponentDslContext(Component component) { + this.component = component; + } + + Component getComponent() { + return component; + } + + @Override + ModelItem getModelItem() { + return getComponent(); + } + + @Override + GroupableElement getElement() { + return component; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java new file mode 100644 index 000000000..9940d837a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java @@ -0,0 +1,77 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Component; +import com.structurizr.model.Container; + +final class ComponentParser extends AbstractParser { + + private static final String GRAMMAR = "component [description] [technology] [tags]"; + + private final static int NAME_INDEX = 1; + private final static int DESCRIPTION_INDEX = 2; + private final static int TECHNOLOGY_INDEX = 3; + private final static int TAGS_INDEX = 4; + + Component parse(ContainerDslContext context, Tokens tokens) { + // component [description] [technology] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + Container container = context.getContainer(); + Component component = null; + String name = tokens.get(NAME_INDEX); + + if (context.isExtendingWorkspace()) { + component = container.getComponentWithName(name); + } + + if (component == null) { + component = container.addComponent(name); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + String description = tokens.get(DESCRIPTION_INDEX); + component.setDescription(description); + } + + if (tokens.includes(TECHNOLOGY_INDEX)) { + String technology = tokens.get(TECHNOLOGY_INDEX); + component.setTechnology(technology); + } + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + component.addTags(tags.split(",")); + } + + if (context.hasGroup()) { + component.setGroup(context.getGroup().getName()); + context.getGroup().addElement(component); + } + + return component; + } + + void parseTechnology(ComponentDslContext context, Tokens tokens) { + int index = 1; + + // technology + if (tokens.hasMoreThan(index)) { + throw new RuntimeException("Too many tokens, expected: technology "); + } + + if (!tokens.includes(index)) { + throw new RuntimeException("Expected: technology "); + } + + String technology = tokens.get(index); + context.getComponent().setTechnology(technology); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewDslContext.java new file mode 100644 index 000000000..31b482510 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewDslContext.java @@ -0,0 +1,11 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ComponentView; + +final class ComponentViewDslContext extends StaticViewDslContext { + + ComponentViewDslContext(ComponentView view) { + super(view); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java new file mode 100644 index 000000000..3acd88669 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java @@ -0,0 +1,62 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Container; +import com.structurizr.model.Element; +import com.structurizr.view.ComponentView; + +final class ComponentViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "component [key] [description] {"; + + private static final String VIEW_TYPE = "Component"; + + private static final int CONTAINER_IDENTIFIER_INDEX = 1; + private static final int KEY_INDEX = 2; + private static final int DESCRIPTION_INDEX = 3; + + ComponentView parse(DslContext context, Tokens tokens) { + // component [key] [description] { + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(CONTAINER_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + Workspace workspace = context.getWorkspace(); + Container container; + String key = ""; + String description = ""; + + String containerIdentifier = tokens.get(CONTAINER_IDENTIFIER_INDEX); + Element element = context.getElement(containerIdentifier); + if (element == null) { + throw new RuntimeException("The container \"" + containerIdentifier + "\" does not exist"); + } + if (element instanceof Container) { + container = (Container)element; + } else { + throw new RuntimeException("The element \"" + containerIdentifier + "\" is not a container"); + } + + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + ComponentView view = workspace.getViews().createComponentView(container, key, description); + view.setExternalSoftwareSystemBoundariesVisible(true); + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationDslContext.java new file mode 100644 index 000000000..18b211f57 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationDslContext.java @@ -0,0 +1,15 @@ +package com.structurizr.dsl; + +final class ConfigurationDslContext extends DslContext { + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.SCOPE_TOKEN, + StructurizrDslTokens.VISIBILITY_TOKEN, + StructurizrDslTokens.USERS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationParser.java new file mode 100644 index 000000000..ddd24799e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ConfigurationParser.java @@ -0,0 +1,61 @@ +package com.structurizr.dsl; + +import com.structurizr.configuration.Visibility; +import com.structurizr.configuration.WorkspaceScope; + +final class ConfigurationParser extends AbstractParser { + + private static final String SCOPE_GRAMMAR = "scope "; + private static final String SCOPE_LANDSCAPE = "landscape"; + private static final String SCOPE_SOFTWARE_SYSTEM = "softwaresystem"; + private static final String SCOPE_NONE = "none"; + + private static final String VISIBILITY_GRAMMAR = "visibility "; + private static final String VISIBILITY_PRIVATE = "private"; + private static final String VISIBILITY_PUBLIC = "public"; + + private static final int FIRST_PROPERTY_INDEX = 1; + + void parseScope(DslContext context, Tokens tokens) { + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + SCOPE_GRAMMAR); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String scope = tokens.get(1).toLowerCase(); + + if (scope.equalsIgnoreCase(SCOPE_LANDSCAPE)) { + context.getWorkspace().getConfiguration().setScope(WorkspaceScope.Landscape); + } else if (scope.equalsIgnoreCase(SCOPE_SOFTWARE_SYSTEM)) { + context.getWorkspace().getConfiguration().setScope(WorkspaceScope.SoftwareSystem); + } else if (scope.equalsIgnoreCase(SCOPE_NONE)) { + context.getWorkspace().getConfiguration().setScope(null); + } else { + throw new RuntimeException("The scope \"" + scope + "\" is not valid"); + } + } else { + throw new RuntimeException("Expected: " + SCOPE_GRAMMAR); + } + } + + void parseVisibility(DslContext context, Tokens tokens) { + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + VISIBILITY_GRAMMAR); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String visibility = tokens.get(1).toLowerCase(); + + if (visibility.equalsIgnoreCase(VISIBILITY_PRIVATE)) { + context.getWorkspace().getConfiguration().setVisibility(Visibility.Private); + } else if (visibility.equalsIgnoreCase(VISIBILITY_PUBLIC)) { + context.getWorkspace().getConfiguration().setVisibility(Visibility.Public); + } else { + throw new RuntimeException("The visibility \"" + visibility + "\" is not valid"); + } + } else { + throw new RuntimeException("Expected: " + VISIBILITY_GRAMMAR); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Constant.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Constant.java new file mode 100644 index 000000000..e290a35a6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Constant.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +class Constant { + + private String name; + private String value; + + Constant(String name, String value) { + this.name = name; + this.value = value; + } + + String getName() { + return name; + } + + String getValue() { + return value; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ConstantParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ConstantParser.java new file mode 100644 index 000000000..4b8c28204 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ConstantParser.java @@ -0,0 +1,33 @@ +package com.structurizr.dsl; + +final class ConstantParser extends AbstractParser { + + private static final String GRAMMAR = "!constant "; + + private static final int NAME_INDEX = 1; + private static final int VALUE_INDEX = 2; + + private static final String NAME_REGEX = "[a-zA-Z0-9-_.]+"; + + Constant parse(DslContext context, Tokens tokens) { + // !constant name value + + if (tokens.hasMoreThan(VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(VALUE_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String name = tokens.get(NAME_INDEX); + String value = tokens.get(VALUE_INDEX); + + if (!name.matches(NAME_REGEX)) { + throw new RuntimeException("Constant names must only contain the following characters: a-zA-Z0-9-_."); + } + + return new Constant(name, value); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java new file mode 100644 index 000000000..2099a3b15 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java @@ -0,0 +1,52 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Container; +import com.structurizr.model.GroupableElement; +import com.structurizr.model.ModelItem; + +final class ContainerDslContext extends GroupableElementDslContext { + + private Container container; + + ContainerDslContext(Container container) { + this(container, null); + } + + ContainerDslContext(Container container, ElementGroup group) { + super(group); + + this.container = container; + } + + Container getContainer() { + return container; + } + + @Override + ModelItem getModelItem() { + return getContainer(); + } + + @Override + GroupableElement getElement() { + return container; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DOCS_TOKEN, + StructurizrDslTokens.ADRS_TOKEN, + StructurizrDslTokens.GROUP_TOKEN, + StructurizrDslTokens.COMPONENT_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java new file mode 100644 index 000000000..4268f656b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java @@ -0,0 +1,42 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ContainerInstance; +import com.structurizr.model.ModelItem; +import com.structurizr.model.StaticStructureElementInstance; + +final class ContainerInstanceDslContext extends StaticStructureElementInstanceDslContext { + + private ContainerInstance containerInstance; + + ContainerInstanceDslContext(ContainerInstance containerInstance) { + this.containerInstance = containerInstance; + } + + ContainerInstance getContainerInstance() { + return containerInstance; + } + + @Override + ModelItem getModelItem() { + return getContainerInstance(); + } + + @Override + StaticStructureElementInstance getElementInstance() { + return getContainerInstance(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.HEALTH_CHECK_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java new file mode 100644 index 000000000..c58287956 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java @@ -0,0 +1,64 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +import java.util.HashSet; +import java.util.Set; + +final class ContainerInstanceParser extends AbstractParser { + + private static final String GRAMMAR = "containerInstance [deploymentGroups] [tags]"; + + private static final int IDENTIFIER_INDEX = 1; + private static final int DEPLOYMENT_GROUPS_TOKEN = 2; + private static final int TAGS_INDEX = 3; + + ContainerInstance parse(DeploymentNodeDslContext context, Tokens tokens) { + // containerInstance [deploymentGroups] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String containerIdentifier = tokens.get(IDENTIFIER_INDEX); + + Element element = context.getElement(containerIdentifier, Container.class); + if (element == null) { + throw new RuntimeException("The container \"" + containerIdentifier + "\" does not exist"); + } + + DeploymentNode deploymentNode = context.getDeploymentNode(); + + Set deploymentGroups = new HashSet<>(); + if (tokens.includes(DEPLOYMENT_GROUPS_TOKEN)) { + String token = tokens.get(DEPLOYMENT_GROUPS_TOKEN); + + String[] deploymentGroupReferences = token.split(","); + for (String deploymentGroupReference : deploymentGroupReferences) { + Element e = context.getElement(deploymentGroupReference); + if (e instanceof DeploymentGroup) { + deploymentGroups.add(e.getName()); + } + } + } + + ContainerInstance containerInstance = deploymentNode.add((Container)element, deploymentGroups.toArray(new String[]{})); + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + containerInstance.addTags(tags.split(",")); + } + + if (context.hasGroup()) { + containerInstance.setGroup(context.getGroup().getName()); + context.getGroup().addElement(containerInstance); + } + + return containerInstance; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java new file mode 100644 index 000000000..eb7f38372 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java @@ -0,0 +1,77 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; + +final class ContainerParser extends AbstractParser { + + private static final String GRAMMAR = "container [description] [technology] [tags]"; + + private final static int NAME_INDEX = 1; + private final static int DESCRIPTION_INDEX = 2; + private final static int TECHNOLOGY_INDEX = 3; + private final static int TAGS_INDEX = 4; + + Container parse(SoftwareSystemDslContext context, Tokens tokens) { + // container [description] [technology] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + SoftwareSystem softwareSystem = context.getSoftwareSystem(); + Container container = null; + String name = tokens.get(NAME_INDEX); + + if (context.isExtendingWorkspace()) { + container = softwareSystem.getContainerWithName(name); + } + + if (container == null) { + container = softwareSystem.addContainer(name); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + String description = tokens.get(DESCRIPTION_INDEX); + container.setDescription(description); + } + + if (tokens.includes(TECHNOLOGY_INDEX)) { + String technology = tokens.get(TECHNOLOGY_INDEX); + container.setTechnology(technology); + } + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + container.addTags(tags.split(",")); + } + + if (context.hasGroup()) { + container.setGroup(context.getGroup().getName()); + context.getGroup().addElement(container); + } + + return container; + } + + void parseTechnology(ContainerDslContext context, Tokens tokens) { + int index = 1; + + // technology + if (tokens.hasMoreThan(index)) { + throw new RuntimeException("Too many tokens, expected: technology "); + } + + if (!tokens.includes(index)) { + throw new RuntimeException("Expected: technology "); + } + + String technology = tokens.get(index); + context.getContainer().setTechnology(technology); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewDslContext.java new file mode 100644 index 000000000..c304fd247 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewDslContext.java @@ -0,0 +1,11 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ContainerView; + +final class ContainerViewDslContext extends StaticViewDslContext { + + ContainerViewDslContext(ContainerView view) { + super(view); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java new file mode 100644 index 000000000..b1033eb43 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java @@ -0,0 +1,62 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.ContainerView; + +final class ContainerViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "container [key] [description] {"; + + private static final String VIEW_TYPE = "Container"; + + private static final int SOFTWARE_SYSTEM_IDENTIFIER_INDEX = 1; + private static final int KEY_INDEX = 2; + private static final int DESCRIPTION_INDEX = 3; + + ContainerView parse(DslContext context, Tokens tokens) { + // container [key] [description] { + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(SOFTWARE_SYSTEM_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + Workspace workspace = context.getWorkspace(); + SoftwareSystem softwareSystem; + String key = ""; + String description = ""; + + String softwareSystemIdentifier = tokens.get(SOFTWARE_SYSTEM_IDENTIFIER_INDEX); + Element element = context.getElement(softwareSystemIdentifier); + if (element == null) { + throw new RuntimeException("The software system \"" + softwareSystemIdentifier + "\" does not exist"); + } + if (element instanceof SoftwareSystem) { + softwareSystem = (SoftwareSystem)element; + } else { + throw new RuntimeException("The element \"" + softwareSystemIdentifier + "\" is not a software system"); + } + + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + ContainerView view = workspace.getViews().createContainerView(softwareSystem, key, description); + view.setExternalSoftwareSystemBoundariesVisible(true); + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementDslContext.java new file mode 100644 index 000000000..78daacea1 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementDslContext.java @@ -0,0 +1,41 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +import com.structurizr.model.GroupableElement; +import com.structurizr.model.ModelItem; + +final class CustomElementDslContext extends GroupableElementDslContext { + + private CustomElement customElement; + + CustomElementDslContext(CustomElement customElement) { + this.customElement = customElement; + } + + CustomElement getCustomElement() { + return customElement; + } + + @Override + ModelItem getModelItem() { + return getCustomElement(); + } + + @Override + GroupableElement getElement() { + return customElement; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java new file mode 100644 index 000000000..bef0a7707 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java @@ -0,0 +1,53 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +import com.structurizr.model.Location; +import com.structurizr.model.Person; + +final class CustomElementParser extends AbstractParser { + + private static final String GRAMMAR = "element [metadata] [description] [tags]"; + + private final static int NAME_INDEX = 1; + private final static int METADATA_INDEX = 2; + private final static int DESCRIPTION_INDEX = 3; + private final static int TAGS_INDEX = 4; + + CustomElement parse(GroupableDslContext context, Tokens tokens) { + // element [metadata] [description] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String name = tokens.get(NAME_INDEX); + + String metadata = ""; + if (tokens.includes(METADATA_INDEX)) { + metadata = tokens.get(METADATA_INDEX); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + CustomElement customElement = context.getWorkspace().getModel().addCustomElement(name, metadata, description); + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + customElement.addTags(tags.split(",")); + } + + if (context.hasGroup()) { + customElement.setGroup(context.getGroup().getName()); + } + + return customElement; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationDslContext.java new file mode 100644 index 000000000..df19917a8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationDslContext.java @@ -0,0 +1,26 @@ +package com.structurizr.dsl; + +import com.structurizr.view.CustomView; + +class CustomViewAnimationDslContext extends DslContext { + + private CustomView view; + + CustomViewAnimationDslContext(CustomView view) { + super(); + + this.view = view; + } + + CustomView getView() { + return view; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ANIMATION_STEP_IN_VIEW_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java new file mode 100644 index 000000000..c455c18d4 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java @@ -0,0 +1,51 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +import com.structurizr.model.Element; +import com.structurizr.view.CustomView; + +import java.util.ArrayList; +import java.util.List; + +final class CustomViewAnimationStepParser extends AbstractParser { + + void parse(CustomViewDslContext context, Tokens tokens) { + // animationStep [identifier...] + + if (!tokens.includes(1)) { + throw new RuntimeException("Expected: animationStep [identifier...]"); + } + + parse(context, context.getCustomView(), tokens, 1); + } + + void parse(CustomViewAnimationDslContext context, Tokens tokens) { + // [identifier...] + + if (!tokens.includes(0)) { + throw new RuntimeException("Expected: [identifier...]"); + } + + parse(context, context.getView(), tokens, 0); + } + + void parse(DslContext context, CustomView view, Tokens tokens, int startIndex) { + List elements = new ArrayList<>(); + + for (int i = startIndex; i < tokens.size(); i++) { + String elementIdentifier = tokens.get(i); + + Element element = context.getElement(elementIdentifier); + if (element == null) { + throw new RuntimeException("The element \"" + elementIdentifier + "\" does not exist"); + } + + if (element instanceof CustomElement) { + elements.add((CustomElement)element); + } + } + + view.addAnimation(elements.toArray(new CustomElement[0])); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewContentParser.java new file mode 100644 index 000000000..47e97a6cc --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewContentParser.java @@ -0,0 +1,97 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +import com.structurizr.model.Element; +import com.structurizr.model.ModelItem; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; +import com.structurizr.view.CustomView; +import com.structurizr.view.ElementNotPermittedInViewException; + +final class CustomViewContentParser extends ModelViewContentParser { + + private static final int FIRST_IDENTIFIER_INDEX = 1; + + void parseInclude(CustomViewDslContext context, Tokens tokens) { + if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: include <*|identifier> [*|identifier...]"); + } + + CustomView view = context.getCustomView(); + + // include [identifier...] + for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { + String token = tokens.get(i); + + if (token.equals(WILDCARD) || token.equals(ELEMENT_WILDCARD)) { + // include * or include element==* + view.addDefaultElements(); + } else if (isExpression(token)) { + new CustomViewExpressionParser().parseExpression(token, context).forEach(mi -> addModelItemToView(mi, view, null)); + } else { + new CustomViewExpressionParser().parseIdentifier(token, context).forEach(mi -> addModelItemToView(mi, view, token)); + } + } + } + + void parseExclude(CustomViewDslContext context, Tokens tokens) { + if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: exclude [identifier...]"); + } + + CustomView view = context.getCustomView(); + + // exclude [identifier...] + for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { + String token = tokens.get(i); + if (isExpression(token)) { + new CustomViewExpressionParser().parseExpression(token, context).forEach(mi -> removeModelItemFromView(mi, view)); + } else { + new CustomViewExpressionParser().parseIdentifier(token, context).forEach(mi -> removeModelItemFromView(mi, view)); + } + } + } + + private void addModelItemToView(ModelItem modelItem, CustomView view, String identifier) { + if (modelItem instanceof Element) { + addElementToView((Element)modelItem, view, identifier); + } else { + addRelationshipToView((Relationship)modelItem, view); + } + } + + private void addElementToView(Element element, CustomView view, String identifier) { + try { + if (element instanceof CustomElement) { + view.add((CustomElement) element); + } else { + if (!StringUtils.isNullOrEmpty(identifier)) { + throw new RuntimeException("The element \"" + identifier + "\" can not be added to this type of view"); + } + } + } catch (ElementNotPermittedInViewException e) { + // ignore + } + } + + private void removeModelItemFromView(ModelItem modelItem, CustomView view) { + if (modelItem instanceof Element) { + removeElementFromView((Element)modelItem, view); + } else { + removeRelationshipFromView((Relationship)modelItem, view); + } + } + + private void removeElementFromView(Element element, CustomView view) { + if (element instanceof CustomElement) { + view.remove((CustomElement) element); + } + } + + private void addRelationshipToView(Relationship relationship, CustomView view) { + if (view.isElementInView(relationship.getSource()) && view.isElementInView(relationship.getDestination())) { + view.add(relationship); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewDslContext.java new file mode 100644 index 000000000..e434f9bed --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewDslContext.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.view.CustomView; + +final class CustomViewDslContext extends ModelViewDslContext { + + CustomViewDslContext(CustomView view) { + super(view); + } + + public CustomView getCustomView() { + return (CustomView)super.getView(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.INCLUDE_IN_VIEW_TOKEN, + StructurizrDslTokens.EXCLUDE_IN_VIEW_TOKEN, + StructurizrDslTokens.AUTOLAYOUT_VIEW_TOKEN, + StructurizrDslTokens.DEFAULT_VIEW_TOKEN, + StructurizrDslTokens.ANIMATION_IN_VIEW_TOKEN, + StructurizrDslTokens.VIEW_TITLE_TOKEN, + StructurizrDslTokens.VIEW_DESCRIPTION_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java new file mode 100644 index 000000000..585a23899 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java @@ -0,0 +1,45 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +import com.structurizr.model.Element; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; + +final class CustomViewExpressionParser extends AbstractExpressionParser { + + @Override + protected Set evaluateElementTypeExpression(String expr, DslContext context) { + Set elements = new LinkedHashSet<>(); + + String type = expr.substring(ELEMENT_TYPE_EQUALS_EXPRESSION.length()); + switch (type.toLowerCase()) { + case "custom": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof CustomElement).forEach(elements::add); + break; + default: + throw new RuntimeException("The element type of \"" + type + "\" is not valid for this view"); + } + + return elements; + } + + protected Set findAfferentCouplings(Element element) { + Set elements = new LinkedHashSet<>(); + + elements.addAll(findAfferentCouplings(element, CustomElement.class)); + + return elements; + } + + protected Set findEfferentCouplings(Element element) { + Set elements = new LinkedHashSet<>(); + + elements.addAll(findEfferentCouplings(element, CustomElement.class)); + + return elements; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewParser.java new file mode 100644 index 000000000..5aedf5dc9 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewParser.java @@ -0,0 +1,48 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.view.CustomView; + +final class CustomViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "custom [key] [title] [description] {"; + + private static final String VIEW_TYPE = "Custom"; + + private static final int KEY_INDEX = 1; + private static final int TITLE_INDEX = 2; + private static final int DESCRIPTION_INDEX = 3; + + CustomView parse(DslContext context, Tokens tokens) { + // custom [key] [title] [description] + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + Workspace workspace = context.getWorkspace(); + String key = ""; + String title = ""; + String description = ""; + + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + if (tokens.includes(TITLE_INDEX)) { + title = tokens.get(TITLE_INDEX); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + CustomView view = workspace.getViews().createCustomView(key, title, description); + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DefaultViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DefaultViewParser.java new file mode 100644 index 000000000..919e2ec57 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DefaultViewParser.java @@ -0,0 +1,12 @@ +package com.structurizr.dsl; + +import com.structurizr.view.View; + +final class DefaultViewParser extends AbstractParser { + + void parse(ViewDslContext context) { + View view = context.getView(); + context.getWorkspace().getViews().getConfiguration().setDefaultView(view); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironment.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironment.java new file mode 100644 index 000000000..2b7f60b6c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironment.java @@ -0,0 +1,49 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; + +import java.util.Objects; +import java.util.Set; + +class DeploymentEnvironment extends Element { + + private String name; + + DeploymentEnvironment(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getCanonicalName() { + return name; + } + + @Override + public Element getParent() { + return null; + } + + @Override + public Set getDefaultTags() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeploymentEnvironment that = (DeploymentEnvironment) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java new file mode 100644 index 000000000..c35027242 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java @@ -0,0 +1,31 @@ +package com.structurizr.dsl; + +final class DeploymentEnvironmentDslContext extends GroupableDslContext { + + private final String environment; + + DeploymentEnvironmentDslContext(String environment) { + super(null); + this.environment = environment; + } + + DeploymentEnvironmentDslContext(String environment, ElementGroup group) { + super(group); + this.environment = environment; + } + + String getEnvironment() { + return environment; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.GROUP_TOKEN, + StructurizrDslTokens.DEPLOYMENT_GROUP_TOKEN, + StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentParser.java new file mode 100644 index 000000000..d206f5f60 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentParser.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class DeploymentEnvironmentParser extends AbstractParser { + + private static final String GRAMMAR = "deploymentEnvironment {"; + + private static final int DEPLOYMENT_ENVIRONMENT_NAME_INDEX = 1; + + String parse(Tokens tokens) { + // deploymentEnvironment + + if (tokens.hasMoreThan(DEPLOYMENT_ENVIRONMENT_NAME_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } else if (tokens.size() != DEPLOYMENT_ENVIRONMENT_NAME_INDEX + 1) { + throw new RuntimeException("Expected: " + GRAMMAR); + } else { + return tokens.get(DEPLOYMENT_ENVIRONMENT_NAME_INDEX); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java new file mode 100644 index 000000000..0027da22f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java @@ -0,0 +1,35 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; + +import java.util.Set; + +class DeploymentGroup extends Element { + + private String name; + + DeploymentGroup(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getCanonicalName() { + return name; + } + + @Override + public Element getParent() { + return null; + } + + @Override + public Set getDefaultTags() { + return null; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroupParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroupParser.java new file mode 100644 index 000000000..ff0b79fc4 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroupParser.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class DeploymentGroupParser extends AbstractParser { + + private static final String GRAMMAR = "deploymentGroup "; + + private static final int DEPLOYMENT_GROUP_NAME_INDEX = 1; + + String parse(Tokens tokens) { + // deploymentGroup + + if (tokens.hasMoreThan(DEPLOYMENT_GROUP_NAME_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } else if (tokens.size() != DEPLOYMENT_GROUP_NAME_INDEX + 1) { + throw new RuntimeException("Expected: " + GRAMMAR); + } else { + return tokens.get(DEPLOYMENT_GROUP_NAME_INDEX); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java new file mode 100644 index 000000000..0cfecda3d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java @@ -0,0 +1,54 @@ +package com.structurizr.dsl; + +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.GroupableElement; +import com.structurizr.model.ModelItem; + +final class DeploymentNodeDslContext extends GroupableElementDslContext { + + private final DeploymentNode deploymentNode; + + DeploymentNodeDslContext(DeploymentNode deploymentNode) { + this(deploymentNode, null); + } + + public DeploymentNodeDslContext(DeploymentNode deploymentNode, ElementGroup group) { + super(group); + + this.deploymentNode = deploymentNode; + } + + DeploymentNode getDeploymentNode() { + return deploymentNode; + } + + @Override + ModelItem getModelItem() { + return getDeploymentNode(); + } + + @Override + GroupableElement getElement() { + return deploymentNode; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.GROUP_TOKEN, + StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, + StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, + StructurizrDslTokens.SOFTWARE_SYSTEM_INSTANCE_TOKEN, + StructurizrDslTokens.CONTAINER_INSTANCE_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.INSTANCES_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java new file mode 100644 index 000000000..ba88d73af --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java @@ -0,0 +1,140 @@ +package com.structurizr.dsl; + +import com.structurizr.model.DeploymentNode; + +final class DeploymentNodeParser extends AbstractParser { + + private static final String GRAMMAR = "deploymentNode [description] [technology] [tags] [instances] {"; + + private static final int NAME_INDEX = 1; + private static final int DESCRIPTION_INDEX = 2; + private static final int TECHNOLOGY_INDEX = 3; + private static final int TAGS_INDEX = 4; + private static final int INSTANCES_INDEX = 5; + + DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens) { + // deploymentNode [description] [technology] [tags] [instances] + + if (tokens.hasMoreThan(INSTANCES_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + DeploymentNode deploymentNode = null; + String name = tokens.get(NAME_INDEX); + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + + deploymentNode = context.getWorkspace().getModel().addDeploymentNode(context.getEnvironment(), name, description, technology); + + String tags = ""; + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX); + deploymentNode.addTags(tags.split(",")); + } + + String instances = "1"; + if (tokens.includes(INSTANCES_INDEX)) { + instances = tokens.get(INSTANCES_INDEX); + deploymentNode.setInstances(instances); + } + + if (context.hasGroup()) { + deploymentNode.setGroup(context.getGroup().getName()); + context.getGroup().addElement(deploymentNode); + } + + return deploymentNode; + } + + DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens) { + // deploymentNode [description] [technology] [tags] [instances] + + if (tokens.hasMoreThan(INSTANCES_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + DeploymentNode deploymentNode = null; + String name = tokens.get(NAME_INDEX); + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + + DeploymentNode parent = context.getDeploymentNode(); + deploymentNode = parent.addDeploymentNode(name, description, technology); + + String tags = ""; + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX); + deploymentNode.addTags(tags.split(",")); + } + + String instances = "1"; + if (tokens.includes(INSTANCES_INDEX)) { + instances = tokens.get(INSTANCES_INDEX); + deploymentNode.setInstances(instances); + } + + if (context.hasGroup()) { + deploymentNode.setGroup(context.getGroup().getName()); + context.getGroup().addElement(deploymentNode); + } + + return deploymentNode; + } + + void parseTechnology(DeploymentNodeDslContext context, Tokens tokens) { + int index = 1; + + // technology + if (tokens.hasMoreThan(index)) { + throw new RuntimeException("Too many tokens, expected: technology "); + } + + if (!tokens.includes(index)) { + throw new RuntimeException("Expected: technology "); + } + + String technology = tokens.get(index); + context.getDeploymentNode().setTechnology(technology); + } + + void parseInstances(DeploymentNodeDslContext context, Tokens tokens) { + int index = 1; + + // instances + if (tokens.hasMoreThan(index)) { + throw new RuntimeException("Too many tokens, expected: instances "); + } + + if (!tokens.includes(index)) { + throw new RuntimeException("Expected: instances "); + } + + String instances = tokens.get(index); + context.getDeploymentNode().setInstances(instances); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationDslContext.java new file mode 100644 index 000000000..b08ad1465 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationDslContext.java @@ -0,0 +1,26 @@ +package com.structurizr.dsl; + +import com.structurizr.view.DeploymentView; + +class DeploymentViewAnimationDslContext extends DslContext { + + private DeploymentView view; + + DeploymentViewAnimationDslContext(DeploymentView view) { + super(); + + this.view = view; + } + + DeploymentView getView() { + return view; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ANIMATION_STEP_IN_VIEW_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java new file mode 100644 index 000000000..93598de55 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java @@ -0,0 +1,60 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ContainerInstance; +import com.structurizr.model.Element; +import com.structurizr.model.InfrastructureNode; +import com.structurizr.model.StaticStructureElementInstance; +import com.structurizr.view.DeploymentView; + +import java.util.ArrayList; +import java.util.List; + +final class DeploymentViewAnimationStepParser extends AbstractParser { + + void parse(DeploymentViewDslContext context, Tokens tokens) { + // animationStep [identifier...] + + if (!tokens.includes(1)) { + throw new RuntimeException("Expected: animationStep [identifier...]"); + } + + parse(context, context.getView(), tokens, 1); + } + + void parse(DeploymentViewAnimationDslContext context, Tokens tokens) { + // animationStep [identifier...] + + if (!tokens.includes(0)) { + throw new RuntimeException("Expected: [identifier...]"); + } + + parse(context, context.getView(), tokens, 0); + } + + void parse(DslContext context, DeploymentView view, Tokens tokens, int startIndex) { + List staticStructureElementInstances = new ArrayList<>(); + List infrastructureNodes = new ArrayList<>(); + + for (int i = startIndex; i < tokens.size(); i++) { + String identifier = tokens.get(i); + + Element element = context.getElement(identifier); + if (element == null) { + throw new RuntimeException("The element \"" + identifier + "\" does not exist"); + } + + if (element instanceof StaticStructureElementInstance) { + staticStructureElementInstances.add((StaticStructureElementInstance)element); + } + + if (element instanceof InfrastructureNode) { + infrastructureNodes.add((InfrastructureNode)element); + } + } + + if (!(staticStructureElementInstances.isEmpty() && infrastructureNodes.isEmpty())) { + view.addAnimation(staticStructureElementInstances.toArray(new StaticStructureElementInstance[0]), infrastructureNodes.toArray(new InfrastructureNode[0])); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java new file mode 100644 index 000000000..00a955543 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java @@ -0,0 +1,154 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +final class DeploymentViewContentParser extends ModelViewContentParser { + + private static final int FIRST_IDENTIFIER_INDEX = 1; + + void parseInclude(DeploymentViewDslContext context, Tokens tokens) { + if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: include <*|identifier> [*|identifier...]"); + } + + DeploymentView view = context.getView(); + + // include [identifier...] + for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { + String token = tokens.get(i); + + if (token.equals(WILDCARD) || token.equals(ELEMENT_WILDCARD)) { + // include * or include element==* + view.addDefaultElements(); + } else if (isExpression(token)) { + new DeploymentViewExpressionParser().parseExpression(token, context).forEach(mi -> addModelItemToView(mi, view, null)); + } else { + new DeploymentViewExpressionParser().parseIdentifier(token, context).forEach(mi -> addModelItemToView(mi, view, token)); + } + } + } + + void parseExclude(DeploymentViewDslContext context, Tokens tokens) { + if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: exclude [identifier...]"); + } + + DeploymentView view = context.getView(); + + // exclude [identifier...] + for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { + String token = tokens.get(i); + + if (isExpression(token)) { + new DeploymentViewExpressionParser().parseExpression(token, context).forEach(e -> removeModelItemFromView(e, view)); + } else { + new DeploymentViewExpressionParser().parseIdentifier(token, context).forEach(mi -> removeModelItemFromView(mi, view)); + } + } + } + + private void addModelItemToView(ModelItem modelItem, DeploymentView view, String identifier) { + if (modelItem instanceof Element) { + addElementToView((Element)modelItem, view, identifier); + } else { + addRelationshipToView((Relationship)modelItem, view); + } + } + + private void addElementToView(Element element, DeploymentView view, String identifier) { + try { + if (element instanceof CustomElement) { + view.add((CustomElement) element); + } else if (element instanceof DeploymentNode) { + view.add((DeploymentNode) element); + } else if (element instanceof InfrastructureNode) { + view.add((InfrastructureNode) element); + } else if (element instanceof SoftwareSystem) { + // find instances of this software system + view.getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).map(e -> (SoftwareSystemInstance)e).filter(ssi -> ssi.getSoftwareSystem().equals(element) && ssi.getEnvironment().equals(view.getEnvironment())).forEach(view::add); + } else if (element instanceof SoftwareSystemInstance) { + view.add((SoftwareSystemInstance) element); + } else if (element instanceof Container) { + // find instances of this container + view.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getContainer().equals(element) && ci.getEnvironment().equals(view.getEnvironment())).forEach(view::add); + } else if (element instanceof ContainerInstance) { + view.add((ContainerInstance) element); + } else { + if (!StringUtils.isNullOrEmpty(identifier)) { + throw new RuntimeException("The element \"" + identifier + "\" can not be added to this type of view"); + } + } + } catch (ElementNotPermittedInViewException e) { + // ignore + } + } + + private void removeModelItemFromView(ModelItem modelItem, DeploymentView view) { + if (modelItem instanceof Element) { + removeElementFromView((Element)modelItem, view); + } else { + removeRelationshipFromView((Relationship)modelItem, view); + } + } + + private void removeElementFromView(Element element, DeploymentView view) { + if (element instanceof CustomElement) { + view.remove((CustomElement)element); + } else if (element instanceof DeploymentNode) { + view.remove((DeploymentNode)element); + } else if (element instanceof InfrastructureNode) { + view.remove((InfrastructureNode)element); + } else if (element instanceof SoftwareSystem) { + // find instances of this software system + view.getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).map(e -> (SoftwareSystemInstance)e).filter(ssi -> ssi.getSoftwareSystem().equals(element) && ssi.getEnvironment().equals(view.getEnvironment())).forEach(view::remove); + } else if (element instanceof SoftwareSystemInstance) { + view.remove((SoftwareSystemInstance)element); + } else if (element instanceof Container) { + // find instances of this container + view.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getContainer().equals(element) && ci.getEnvironment().equals(view.getEnvironment())).forEach(view::remove); + } else if (element instanceof ContainerInstance) { + view.remove((ContainerInstance)element); + } + } + + private void addRelationshipToView(Relationship relationship, DeploymentView view) { + if (view.isElementInView(relationship.getSource()) && view.isElementInView(relationship.getDestination())) { + view.add(relationship); + } else { + // we have a relationship, but the source and/or destination elements are not present in the view + // if both sides are static structure elements, then perhaps there's a replicated version of the relationship that should be added instead + Element sourceElement = relationship.getSource(); + Element destinationElement = relationship.getDestination(); + + if ((sourceElement instanceof SoftwareSystem || sourceElement instanceof Container) && (destinationElement instanceof SoftwareSystem || destinationElement instanceof Container)) { + String relationshipId = relationship.getId(); + + Set replicatedRelationships = view.getModel().getRelationships().stream().filter(r -> relationshipId.equals(r.getLinkedRelationshipId())).collect(Collectors.toSet()); + for (Relationship replicatedRelationship : replicatedRelationships) { + if (view.isElementInView(replicatedRelationship.getSource()) && view.isElementInView(replicatedRelationship.getDestination())) { + view.add(replicatedRelationship); + } + } + } + } + } + + @Override + protected void removeRelationshipFromView(Relationship relationship, ModelView view) { + // remove the specified relationship + view.remove(relationship); + + // and also remove any replicated versions of the specified relationship + Collection replicatedRelationshipsInView = view.getRelationships().stream().map(RelationshipView::getRelationship).filter(r -> r.getLinkedRelationshipId() != null && r.getLinkedRelationshipId().equals(relationship.getId())).collect(Collectors.toSet()); + for (Relationship r : replicatedRelationshipsInView) { + view.remove(r); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewDslContext.java new file mode 100644 index 000000000..7aca919f2 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewDslContext.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.view.DeploymentView; + +final class DeploymentViewDslContext extends ModelViewDslContext { + + DeploymentViewDslContext(DeploymentView view) { + super(view); + } + + DeploymentView getView() { + return (DeploymentView)super.getView(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.INCLUDE_IN_VIEW_TOKEN, + StructurizrDslTokens.EXCLUDE_IN_VIEW_TOKEN, + StructurizrDslTokens.AUTOLAYOUT_VIEW_TOKEN, + StructurizrDslTokens.DEFAULT_VIEW_TOKEN, + StructurizrDslTokens.ANIMATION_IN_VIEW_TOKEN, + StructurizrDslTokens.VIEW_TITLE_TOKEN, + StructurizrDslTokens.VIEW_DESCRIPTION_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java new file mode 100644 index 000000000..1919888d4 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java @@ -0,0 +1,70 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; + +final class DeploymentViewExpressionParser extends AbstractExpressionParser { + + @Override + protected Set evaluateElementTypeExpression(String expr, DslContext context) { + Set elements = new LinkedHashSet<>(); + + String type = expr.substring(ELEMENT_TYPE_EQUALS_EXPRESSION.length()); + switch (type.toLowerCase()) { + case "custom": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof CustomElement).forEach(elements::add); + break; + case "deploymentnode": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).forEach(elements::add); + break; + case "infrastructurenode": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).forEach(elements::add); + break; + case "softwaresystem": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof SoftwareSystem).forEach(elements::add); + break; + case "softwaresysteminstance": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).forEach(elements::add); + break; + case "container": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Container).forEach(elements::add); + break; + case "containerinstance": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).forEach(elements::add); + break; + default: + throw new RuntimeException("The element type of \"" + type + "\" is not valid for this view"); + } + + return elements; + } + + protected Set findAfferentCouplings(Element element) { + Set elements = new LinkedHashSet<>(); + + elements.addAll(findAfferentCouplings(element, CustomElement.class)); + elements.addAll(findAfferentCouplings(element, DeploymentNode.class)); + elements.addAll(findAfferentCouplings(element, InfrastructureNode.class)); + elements.addAll(findAfferentCouplings(element, SoftwareSystemInstance.class)); + elements.addAll(findAfferentCouplings(element, ContainerInstance.class)); + + return elements; + } + + protected Set findEfferentCouplings(Element element) { + Set elements = new LinkedHashSet<>(); + + elements.addAll(findEfferentCouplings(element, CustomElement.class)); + elements.addAll(findEfferentCouplings(element, DeploymentNode.class)); + elements.addAll(findEfferentCouplings(element, InfrastructureNode.class)); + elements.addAll(findEfferentCouplings(element, SoftwareSystemInstance.class)); + elements.addAll(findEfferentCouplings(element, ContainerInstance.class)); + + return elements; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewParser.java new file mode 100644 index 000000000..8794c680e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewParser.java @@ -0,0 +1,85 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.DeploymentView; + +final class DeploymentViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "deployment <*|software system identifier> [key] [description] {"; + + private static final String VIEW_TYPE = "Deployment"; + + private static final int SCOPE_IDENTIFIER_INDEX = 1; + private static final int ENVIRONMENT_INDEX = 2; + private static final int KEY_INDEX = 3; + private static final int DESCRIPTION_INDEX = 4; + + DeploymentView parse(DslContext context, Tokens tokens) { + // deployment <*|software system identifier> [key] [description] { + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(ENVIRONMENT_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + Workspace workspace = context.getWorkspace(); + String key = ""; + String scopeIdentifier = tokens.get(SCOPE_IDENTIFIER_INDEX); + String environment = tokens.get(ENVIRONMENT_INDEX); + if (context.getElement(environment) != null && context.getElement(environment) instanceof DeploymentEnvironment) { + environment = context.getElement(environment).getName(); + } + + // check that the deployment environment exists in the model + final String env = environment; + if (context.getWorkspace().getModel().getDeploymentNodes().stream().noneMatch(dn -> dn.getEnvironment().equals(env))) { + throw new RuntimeException("The environment \"" + environment + "\" does not exist"); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + DeploymentView view; + + if ("*".equals(scopeIdentifier)) { + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + view = workspace.getViews().createDeploymentView(key, description); + } else { + Element element = context.getElement(scopeIdentifier); + if (element == null) { + throw new RuntimeException("The software system \"" + scopeIdentifier + "\" does not exist"); + } + + if (element instanceof SoftwareSystem) { + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + view = workspace.getViews().createDeploymentView((SoftwareSystem)element, key, description); + } else { + throw new RuntimeException("The element \"" + scopeIdentifier + "\" is not a software system"); + } + } + + view.setEnvironment(environment); + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java new file mode 100644 index 000000000..4eaba7a23 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java @@ -0,0 +1,80 @@ +package com.structurizr.dsl; + +import com.structurizr.documentation.Documentable; +import com.structurizr.importer.documentation.DefaultImageImporter; +import com.structurizr.importer.documentation.DocumentationImporter; + +import java.io.File; +import java.lang.reflect.Constructor; + +final class DocsParser extends AbstractParser { + + private static final String DEFAULT_DOCUMENT_IMPORTER = "com.structurizr.importer.documentation.DefaultDocumentationImporter"; + + private static final String GRAMMAR = "!docs "; + + private static final int PATH_INDEX = 1; + private static final int FQN_INDEX = 2; + + void parse(WorkspaceDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getWorkspace(), dslFile, tokens); + } + + void parse(SoftwareSystemDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getSoftwareSystem(), dslFile, tokens); + } + + void parse(ContainerDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getContainer(), dslFile, tokens); + } + + void parse(ComponentDslContext context, File dslFile, Tokens tokens) { + parse(context, context.getComponent(), dslFile, tokens); + } + + private void parse(DslContext context, Documentable documentable, File dslFile, Tokens tokens) { + // !docs + + if (tokens.hasMoreThan(FQN_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(PATH_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String fullyQualifiedClassName = DEFAULT_DOCUMENT_IMPORTER; + if (tokens.includes(FQN_INDEX)) { + fullyQualifiedClassName = tokens.get(FQN_INDEX); + } + + if (dslFile != null) { + File path = new File(dslFile.getParentFile(), tokens.get(PATH_INDEX)); + + if (!path.exists()) { + throw new RuntimeException("Documentation path " + path + " does not exist"); + } + + if (!path.isDirectory()) { + throw new RuntimeException("Documentation path " + path + " is not a directory"); + } + + try { + Class documentationImporterClass = context.loadClass(fullyQualifiedClassName, dslFile); + Constructor constructor = documentationImporterClass.getDeclaredConstructor(); + DocumentationImporter documentationImporter = (DocumentationImporter)constructor.newInstance(); + documentationImporter.importDocumentation(documentable, path); + + if (!tokens.includes(FQN_INDEX)) { + DefaultImageImporter imageImporter = new DefaultImageImporter(); + imageImporter.importDocumentation(documentable, path); + } + } catch (ClassNotFoundException cnfe) { + throw new RuntimeException("Error importing documentation from " + path.getAbsolutePath() + ": " + fullyQualifiedClassName + " was not found"); + } catch (Exception e) { + throw new RuntimeException("Error importing documentation from " + path.getAbsolutePath() + ": " + e.getMessage()); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java new file mode 100644 index 000000000..ef529853c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java @@ -0,0 +1,126 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; + +abstract class DslContext { + + private static final String PLUGINS_DIRECTORY_NAME = "plugins"; + + static final String CONTEXT_START_TOKEN = "{"; + static final String CONTEXT_END_TOKEN = "}"; + + private Workspace workspace; + private boolean extendingWorkspace; + + protected IdentifiersRegister identifiersRegister = new IdentifiersRegister(); + + Workspace getWorkspace() { + return workspace; + } + + void setWorkspace(Workspace workspace) { + this.workspace = workspace; + } + + boolean isExtendingWorkspace() { + return extendingWorkspace; + } + + void setExtendingWorkspace(boolean extendingWorkspace) { + this.extendingWorkspace = extendingWorkspace; + } + + void setIdentifierRegister(IdentifiersRegister identifersRegister) { + this.identifiersRegister = identifersRegister; + } + + Element getElement(String identifier) { + return getElement(identifier, null); + } + + Element getElement(String identifier, Class type) { + Element element = null; + identifier = identifier.toLowerCase(); + + if (identifiersRegister.getIdentifierScope() == IdentifierScope.Hierarchical) { + if (this instanceof ModelItemDslContext) { + ModelItemDslContext modelItemDslContext = (ModelItemDslContext)this; + if (modelItemDslContext.getModelItem() instanceof Element) { + Element parent = (Element)modelItemDslContext.getModelItem(); + while (parent != null && element == null) { + String parentIdentifier = identifiersRegister.findIdentifier(parent); + + element = identifiersRegister.getElement(parentIdentifier + "." + identifier); + parent = parent.getParent(); + + element = checkElementType(element, type); + } + } + } else if (this instanceof DeploymentEnvironmentDslContext) { + DeploymentEnvironmentDslContext deploymentEnvironmentDslContext = (DeploymentEnvironmentDslContext)this; + DeploymentEnvironment deploymentEnvironment = new DeploymentEnvironment(deploymentEnvironmentDslContext.getEnvironment()); + String parentIdentifier = identifiersRegister.findIdentifier(deploymentEnvironment); + + element = identifiersRegister.getElement(parentIdentifier + "." + identifier); + } + + if (element == null) { + // default to finding a top-level element + element = identifiersRegister.getElement(identifier); + } + } else { + element = identifiersRegister.getElement(identifier); + } + + element = checkElementType(element, type); + return element; + } + + Element checkElementType(Element element, Class type) { + if (element != null && type != null) { + if (!element.getClass().isAssignableFrom(type)) { + element = null; + } + } + + return element; + } + + Relationship getRelationship(String identifier) { + return identifiersRegister.getRelationship(identifier.toLowerCase()); + } + + protected Class loadClass(String fqn, File dslFile) throws Exception { + File pluginsDirectory = new File(dslFile.getParent(), PLUGINS_DIRECTORY_NAME); + URL[] urls = new URL[0]; + + if (pluginsDirectory.exists()) { + File[] jarFiles = pluginsDirectory.listFiles((dir, name) -> name.endsWith(".jar")); + if (jarFiles != null) { + urls = new URL[jarFiles.length]; + for (int i = 0; i < jarFiles.length; i++) { + try { + urls[i] = jarFiles[i].toURI().toURL(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + URLClassLoader childClassLoader = new URLClassLoader(urls, getClass().getClassLoader()); + return childClassLoader.loadClass(fqn); + } + + void end() { + } + + protected abstract String[] getPermittedTokens(); + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java new file mode 100644 index 000000000..6c8c99944 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java @@ -0,0 +1,24 @@ +package com.structurizr.dsl; + +/** + * Represents a line of DSL, and its line number from the source file. + */ +class DslLine { + + private final String source; + private final int lineNumber; + + DslLine(String source, int lineNumber) { + this.source = source; + this.lineNumber = lineNumber; + } + + String getSource() { + return source; + } + + int getLineNumber() { + return lineNumber; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java new file mode 100644 index 000000000..f7f7491d8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java @@ -0,0 +1,38 @@ +package com.structurizr.dsl; + +import java.io.File; + +final class DslParserContext extends DslContext { + + private boolean restricted; + private File file; + + DslParserContext(File file, boolean restricted) { + this.file = file; + this.restricted = restricted; + } + + File getFile() { + return file; + } + + boolean isRestricted() { + return restricted; + } + + void copyFrom(IdentifiersRegister identifersRegister) { + for (String identifier : identifersRegister.getElementIdentifiers()) { + this.identifiersRegister.register(identifier, identifersRegister.getElement(identifier)); + } + + for (String identifier : identifersRegister.getRelationshipIdentifiers()) { + this.identifiersRegister.register(identifier, identifersRegister.getRelationship(identifier)); + } + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java new file mode 100644 index 000000000..16e73789e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java @@ -0,0 +1,48 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Utility methods to get/set DSL on a workspace. + */ +public class DslUtils { + + private static final String STRUCTURIZR_DSL_PROPERTY_NAME = "structurizr.dsl"; + + /** + * Gets the DSL associated with a workspace. + * + * @param workspace a Workspace object + * @return a DSL string + */ + public static String getDsl(Workspace workspace) { + String base64 = workspace.getProperties().get(STRUCTURIZR_DSL_PROPERTY_NAME); + String dsl = ""; + + if (!StringUtils.isNullOrEmpty(base64)) { + dsl = new String(Base64.getDecoder().decode(base64)); + } + + return dsl; + } + + /** + * Sets the DSL associated with a workspace. + * + * @param workspace a Workspace object + * @param dsl the DSL string + */ + public static void setDsl(Workspace workspace, String dsl) { + String base64 = ""; + if (!StringUtils.isNullOrEmpty(dsl)) { + base64 = Base64.getEncoder().encodeToString(dsl.getBytes(StandardCharsets.UTF_8)); + } + + workspace.addProperty(STRUCTURIZR_DSL_PROPERTY_NAME, base64); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java new file mode 100644 index 000000000..e96f6babf --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java @@ -0,0 +1,100 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.model.StaticStructureElement; +import com.structurizr.view.DynamicView; +import com.structurizr.view.RelationshipView; + +final class DynamicViewContentParser extends AbstractParser { + + private static final String GRAMMAR_1 = " -> [description] [technology]"; + private static final String GRAMMAR_2 = " [description]"; + + private static final int SOURCE_IDENTIFIER_INDEX = 0; + private static final int RELATIONSHIP_TOKEN_INDEX = 1; + private static final int DESTINATION_IDENTIFIER_INDEX = 2; + private static final int DESCRIPTION_INDEX = 3; + private static final int TECHNOLOGY_INDEX = 4; + + private static final int RELATIONSHIP_IDENTIFIER_INDEX = 0; + + RelationshipView parseRelationship(DynamicViewDslContext context, Tokens tokens) { + DynamicView view = context.getView(); + + if (tokens.size() > 1 && StructurizrDslTokens.RELATIONSHIP_TOKEN.equals(tokens.get(RELATIONSHIP_TOKEN_INDEX))) { + // -> [description] [technology] + if (tokens.hasMoreThan(TECHNOLOGY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_1); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR_1); + } + + String sourceId = tokens.get(SOURCE_IDENTIFIER_INDEX); + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + + Element sourceElement = context.getElement(sourceId); + if (sourceElement == null) { + throw new RuntimeException("The source element \"" + sourceId + "\" does not exist"); + } + + if (!(sourceElement instanceof StaticStructureElement || sourceElement instanceof CustomElement)) { + throw new RuntimeException("The source element \"" + sourceId + "\" should be a static structure or custom element"); + } + + Element destinationElement = context.getElement(destinationId); + if (destinationElement == null) { + throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); + } + + if (!(destinationElement instanceof StaticStructureElement || destinationElement instanceof CustomElement)) { + throw new RuntimeException("The destination element \"" + destinationId + "\" should be a static structure or custom element"); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + + if (sourceElement instanceof StaticStructureElement && destinationElement instanceof StaticStructureElement) { + return view.add((StaticStructureElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); + } else if (sourceElement instanceof StaticStructureElement && destinationElement instanceof CustomElement) { + return view.add((StaticStructureElement) sourceElement, description, technology, (CustomElement) destinationElement); + } else if (sourceElement instanceof CustomElement && destinationElement instanceof StaticStructureElement) { + return view.add((CustomElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); + } else if (sourceElement instanceof CustomElement && destinationElement instanceof CustomElement) { + return view.add((CustomElement) sourceElement, description, technology, (CustomElement) destinationElement); + } + } else { + // [description] [technology] + String relationshipId = tokens.get(RELATIONSHIP_IDENTIFIER_INDEX); + Relationship relationship = context.getRelationship(relationshipId); + + if (tokens.hasMoreThan(RELATIONSHIP_IDENTIFIER_INDEX+1)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_2); + } + + if (relationship == null) { + throw new RuntimeException("The relationship \"" + relationshipId + "\" does not exist"); + } + + String description = ""; + if (tokens.includes(RELATIONSHIP_IDENTIFIER_INDEX+1)) { + description = tokens.get(RELATIONSHIP_IDENTIFIER_INDEX+1); + } + + return view.add(relationship, description); + } + + throw new RuntimeException("The specified relationship could not be added"); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewDslContext.java new file mode 100644 index 000000000..6ec05d24e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewDslContext.java @@ -0,0 +1,27 @@ +package com.structurizr.dsl; + +import com.structurizr.view.DynamicView; + +class DynamicViewDslContext extends ModelViewDslContext { + + DynamicViewDslContext(DynamicView view) { + super(view); + } + + DynamicView getView() { + return (DynamicView)super.getView(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.AUTOLAYOUT_VIEW_TOKEN, + StructurizrDslTokens.DEFAULT_VIEW_TOKEN, + StructurizrDslTokens.VIEW_TITLE_TOKEN, + StructurizrDslTokens.VIEW_DESCRIPTION_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParallelSequenceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParallelSequenceDslContext.java new file mode 100644 index 000000000..316bf07a4 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParallelSequenceDslContext.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class DynamicViewParallelSequenceDslContext extends DynamicViewDslContext { + + private boolean relationships = false; + + DynamicViewParallelSequenceDslContext(DynamicViewDslContext context) { + super(context.getView()); + getView().startParallelSequence(); + } + + void hasRelationships(boolean definesRelationships) { + this.relationships = definesRelationships; + } + + @Override + void end() { + getView().endParallelSequence(!relationships); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java new file mode 100644 index 000000000..78b23103e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java @@ -0,0 +1,89 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Container; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.DynamicView; + +import java.text.DecimalFormat; + +class DynamicViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "dynamic <*|software system identifier|container identifier> [key] [description] {"; + + private static final String VIEW_TYPE = "Dynamic"; + + private static final int SCOPE_IDENTIFIER_INDEX = 1; + private static final int KEY_INDEX = 2; + private static final int DESCRIPTION_INDEX = 3; + + private static final String WILDCARD = "*"; + + DynamicView parse(DslContext context, Tokens tokens) { + // dynamic <*|software system identifier|container identifier> [key] [description] { + + Workspace workspace = context.getWorkspace(); + String key = ""; + String description = ""; + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(SCOPE_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + DynamicView view; + + String scopeIdentifier = tokens.get(SCOPE_IDENTIFIER_INDEX); + if (WILDCARD.equals(scopeIdentifier)) { + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + view = workspace.getViews().createDynamicView(key, description); + } else { + Element element = context.getElement(scopeIdentifier); + if (element == null) { + throw new RuntimeException("The software system or container \"" + scopeIdentifier + "\" does not exist"); + } + + if (element instanceof SoftwareSystem) { + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + view = workspace.getViews().createDynamicView((SoftwareSystem)element, key, description); + } else if (element instanceof Container) { + Container container = (Container)element; + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + view = workspace.getViews().createDynamicView((Container)element, key, description); + } else { + throw new RuntimeException("The element \"" + scopeIdentifier + "\" is not a software system or container"); + } + } + + view.setExternalBoundariesVisible(true); + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipContext.java new file mode 100644 index 000000000..910ef6199 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipContext.java @@ -0,0 +1,26 @@ +package com.structurizr.dsl; + +import com.structurizr.view.RelationshipView; + +class DynamicViewRelationshipContext extends DslContext { + + private final RelationshipView relationshipView; + + DynamicViewRelationshipContext(RelationshipView relationshipView) { + super(); + + this.relationshipView = relationshipView; + } + + RelationshipView getRelationshipView() { + return relationshipView; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.URL_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipParser.java new file mode 100644 index 000000000..5562e4d39 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewRelationshipParser.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class DynamicViewRelationshipParser extends AbstractParser { + + private final static int URL_INDEX = 1; + + void parseUrl(DynamicViewRelationshipContext context, Tokens tokens) { + // url + if (tokens.hasMoreThan(URL_INDEX)) { + throw new RuntimeException("Too many tokens, expected: url "); + } + + if (!tokens.includes(URL_INDEX)) { + throw new RuntimeException("Expected: url "); + } + + String url = tokens.get(URL_INDEX); + context.getRelationshipView().setUrl(url); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java new file mode 100644 index 000000000..751dc1b0f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java @@ -0,0 +1,74 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.Model; +import com.structurizr.util.StringUtils; + +import java.util.HashSet; +import java.util.Set; + +class ElementGroup extends Element { + + private static final String STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator"; + + private Element parent; + private final ElementGroup parentGroup; + private final String name; + + private final Set elements = new HashSet<>(); + + ElementGroup(Model model, String name) { + setModel(model); + this.name = name; + this.parentGroup = null; + } + + ElementGroup(Model model, String name, ElementGroup parentGroup) { + setModel(model); + String groupSeparator = getModel().getProperties().getOrDefault(STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME, ""); + + if (StringUtils.isNullOrEmpty(groupSeparator)) { + throw new RuntimeException("To use nested groups, please define a model property named " + STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME); + } + + this.name = parentGroup.getName() + groupSeparator + name; + this.parentGroup = parentGroup; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getCanonicalName() { + return name; + } + + void setParent(Element parent) { + this.parent = parent; + } + + @Override + public Element getParent() { + return parent; + } + + @Override + public Set getDefaultTags() { + return null; + } + + void addElement(Element element) { + elements.add(element); + + if (parentGroup != null) { + parentGroup.addElement(element); + } + } + + Set getElements() { + return new HashSet<>(elements); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java new file mode 100644 index 000000000..1ab73cbe3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java @@ -0,0 +1,46 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ElementStyle; + +import java.io.File; + +final class ElementStyleDslContext extends DslContext { + + private File file; + private ElementStyle style; + + ElementStyleDslContext(ElementStyle style, File file) { + this.style = style; + this.file = file; + } + + File getFile() { + return file; + } + + ElementStyle getStyle() { + return style; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ELEMENT_STYLE_SHAPE_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_ICON_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_WIDTH_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_HEIGHT_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_BACKGROUND_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_COLOR_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_COLOUR_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_STROKE_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_STROKE_WIDTH_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_FONT_SIZE_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_BORDER_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_OPACITY_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_METADATA_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_DESCRIPTION_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java new file mode 100644 index 000000000..1769ec5c9 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java @@ -0,0 +1,324 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; +import com.structurizr.view.Border; +import com.structurizr.view.ElementStyle; +import com.structurizr.view.Shape; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +final class ElementStyleParser extends AbstractParser { + + private static final int FIRST_PROPERTY_INDEX = 1; + + ElementStyle parseElementStyle(DslContext context, Tokens tokens) { + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: element {"); + } else if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String tag = tokens.get(1); + + if (StringUtils.isNullOrEmpty(tag)) { + throw new RuntimeException("A tag must be specified"); + } + + Workspace workspace = context.getWorkspace(); + ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle(tag); + if (elementStyle == null) { + elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle(tag); + } + + return elementStyle; + } else { + throw new RuntimeException("Expected: element {"); + } + } + + void parseShape(ElementStyleDslContext context, Tokens tokens) { + Map shapes = new HashMap<>(); + String shapesAsString = ""; + for (Shape shape : Shape.values()) { + shapes.put(shape.toString().toLowerCase(), shape); + shapesAsString += shape; + shapesAsString += "|"; + } + shapesAsString = shapesAsString.substring(0, shapesAsString.length()-1); + + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: shape <" + shapesAsString + ">"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String shape = tokens.get(1).toLowerCase(); + + if (shapes.containsKey(shape)) { + style.setShape(shapes.get(shape)); + } else { + throw new RuntimeException("The shape \"" + shape + "\" is not valid"); + } + } else { + throw new RuntimeException("Expected: shape <" + shapesAsString + ">"); + } + } + + void parseBackground(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: background <#rrggbb|color name>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String colour = tokens.get(1); + style.setBackground(colour); + } else { + throw new RuntimeException("Expected: background <#rrggbb|color name>"); + } + } + + void parseStroke(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: stroke <#rrggbb|color name>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String colour = tokens.get(1); + style.setStroke(colour); + } else { + throw new RuntimeException("Expected: stroke <#rrggbb|color name>"); + } + } + + void parseStrokeWidth(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: strokeWidth <1-10>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String strokeWidthAsString = tokens.get(1); + + try { + int strokeWidth = Integer.parseInt(strokeWidthAsString); + style.setStrokeWidth(strokeWidth); + } catch (NumberFormatException e) { + throw new RuntimeException("Stroke width must be an integer between 1 and 10"); + } + } else { + throw new RuntimeException("Expected: strokeWidth <1-10>"); + } + } + + void parseColour(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: colour <#rrggbb|color name>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String colour = tokens.get(1); + style.setColor(colour); + } else { + throw new RuntimeException("Expected: colour <#rrggbb|color name>"); + } + } + + void parseBorder(ElementStyleDslContext context, Tokens tokens) { + Map borders = new HashMap<>(); + for (Border border : Border.values()) { + borders.put(border.toString().toLowerCase(), border); + } + + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: border "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String border = tokens.get(1).toLowerCase(); + + if (borders.containsKey(border)) { + style.setBorder(borders.get(border)); + } else { + throw new RuntimeException("The border \"" + border + "\" is not valid"); + } + } else { + throw new RuntimeException("Expected: border "); + } + } + + void parseOpacity(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: opacity <0-100>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String opacityAsString = tokens.get(1); + + try { + int opacity = Integer.parseInt(opacityAsString); + style.setOpacity(opacity); + } catch (NumberFormatException e) { + throw new RuntimeException("Opacity must be an integer between 0 and 100"); + } + } else { + throw new RuntimeException("Expected: opacity <0-100>"); + } + } + + void parseWidth(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: width "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String widthAsString = tokens.get(1); + + try { + int width = Integer.parseInt(widthAsString); + style.setWidth(width); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Width must be a positive integer"); + } + } else { + throw new RuntimeException("Expected: width "); + } + } + + void parseHeight(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: height "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String heightAsString = tokens.get(1); + + try { + int height = Integer.parseInt(heightAsString); + style.setHeight(height); + } catch (NumberFormatException e) { + throw new RuntimeException("Height must be a positive integer"); + } + } else { + throw new RuntimeException("Expected: height "); + } + } + + void parseFontSize(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: fontSize "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String fontSizeAsString = tokens.get(1); + + try { + int fontSize = Integer.parseInt(fontSizeAsString); + style.setFontSize(fontSize); + } catch (NumberFormatException e) { + throw new RuntimeException("Font size must be a positive integer"); + } + } else { + throw new RuntimeException("Expected: fontSize "); + } + } + + void parseMetadata(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: metadata "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String metadata = tokens.get(1); + + if ("true".equalsIgnoreCase(metadata)) { + style.setMetadata(true); + } else if ("false".equalsIgnoreCase(metadata)) { + style.setMetadata(false); + } else { + throw new RuntimeException("Metadata must be true or false"); + } + } else { + throw new RuntimeException("Expected: metadata "); + } + } + + void parseDescription(ElementStyleDslContext context, Tokens tokens) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: description "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String description = tokens.get(1); + + if ("true".equalsIgnoreCase(description)) { + style.setDescription(true); + } else if ("false".equalsIgnoreCase(description)) { + style.setDescription(false); + } else { + throw new RuntimeException("Description must be true or false"); + } + } else { + throw new RuntimeException("Expected: description "); + } + } + + void parseIcon(ElementStyleDslContext context, Tokens tokens, boolean restricted) { + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: icon "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String path = tokens.get(1); + + if (path.startsWith("data:image/") || path.startsWith("https://") || path.startsWith("http://")) { + if (IconUtils.isSupported(path)) { + style.setIcon(path); + } else { + throw new IllegalArgumentException("Only PNG and JPG URLs/data URIs are supported: " + path); + } + } else { + if (!restricted) { + File file = new File(context.getFile().getParent(), path); + if (file.exists() && !file.isDirectory()) { + try { + style.setIcon(ImageUtils.getImageAsDataUri(file)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException(path + " does not exist"); + } + } + } + } else { + throw new RuntimeException("Expected: icon "); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java new file mode 100644 index 000000000..3fe12598e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java @@ -0,0 +1,23 @@ +package com.structurizr.dsl; + +final class EnterpriseDslContext extends GroupableDslContext { + + EnterpriseDslContext() { + super(); + } + + EnterpriseDslContext(ElementGroup group) { + super(group); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.GROUP_TOKEN, + StructurizrDslTokens.PERSON_TOKEN, + StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java new file mode 100644 index 000000000..54cbd3bfb --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java @@ -0,0 +1,30 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Enterprise; + +final class EnterpriseParser extends AbstractParser { + + private static final String GRAMMAR = "enterprise [name]"; + + private static final int NAME_INDEX = 1; + + void parse(DslContext context, Tokens tokens) { + Workspace workspace = context.getWorkspace(); + + if (tokens.hasMoreThan(NAME_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } else if (tokens.includes(NAME_INDEX)) { + String name = tokens.get(1); + + if (workspace.getModel().getEnterprise() == null) { + workspace.getModel().setEnterprise(new Enterprise(name)); + } else if (!name.equals(workspace.getModel().getEnterprise().getName())) { + throw new RuntimeException("The name of the enterprise has already been set"); + } + } else { + // do nothing ... this will just create an EnterpriseDslContext, so that people and software systems can be marked as "internal" + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java new file mode 100644 index 000000000..055167d93 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -0,0 +1,72 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +final class ExplicitRelationshipParser extends AbstractRelationshipParser { + + private static final String GRAMMAR = " -> [description] [technology] [tags]"; + + private static final int SOURCE_IDENTIFIER_INDEX = 0; + private static final int DESTINATION_IDENTIFIER_INDEX = 2; + private final static int DESCRIPTION_INDEX = 3; + private final static int TECHNOLOGY_INDEX = 4; + private final static int TAGS_INDEX = 5; + + Relationship parse(DslContext context, Tokens tokens) { + // -> [description] [technology] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String sourceId = tokens.get(SOURCE_IDENTIFIER_INDEX); + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + + Element sourceElement = context.getElement(sourceId); + Element destinationElement = context.getElement(destinationId); + + if (sourceElement == null) { + if (StructurizrDslTokens.THIS_TOKEN.equalsIgnoreCase(sourceId) && context instanceof GroupableElementDslContext) { + GroupableElementDslContext groupableElementDslContext = (GroupableElementDslContext)context; + sourceElement = groupableElementDslContext.getElement(); + } else { + throw new RuntimeException("The source element \"" + sourceId + "\" does not exist"); + } + } + + if (destinationElement == null) { + if (StructurizrDslTokens.THIS_TOKEN.equalsIgnoreCase(destinationId) && context instanceof ModelItemDslContext) { + ModelItemDslContext modelItemDslContext = (ModelItemDslContext) context; + if (modelItemDslContext.getModelItem() instanceof Element) { + destinationElement = (Element)modelItemDslContext.getModelItem(); + } + } + } + + if (destinationElement == null) { + throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + + String[] tags = new String[0]; + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX).split(","); + } + + return createRelationship(sourceElement, description, technology, tags, destinationElement); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java new file mode 100644 index 000000000..53305c12f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java @@ -0,0 +1,41 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +class ExternalScriptDslContext extends ScriptDslContext { + + private final String filename; + + ExternalScriptDslContext(DslContext parentContext, File dslFile, String filename) { + super(parentContext, dslFile); + + this.filename = filename; + } + + @Override + void end() { + try { + File scriptFile = new File(dslFile.getParent(), filename); + if (!scriptFile.exists()) { + throw new RuntimeException("Script file " + scriptFile.getCanonicalPath() + " does not exist"); + } + + String fileExtension = filename.substring(filename.lastIndexOf('.') + 1); + List lines = Files.readAllLines(scriptFile.toPath(), StandardCharsets.UTF_8); + + run(this, fileExtension, lines); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Error running script at " + filename + ", caused by " + e.getClass().getName() + ": " + e.getMessage()); + } + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FileUtils.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FileUtils.java new file mode 100644 index 000000000..4f1279c62 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FileUtils.java @@ -0,0 +1,46 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +final class FileUtils { + + private static final String STRUCTURIZR_DSL_FILE_EXTENSION = ".dsl"; + + static List findFiles(File path) { + List files = new ArrayList<>(); + if (path.isDirectory()) { + files = findFilesInDirectory(path); + } else { + files.add(path); + } + + return files; + } + + private static List findFilesInDirectory(File directory) { + List files = new ArrayList<>(); + + File[] filesInDirectory = directory.listFiles(); + if (filesInDirectory == null || filesInDirectory.length == 0) { + return files; + } + + Arrays.sort(filesInDirectory); + + for (File file : filesInDirectory) { + if (!file.isDirectory() && file.getName().endsWith(STRUCTURIZR_DSL_FILE_EXTENSION)) { + files.add(file); + } + + if (file.isDirectory()) { + files.addAll(findFilesInDirectory(file)); + } + } + + return files; + } + +} diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewDslContext.java new file mode 100644 index 000000000..d385e6bf0 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewDslContext.java @@ -0,0 +1,25 @@ +package com.structurizr.dsl; + +import com.structurizr.view.FilteredView; + +class FilteredViewDslContext extends ViewDslContext { + + FilteredViewDslContext(FilteredView view) { + super(view); + } + + FilteredView getView() { + return (FilteredView)super.getView(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DEFAULT_VIEW_TOKEN, + StructurizrDslTokens.VIEW_TITLE_TOKEN, + StructurizrDslTokens.VIEW_DESCRIPTION_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java new file mode 100644 index 000000000..f90dd9de0 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java @@ -0,0 +1,88 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.StringUtils; +import com.structurizr.view.FilterMode; +import com.structurizr.view.FilteredView; +import com.structurizr.view.StaticView; + +import java.text.DecimalFormat; +import java.util.HashSet; +import java.util.Set; + +final class FilteredViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "filtered [key] [description]"; + + private static final String VIEW_TYPE = "Filtered"; + + private static final int BASE_KEY_INDEX = 1; + private static final int MODE_INDEX = 2; + private static final int TAGS_INDEX = 3; + private static final int KEY_INDEX = 4; + private static final int DESCRIPTION_INDEX = 5; + + private static final String FILTER_MODE_INCLUDE = "include"; + private static final String FILTER_MODE_EXCLUDE = "exclude"; + + FilteredView parse(DslContext context, Tokens tokens) { + // filtered [key} [description] + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(TAGS_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + Workspace workspace = context.getWorkspace(); + String key; + + StaticView baseView; + String baseKey = tokens.get(BASE_KEY_INDEX); + String mode = tokens.get(MODE_INDEX); + String tagsAsString = tokens.get(TAGS_INDEX); + Set tags = new HashSet<>(); + + for (String tag : tagsAsString.split(",")) { + if (!StringUtils.isNullOrEmpty(tag)) { + tags.add(tag.trim()); + } + } + + String description = ""; + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + FilterMode filterMode; + if (FILTER_MODE_INCLUDE.equalsIgnoreCase(mode)) { + filterMode = FilterMode.Include; + } else if (FILTER_MODE_EXCLUDE.equalsIgnoreCase(mode)) { + filterMode = FilterMode.Exclude; + } else { + throw new RuntimeException("Filter mode should be include or exclude"); + } + + if (workspace.getViews().getViews().stream().noneMatch(v -> v.getKey().equals(baseKey))) { + throw new RuntimeException("The view \"" + baseKey + "\" does not exist"); + } + + baseView = (StaticView)workspace.getViews().getViews().stream().filter(v -> v instanceof StaticView && v.getKey().equals(baseKey)).findFirst().orElse(null); + if (baseView == null) { + throw new RuntimeException("The view \"" + baseKey + "\" must be a System Landscape, System Context, Container, or Component view"); + } + + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + return workspace.getViews().createFilteredView(baseView, key, description, filterMode, tags.toArray(new String[0])); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java new file mode 100644 index 000000000..8ff6e0fa9 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java @@ -0,0 +1,35 @@ +package com.structurizr.dsl; + +class GroupParser { + + private static final String GRAMMAR = "group {"; + + private final static int NAME_INDEX = 1; + private final static int BRACE_INDEX = 2; + + ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) { + // group { + + if (tokens.hasMoreThan(BRACE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(BRACE_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + if (!DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(tokens.get(BRACE_INDEX))) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + ElementGroup group; + if (dslContext.hasGroup()) { + group = new ElementGroup(dslContext.getWorkspace().getModel(), tokens.get(NAME_INDEX), dslContext.getGroup()); + } else { + group = new ElementGroup(dslContext.getWorkspace().getModel(), tokens.get(NAME_INDEX)); + } + + return group; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java new file mode 100644 index 000000000..5ac594434 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java @@ -0,0 +1,23 @@ +package com.structurizr.dsl; + +abstract class GroupableDslContext extends DslContext { + + private ElementGroup group; + + GroupableDslContext() { + this(null); + } + + GroupableDslContext(ElementGroup group) { + this.group = group; + } + + boolean hasGroup() { + return group != null; + } + + ElementGroup getGroup() { + return group; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java new file mode 100644 index 000000000..a26b58cd6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java @@ -0,0 +1,17 @@ +package com.structurizr.dsl; + +import com.structurizr.model.GroupableElement; + +abstract class GroupableElementDslContext extends ModelItemDslContext { + + GroupableElementDslContext() { + super(); + } + + GroupableElementDslContext(ElementGroup group) { + super(group); + } + + abstract GroupableElement getElement(); + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/HealthCheckParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/HealthCheckParser.java new file mode 100644 index 000000000..6cc6ab9a1 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/HealthCheckParser.java @@ -0,0 +1,61 @@ +package com.structurizr.dsl; + +import com.structurizr.model.StaticStructureElementInstance; + +class HealthCheckParser extends AbstractParser { + + private static final String GRAMMAR = "healthCheck [interval] [timeout]"; + + private final static int NAME_INDEX = 1; + private final static int URL_INDEX = 2; + private final static int INTERVAL_INDEX = 3; + private final static int TIMEOUT_INDEX = 4; + + private final static int DEFAULT_INTERVAL = 60; + private final static long DEFAULT_TIMEOUT = 0; + + void parse(StaticStructureElementInstanceDslContext context, Tokens tokens) { + // healthCheck [interval] [timeout] + + if (tokens.hasMoreThan(TIMEOUT_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(URL_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String name = tokens.get(NAME_INDEX); + String url = tokens.get(URL_INDEX); + int interval = DEFAULT_INTERVAL; + long timeout = DEFAULT_TIMEOUT; + + if (tokens.includes(INTERVAL_INDEX)) { + try { + interval = Integer.parseInt(tokens.get(INTERVAL_INDEX)); + + if (interval < 1) { + throw new RuntimeException("The interval must be a positive integer (number of seconds)"); + } + } catch (NumberFormatException e) { + throw new RuntimeException("The interval of \"" + tokens.get(INTERVAL_INDEX) + "\" is not valid - it must be a positive integer (number of seconds)"); + } + } + + if (tokens.includes(TIMEOUT_INDEX)) { + try { + timeout = Integer.parseInt(tokens.get(TIMEOUT_INDEX)); + + if (timeout < 0) { + throw new RuntimeException("The timeout must be zero or a positive integer (number of milliseconds)"); + } + } catch (NumberFormatException e) { + throw new RuntimeException("The timeout of \"" + tokens.get(TIMEOUT_INDEX) + "\" is not valid - it must be zero or a positive integer (number of milliseconds)"); + } + } + + StaticStructureElementInstance elementInstance = context.getElementInstance(); + elementInstance.addHealthCheck(name, url, interval, timeout); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java new file mode 100644 index 000000000..b503c0d5f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java @@ -0,0 +1,38 @@ +package com.structurizr.dsl; + +import com.structurizr.util.Url; + +class IconUtils { + + public static boolean isSupported(String url) { + url = url.trim(); + + if (Url.isUrl(url) && isSupportedUrl(url)) { + // all good + return true; + } + + if (url.startsWith("data:image")) { + if (isSupportedDataUri(url)) { + // all good + return true; + } else { + // it's a data URI, but not supported + return false; + } + } + + return false; + } + + private static boolean isSupportedDataUri(String uri) { + return uri.startsWith("data:image/png;base64,") || uri.startsWith("data:image/jpeg;base64,"); + } + + private static boolean isSupportedUrl(String url) { + url = url.toLowerCase(); + + return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg"); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScope.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScope.java new file mode 100644 index 000000000..1596bcc4b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScope.java @@ -0,0 +1,8 @@ +package com.structurizr.dsl; + +enum IdentifierScope { + + Flat, + Hierarchical, + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScopeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScopeParser.java new file mode 100644 index 000000000..93599f71e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifierScopeParser.java @@ -0,0 +1,30 @@ +package com.structurizr.dsl; + +final class IdentifierScopeParser extends AbstractParser { + + private static final String GRAMMAR = "!identifiers "; + + private static final int MODE_INDEX = 1; + + IdentifierScope parse(DslContext context, Tokens tokens) { + // !identifiers + + if (tokens.hasMoreThan(MODE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(MODE_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String name = tokens.get(MODE_INDEX); + if ("flat".equalsIgnoreCase(name)) { + return IdentifierScope.Flat; + } else if ("hierarchical".equalsIgnoreCase(name)) { + return IdentifierScope.Hierarchical; + } else { + throw new RuntimeException("Expected: " + GRAMMAR); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java new file mode 100644 index 000000000..1d5524552 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java @@ -0,0 +1,222 @@ +package com.structurizr.dsl; + +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +/** + * A register of elements and relationships that were created with an identifier in the DSL. + */ +public class IdentifiersRegister { + + private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("\\w[a-zA-Z0-9_-]*"); + + private IdentifierScope identifierScope = IdentifierScope.Flat; + + private final Map elementsByIdentifier = new HashMap<>(); + + private final Map relationshipsByIdentifier = new HashMap<>(); + + IdentifiersRegister() { + } + + /** + * Gets the identifier scope in use (i.e. Flat or Hierarchical ... applies to elements only). + * + * @return an IdentifierScope enum + */ + public IdentifierScope getIdentifierScope() { + return identifierScope; + } + + /** + * Sets the identifier scope (i.e. Flat or Hierarchical ... applies to elements only). + * + * @param identifierScope an IdentifierScope enum + */ + public void setIdentifierScope(IdentifierScope identifierScope) { + this.identifierScope = identifierScope; + } + + /** + * Gets the set of element identifiers. + * + * @return a Set of String identifiers + */ + public Set getElementIdentifiers() { + return elementsByIdentifier.keySet(); + } + + /** + * Gets the set of relationship identifiers. + * + * @return a Set of String identifiers + */ + public Set getRelationshipIdentifiers() { + return relationshipsByIdentifier.keySet(); + } + + /** + * Gets the element identified by the specified identifier. + * + * @param identifier a String identifier + * @return an Element, or null if one doesn't exist + */ + public Element getElement(String identifier) { + identifier = identifier.toLowerCase(); + return elementsByIdentifier.get(identifier); + } + + /** + * Registers an element with the given identifier. + * + * @param identifier an identifier + * @param element an Element instance + */ + public void register(String identifier, Element element) { + if (element == null) { + throw new IllegalArgumentException("An element must be specified"); + } + + if (StringUtils.isNullOrEmpty(identifier)) { + identifier = UUID.randomUUID().toString(); + } + + identifier = identifier.toLowerCase(); + + if (identifierScope == IdentifierScope.Hierarchical) { + identifier = calculateHierarchicalIdentifier(identifier, element); + } + + // check whether this element has already been registered with another identifier + for (String id : elementsByIdentifier.keySet()) { + Element e = elementsByIdentifier.get(id); + + if (e.equals(element) && !id.equals(identifier)) { + if (id.matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")) { + throw new RuntimeException("Please assign an identifier to \"" + element.getCanonicalName() + "\" before using it with !ref"); + } else { + throw new RuntimeException("The element is already registered with an identifier of \"" + id + "\""); + } + } + } + + Element e = elementsByIdentifier.get(identifier); + Relationship r = relationshipsByIdentifier.get(identifier); + + if ((e == null && r == null) || (e == element)) { + elementsByIdentifier.put(identifier, element); + } else { + throw new RuntimeException("The identifier \"" + identifier + "\" is already in use"); + } + } + + /** + * Gets the relationship identified by the specified identifier. + * + * @param identifier a String identifier + * @return a Relationship, or null if one doesn't exist + */ + public Relationship getRelationship(String identifier) { + identifier = identifier.toLowerCase(); + return relationshipsByIdentifier.get(identifier); + } + + /** + * Registers a relationship with the given identifier. + * + * @param identifier an identifier + * @param relationship a Relationship instance + */ + public void register(String identifier, Relationship relationship) { + if (relationship == null) { + throw new IllegalArgumentException("A relationship must be specified"); + } + + if (StringUtils.isNullOrEmpty(identifier)) { + identifier = UUID.randomUUID().toString(); + } + + identifier = identifier.toLowerCase(); + + Element e = elementsByIdentifier.get(identifier); + Relationship r = relationshipsByIdentifier.get(identifier); + + if ((e == null && r == null) || (r == relationship)) { + relationshipsByIdentifier.put(identifier, relationship); + } else { + throw new RuntimeException("The identifier \"" + identifier + "\" is already in use"); + } + } + + private String calculateHierarchicalIdentifier(String identifier, Element element) { + if (element.getParent() == null) { + if (element instanceof DeploymentNode) { + DeploymentNode dn = (DeploymentNode)element; + return findIdentifier(new DeploymentEnvironment(dn.getEnvironment())) + "." + identifier; + } else { + return identifier; + } + } else { + return findIdentifier(element.getParent()) + "." + identifier; + } + } + + /** + * Finds the identifier used when defining an element. + * + * @param element an Element instance + * @return a String identifier (could be null if no identifier was explicitly specified) + */ + public String findIdentifier(Element element) { + if (elementsByIdentifier.containsValue(element)) { + for (String identifier : elementsByIdentifier.keySet()) { + Element e = elementsByIdentifier.get(identifier); + + if (e.equals(element)) { + return identifier; + } + } + } + + return null; + } + + /** + * Finds the identifier used when defining a relationship. + * + * @param relationship a Relationship instance + * @return a String identifier (could be null if no identifier was explicitly specified, or for implied relationships) + */ + public String findIdentifier(Relationship relationship) { + if (relationshipsByIdentifier.containsValue(relationship)) { + for (String identifier : relationshipsByIdentifier.keySet()) { + Relationship r = relationshipsByIdentifier.get(identifier); + + if (r.equals(relationship)) { + return identifier; + } + } + } + + return null; + } + + void validateIdentifierName(String identifier) { + if (identifier.startsWith("-")) { + throw new RuntimeException("Identifiers cannot start with a - character"); + } + + if (!IDENTIFIER_PATTERN.matcher(identifier).matches()) { + throw new RuntimeException("Identifiers can only contain the following characters: a-zA-Z0-9_-"); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java new file mode 100644 index 000000000..5f9192af2 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -0,0 +1,163 @@ +package com.structurizr.dsl; + +import com.structurizr.importer.diagrams.kroki.KrokiImporter; +import com.structurizr.importer.diagrams.mermaid.MermaidImporter; +import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; +import com.structurizr.util.ImageUtils; +import com.structurizr.util.Url; +import com.structurizr.view.ImageView; + +import java.io.File; + +final class ImageViewContentParser extends AbstractParser { + + private static final String PLANTUML_GRAMMAR = "plantuml "; + private static final String MERMAID_GRAMMAR = "mermaid "; + private static final String KROKI_GRAMMAR = "kroki "; + private static final String IMAGE_GRAMMAR = "image "; + + private static final int TITLE_INDEX = 1; + private static final int DESCRIPTION_INDEX = 1; + + private static final int PLANTUML_SOURCE_INDEX = 1; + private static final int MERMAID_SOURCE_INDEX = 1; + private static final int KROKI_FORMAT_INDEX = 1; + private static final int KROKI_SOURCE_INDEX = 2; + private static final int IMAGE_SOURCE_INDEX = 1; + + private boolean restricted = false; + + ImageViewContentParser(boolean restricted) { + this.restricted = restricted; + } + + void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { + // plantuml + + if (tokens.hasMoreThan(PLANTUML_SOURCE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + PLANTUML_GRAMMAR); + } + + ImageView view = context.getView(); + if (view != null) { + if (tokens.size() == 2) { + String source = tokens.get(PLANTUML_SOURCE_INDEX); + + try { + if (Url.isUrl(source)) { + String content = readFromUrl(source); + new PlantUMLImporter().importDiagram(context.getView(), content); + context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + new PlantUMLImporter().importDiagram(context.getView(), file); + } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } else { + throw new RuntimeException("Expected: " + PLANTUML_GRAMMAR); + } + } + } + + void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { + // mermaid + + if (tokens.hasMoreThan(MERMAID_SOURCE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + MERMAID_GRAMMAR); + } + + ImageView view = context.getView(); + if (view != null) { + if (tokens.size() == 2) { + String source = tokens.get(MERMAID_SOURCE_INDEX); + + try { + if (Url.isUrl(source)) { + String content = readFromUrl(source); + new MermaidImporter().importDiagram(context.getView(), content); + context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + new MermaidImporter().importDiagram(context.getView(), file); + } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } else { + throw new RuntimeException("Expected: " + MERMAID_GRAMMAR); + } + } + } + + void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) { + // kroki + + if (tokens.hasMoreThan(KROKI_SOURCE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + KROKI_GRAMMAR); + } + + ImageView view = context.getView(); + if (view != null) { + if (tokens.size() == 3) { + String format = tokens.get(KROKI_FORMAT_INDEX); + String source = tokens.get(KROKI_SOURCE_INDEX); + + try { + if (Url.isUrl(source)) { + String content = readFromUrl(source); + new KrokiImporter().importDiagram(context.getView(), format, content); + context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + new KrokiImporter().importDiagram(context.getView(), format, file); + } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } else { + throw new RuntimeException("Expected: " + KROKI_GRAMMAR); + } + } + } + + void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { + // image + + if (tokens.hasMoreThan(IMAGE_SOURCE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + IMAGE_GRAMMAR); + } + + ImageView view = context.getView(); + if (view != null) { + if (tokens.size() == 2) { + String source = tokens.get(IMAGE_SOURCE_INDEX); + + try { + if (Url.isUrl(source)) { + context.getView().setContent(source); + context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + context.getView().setContent(ImageUtils.getImageAsDataUri(file)); + context.getView().setTitle(file.getName()); + } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } else { + throw new RuntimeException("Expected: " + IMAGE_GRAMMAR); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java new file mode 100644 index 000000000..1a7a996a5 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java @@ -0,0 +1,50 @@ +package com.structurizr.dsl; + +import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; +import com.structurizr.view.ImageView; + +import java.io.IOException; + +class ImageViewDslContext extends ViewDslContext { + + ImageViewDslContext(ImageView view) { + super(view); + } + + ImageView getView() { + return (ImageView)super.getView(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.VIEW_TITLE_TOKEN, + StructurizrDslTokens.VIEW_DESCRIPTION_TOKEN, + StructurizrDslTokens.PLANTUML_TOKEN, + StructurizrDslTokens.MERMAID_TOKEN, + StructurizrDslTokens.KROKI_TOKEN, + StructurizrDslTokens.IMAGE_TOKEN + }; + } + + @Override + void end() { + super.end(); + + // try to set the content type if it hasn't been set ... this helps the diagram render with image sizing/scaling + ImageView imageView = getView(); + if (StringUtils.isNullOrEmpty(imageView.getContentType())) { + if (ImageUtils.isSupportedDataUri(imageView.getContent())) { + imageView.setContentType(ImageUtils.getContentTypeFromDataUri(imageView.getContent())); + } else { + try { + imageView.setContentType(ImageUtils.getContentType(imageView.getContent())); + } catch (IOException e) { + e.printStackTrace(); + // ignore + } + } + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewParser.java new file mode 100644 index 000000000..548e8234c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewParser.java @@ -0,0 +1,56 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.view.ImageView; + +class ImageViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "image <*|element identifier> [key] {"; + + private static final String VIEW_TYPE = "Image"; + + private static final int SCOPE_IDENTIFIER_INDEX = 1; + private static final int KEY_INDEX = 2; + + private static final String WILDCARD = "*"; + + ImageView parse(DslContext context, Tokens tokens) { + // image <*|element identifier> [key] { + + Workspace workspace = context.getWorkspace(); + String key = ""; + + if (tokens.hasMoreThan(KEY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(SCOPE_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + ImageView view; + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + String scopeIdentifier = tokens.get(SCOPE_IDENTIFIER_INDEX); + if (WILDCARD.equals(scopeIdentifier)) { + + view = workspace.getViews().createImageView(key); + } else { + Element element = context.getElement(scopeIdentifier); + if (element == null) { + throw new RuntimeException("The element \"" + scopeIdentifier + "\" does not exist"); + } + + view = workspace.getViews().createImageView(element, key); + } + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java new file mode 100644 index 000000000..d076dffa2 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -0,0 +1,52 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +final class ImplicitRelationshipParser extends AbstractRelationshipParser { + + private static final String GRAMMAR = "-> [description] [technology] [tags]"; + + private static final int DESTINATION_IDENTIFIER_INDEX = 1; + private final static int DESCRIPTION_INDEX = 2; + private final static int TECHNOLOGY_INDEX = 3; + private final static int TAGS_INDEX = 4; + + Relationship parse(ModelItemDslContext context, Tokens tokens) { + // -> [description] [technology] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + + Element sourceElement = (Element)context.getModelItem(); + Element destinationElement = context.getElement(destinationId); + + if (destinationElement == null) { + throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + + String[] tags = new String[0]; + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX).split(","); + } + + return createRelationship(sourceElement, description, technology, tags, destinationElement); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java new file mode 100644 index 000000000..f66785ffe --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java @@ -0,0 +1,34 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy; +import com.structurizr.model.DefaultImpliedRelationshipsStrategy; + +import java.util.ArrayList; +import java.util.List; + +final class ImpliedRelationshipsParser extends AbstractParser { + + private static final String GRAMMAR = "!impliedRelationships "; + + private static final int FLAG_INDEX = 1; + private static final String FALSE = "false"; + + void parse(DslContext context, Tokens tokens) { + // impliedRelationships + + if (tokens.hasMoreThan(FLAG_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(FLAG_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + if (tokens.get(FLAG_INDEX).equalsIgnoreCase(FALSE)) { + context.getWorkspace().getModel().setImpliedRelationshipsStrategy(new DefaultImpliedRelationshipsStrategy()); + } else { + context.getWorkspace().getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java new file mode 100644 index 000000000..e3fcab817 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java @@ -0,0 +1,73 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +final class IncludeParser extends AbstractParser { + + private static final String GRAMMAR = "!include "; + + private static final int SOURCE_INDEX = 1; + + void parse(IncludedDslContext context, Tokens tokens) { + // !include + + if (tokens.hasMoreThan(SOURCE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(SOURCE_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String source = tokens.get(SOURCE_INDEX); + if (source.startsWith("https://")) { + String dsl = readFromUrl(source); + List lines = Arrays.asList(dsl.split("\n")); + context.addFile(context.getParentFile(), lines); + } else { + if (context.getParentFile() != null) { + File path = new File(context.getParentFile().getParent(), source); + + try { + if (!path.exists()) { + throw new RuntimeException(path.getCanonicalPath() + " could not be found"); + } + + readFiles(context, path); + } catch (IOException e) { + throw new RuntimeException("Error including " + path.getAbsolutePath() + ": " + e.getMessage()); + } + } + } + } + + private void readFiles(IncludedDslContext context, File path) throws IOException { + if (path.isHidden() || path.getName().startsWith(".")) { + // ignore + return; + } + + if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null) { + Arrays.sort(files); + + for (File file : files) { + readFiles(context, file); + } + } + } else { + try { + context.addFile(path, Files.readAllLines(path.toPath(), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Error reading file at " + path.getAbsolutePath() + ": " + e.getMessage()); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java new file mode 100644 index 000000000..42c86d00a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java @@ -0,0 +1,33 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +final class IncludedDslContext extends DslContext { + + private final File parentFile; + private final List files = new ArrayList<>(); + + IncludedDslContext(File parentFile) { + this.parentFile = parentFile; + } + + File getParentFile() { + return parentFile; + } + + void addFile(File file, List lines) { + this.files.add(new IncludedFile(file, lines)); + } + + List getFiles() { + return new ArrayList<>(files); + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedFile.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedFile.java new file mode 100644 index 000000000..f2b5a63ea --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedFile.java @@ -0,0 +1,24 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.util.List; + +final class IncludedFile { + + private final File file; + private final List lines; + + IncludedFile(File file, List lines) { + this.file = file; + this.lines = lines; + } + + List getLines() { + return lines; + } + + File getFile() { + return file; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java new file mode 100644 index 000000000..97487a2a5 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java @@ -0,0 +1,36 @@ +package com.structurizr.dsl; + +import com.structurizr.model.InfrastructureNode; +import com.structurizr.model.ModelItem; + +final class InfrastructureNodeDslContext extends ModelItemDslContext { + + private InfrastructureNode infrastructureNode; + + InfrastructureNodeDslContext(InfrastructureNode infrastructureNode) { + this.infrastructureNode = infrastructureNode; + } + + InfrastructureNode getInfrastructureNode() { + return infrastructureNode; + } + + @Override + ModelItem getModelItem() { + return getInfrastructureNode(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java new file mode 100644 index 000000000..9f20ba9a5 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java @@ -0,0 +1,71 @@ +package com.structurizr.dsl; + +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.InfrastructureNode; + +final class InfrastructureNodeParser extends AbstractParser { + + private static final String GRAMMAR = "infrastructureNode [description] [technology] [tags]"; + + private static final int NAME_INDEX = 1; + private static final int DESCRIPTION_INDEX = 2; + private static final int TECHNOLOGY_INDEX = 3; + private static final int TAGS_INDEX = 4; + + InfrastructureNode parse(DeploymentNodeDslContext context, Tokens tokens) { + // infrastructureNode [description] [technology] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + DeploymentNode deploymentNode = context.getDeploymentNode(); + InfrastructureNode infrastructureNode; + String name = tokens.get(NAME_INDEX); + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + + infrastructureNode = deploymentNode.addInfrastructureNode(name, description, technology); + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + infrastructureNode.addTags(tags.split(",")); + } + + if (context.hasGroup()) { + infrastructureNode.setGroup(context.getGroup().getName()); + context.getGroup().addElement(infrastructureNode); + } + + return infrastructureNode; + } + + void parseTechnology(InfrastructureNodeDslContext context, Tokens tokens) { + int index = 1; + + // technology + if (tokens.hasMoreThan(index)) { + throw new RuntimeException("Too many tokens, expected: technology "); + } + + if (!tokens.includes(index)) { + throw new RuntimeException("Expected: technology "); + } + + String technology = tokens.get(index); + context.getInfrastructureNode().setTechnology(technology); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InlineScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InlineScriptDslContext.java new file mode 100644 index 000000000..72803e629 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InlineScriptDslContext.java @@ -0,0 +1,55 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class InlineScriptDslContext extends ScriptDslContext { + + static final Map SUPPORTED_LANGUAGES = new HashMap<>(); + + private final String language; + private final List lines = new ArrayList<>(); + + static { + SUPPORTED_LANGUAGES.put("javascript", "js"); + SUPPORTED_LANGUAGES.put("groovy", "groovy"); + SUPPORTED_LANGUAGES.put("kotlin", "kts"); + SUPPORTED_LANGUAGES.put("ruby", "rb"); + } + + InlineScriptDslContext(DslContext parentContext, File dslFile, String language) { + super(parentContext, dslFile); + + this.language = language.toLowerCase(); + } + + void addLine(String line) { + lines.add(line); + } + + @Override + void end() { + try { + String fileExtension; + + if (SUPPORTED_LANGUAGES.containsKey(language)) { + fileExtension = SUPPORTED_LANGUAGES.get(language); + } else { + throw new RuntimeException("Unsupported scripting language \"" + language + "\""); + } + + run(this, fileExtension, lines); + } catch (Exception e) { + throw new RuntimeException("Error running inline script, caused by " + e.getClass().getName() + ": " + e.getMessage(), e); + } + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java new file mode 100644 index 000000000..e803d4fea --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java @@ -0,0 +1,37 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Location; + +final class ModelDslContext extends GroupableDslContext { + + ModelDslContext() { + super(null); + } + + ModelDslContext(ElementGroup group) { + super(group); + } + + @Override + void end() { + // the location is only set to internal when created inside an "enterprise" block, let's assume the rest are external if an enterprise has been specified + if (getWorkspace().getModel().getEnterprise() != null) { + getWorkspace().getModel().getPeople().stream().filter(p -> p.getLocation() != Location.Internal).forEach(p -> p.setLocation(Location.External)); + getWorkspace().getModel().getSoftwareSystems().stream().filter(ss -> ss.getLocation() != Location.Internal).forEach(ss -> ss.setLocation(Location.External)); + } + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ENTERPRISE_TOKEN, + StructurizrDslTokens.GROUP_TOKEN, + StructurizrDslTokens.PERSON_TOKEN, + StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, + StructurizrDslTokens.DEPLOYMENT_ENVIRONMENT_TOKEN, + StructurizrDslTokens.CUSTOM_ELEMENT_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java new file mode 100644 index 000000000..72150de5a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java @@ -0,0 +1,17 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +abstract class ModelItemDslContext extends GroupableDslContext { + + ModelItemDslContext() { + super(); + } + + ModelItemDslContext(ElementGroup group) { + super(group); + } + + abstract ModelItem getModelItem(); + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java new file mode 100644 index 000000000..823728bf0 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java @@ -0,0 +1,79 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; + +final class ModelItemParser extends AbstractParser { + + private final static int DESCRIPTION_INDEX = 1; + + private final static int TAGS_INDEX = 1; + + private final static int URL_INDEX = 1; + + private final static int PERSPECTIVE_NAME_INDEX = 0; + private final static int PERSPECTIVE_DESCRIPTION_INDEX = 1; + private final static int PERSPECTIVE_VALUE_INDEX = 2; + + void parseTags(ModelItemDslContext context, Tokens tokens) { + // tags [tags] + if (!tokens.includes(TAGS_INDEX)) { + throw new RuntimeException("Expected: tags [tags]"); + } + + for (int i = TAGS_INDEX; i < tokens.size(); i++) { + String tags = tokens.get(i); + context.getModelItem().addTags(tags.split(",")); + } + } + + void parseDescription(ModelItemDslContext context, Tokens tokens) { + // description + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: description "); + } + + if (!tokens.includes(DESCRIPTION_INDEX)) { + throw new RuntimeException("Expected: description "); + } + + String description = tokens.get(DESCRIPTION_INDEX); + ((Element)context.getModelItem()).setDescription(description); + } + + void parseUrl(ModelItemDslContext context, Tokens tokens) { + // url + if (tokens.hasMoreThan(URL_INDEX)) { + throw new RuntimeException("Too many tokens, expected: url "); + } + + if (!tokens.includes(URL_INDEX)) { + throw new RuntimeException("Expected: url "); + } + + String url = tokens.get(URL_INDEX); + context.getModelItem().setUrl(url); + } + + void parsePerspective(ModelItemPerspectivesDslContext context, Tokens tokens) { + // [value] + + if (tokens.hasMoreThan(PERSPECTIVE_VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: [value]"); + } + + if (!tokens.includes(PERSPECTIVE_DESCRIPTION_INDEX)) { + throw new RuntimeException("Expected: [value]"); + } + + String name = tokens.get(PERSPECTIVE_NAME_INDEX); + String description = tokens.get(PERSPECTIVE_DESCRIPTION_INDEX); + String value = ""; + + if (tokens.includes(PERSPECTIVE_VALUE_INDEX)) { + value = tokens.get(PERSPECTIVE_VALUE_INDEX); + } + + context.getModelItem().addPerspective(name, description, value); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java new file mode 100644 index 000000000..37647a492 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java @@ -0,0 +1,22 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +final class ModelItemPerspectivesDslContext extends DslContext { + + private ModelItem modelItem; + + public ModelItemPerspectivesDslContext(ModelItem modelItem) { + this.modelItem = modelItem; + } + + ModelItem getModelItem() { + return this.modelItem; + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java new file mode 100644 index 000000000..44212f52b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java @@ -0,0 +1,19 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Relationship; +import com.structurizr.view.ModelView; + +abstract class ModelViewContentParser extends AbstractParser { + + protected static final String WILDCARD = "*"; + protected static final String ELEMENT_WILDCARD = "element==*"; + + protected boolean isExpression(String token) { + return AbstractExpressionParser.isExpression(token.toLowerCase()); + } + + protected void removeRelationshipFromView(Relationship relationship, ModelView view) { + view.remove(relationship); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewDslContext.java new file mode 100644 index 000000000..a7e966451 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewDslContext.java @@ -0,0 +1,15 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ModelView; + +abstract class ModelViewDslContext extends ViewDslContext { + + ModelViewDslContext(ModelView view) { + super(view); + } + + ModelView getView() { + return (ModelView)super.getView(); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonDslContext.java new file mode 100644 index 000000000..46d01acbb --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonDslContext.java @@ -0,0 +1,41 @@ +package com.structurizr.dsl; + +import com.structurizr.model.GroupableElement; +import com.structurizr.model.ModelItem; +import com.structurizr.model.Person; + +final class PersonDslContext extends GroupableElementDslContext { + + private Person person; + + PersonDslContext(Person person) { + this.person = person; + } + + Person getPerson() { + return person; + } + + @Override + ModelItem getModelItem() { + return getPerson(); + } + + @Override + GroupableElement getElement() { + return person; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java new file mode 100644 index 000000000..9ec33bfa6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -0,0 +1,59 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Location; +import com.structurizr.model.Person; + +final class PersonParser extends AbstractParser { + + private static final String GRAMMAR = "person [description] [tags]"; + + private final static int NAME_INDEX = 1; + private final static int DESCRIPTION_INDEX = 2; + private final static int TAGS_INDEX = 3; + + Person parse(GroupableDslContext context, Tokens tokens) { + // person [description] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + Person person = null; + String name = tokens.get(NAME_INDEX); + + if (context.isExtendingWorkspace()) { + person = context.getWorkspace().getModel().getPersonWithName(name); + } + + if (person == null) { + person = context.getWorkspace().getModel().addPerson(name); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + person.setDescription(description); + } + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + person.addTags(tags.split(",")); + } + + if (context instanceof EnterpriseDslContext) { + person.setLocation(Location.Internal); + } + + if (context.hasGroup()) { + person.setGroup(context.getGroup().getName()); + context.getGroup().addElement(person); + } + + return person; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java new file mode 100644 index 000000000..a648c2e80 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java @@ -0,0 +1,42 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +class PluginDslContext extends DslContext { + + private final String fullyQualifiedClassName; + private final File dslFile; + private final StructurizrDslParser dslParser; + private final Map parameters = new HashMap<>(); + + PluginDslContext(String fullyQualifiedClassName, File dslFile, StructurizrDslParser dslParser) { + this.fullyQualifiedClassName = fullyQualifiedClassName; + this.dslFile = dslFile; + this.dslParser = dslParser; + } + + void addParameter(String name, String value) { + parameters.put(name, value); + } + + @Override + void end() { + try { + Class pluginClass = loadClass(fullyQualifiedClassName, dslFile); + StructurizrDslPlugin plugin = (StructurizrDslPlugin)pluginClass.getDeclaredConstructor().newInstance(); + StructurizrDslPluginContext pluginContext = new StructurizrDslPluginContext(dslParser, dslFile, getWorkspace(), parameters); + plugin.run(pluginContext); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Error running plugin " + fullyQualifiedClassName + ", caused by " + e.getClass().getName() + ": " + e.getMessage()); + } + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginParser.java new file mode 100644 index 000000000..b6471a87c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginParser.java @@ -0,0 +1,43 @@ +package com.structurizr.dsl; + +final class PluginParser extends AbstractParser { + + private static final String GRAMMAR = "!plugin "; + + private static final int FQN_INDEX = 1; + + private final static int PARAMETER_NAME_INDEX = 0; + private final static int PARAMETER_VALUE_INDEX = 1; + + String parse(DslContext context, Tokens tokens) { + // !plugin + + if (tokens.hasMoreThan(FQN_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(FQN_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + return tokens.get(FQN_INDEX); + } + + void parseParameter(PluginDslContext context, Tokens tokens) { + // + + if (tokens.hasMoreThan(PARAMETER_VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: "); + } + + if (tokens.size() != 2) { + throw new RuntimeException("Expected: "); + } + + String name = tokens.get(PARAMETER_NAME_INDEX); + String value = tokens.get(PARAMETER_VALUE_INDEX); + + context.addParameter(name, value); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java new file mode 100644 index 000000000..e1f8184fd --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java @@ -0,0 +1,22 @@ +package com.structurizr.dsl; + +import com.structurizr.PropertyHolder; + +final class PropertiesDslContext extends DslContext { + + private PropertyHolder propertyHolder; + + public PropertiesDslContext(PropertyHolder propertyHolder) { + this.propertyHolder = propertyHolder; + } + + PropertyHolder getPropertyHolder() { + return this.propertyHolder; + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java new file mode 100644 index 000000000..2129a2ddd --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java @@ -0,0 +1,25 @@ +package com.structurizr.dsl; + +final class PropertyParser extends AbstractParser { + + private final static int PROPERTY_NAME_INDEX = 0; + private final static int PROPERTY_VALUE_INDEX = 1; + + void parse(PropertiesDslContext context, Tokens tokens) { + // + + if (tokens.hasMoreThan(PROPERTY_VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: "); + } + + if (tokens.size() != 2) { + throw new RuntimeException("Expected: "); + } + + String name = tokens.get(PROPERTY_NAME_INDEX); + String value = tokens.get(PROPERTY_VALUE_INDEX); + + context.getPropertyHolder().addProperty(name, value); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java new file mode 100644 index 000000000..d9df9b1f8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java @@ -0,0 +1,52 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.StaticStructureElement; + +final class RefParser extends AbstractParser { + + private static final String GRAMMAR = "%s "; + + private final static int IDENTIFIER_INDEX = 1; + + ModelItem parse(DslContext context, Tokens tokens) { + // !ref + + if (tokens.hasMoreThan(IDENTIFIER_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + String.format(GRAMMAR, tokens.get(0))); + } + + if (!tokens.includes(IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + String.format(GRAMMAR, tokens.get(0))); + } + + String s = tokens.get(IDENTIFIER_INDEX); + + ModelItem modelItem; + + if (s.contains("://")) { + modelItem = context.getWorkspace().getModel().getElementWithCanonicalName(s); + } else { + modelItem = context.getElement(s); + + if (modelItem == null) { + modelItem = context.getRelationship(s); + } + } + + if (modelItem == null) { + throw new RuntimeException("An element/relationship identified by \"" + s + "\" could not be found"); + } + + if (context instanceof GroupableDslContext && modelItem instanceof StaticStructureElement) { + GroupableDslContext groupableDslContext = (GroupableDslContext)context; + StaticStructureElement staticStructureElement = (StaticStructureElement)modelItem; + if (groupableDslContext.hasGroup()) { + staticStructureElement.setGroup(groupableDslContext.getGroup().getName()); + } + } + + return modelItem; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipDslContext.java new file mode 100644 index 000000000..b39e3043f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipDslContext.java @@ -0,0 +1,33 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Relationship; + +final class RelationshipDslContext extends ModelItemDslContext { + + private Relationship relationship; + + RelationshipDslContext(Relationship relationship) { + this.relationship = relationship; + } + + Relationship getRelationship() { + return relationship; + } + + @Override + ModelItem getModelItem() { + return getRelationship(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleDslContext.java new file mode 100644 index 000000000..58239b18f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleDslContext.java @@ -0,0 +1,33 @@ +package com.structurizr.dsl; + +import com.structurizr.view.RelationshipStyle; + +final class RelationshipStyleDslContext extends DslContext { + + private RelationshipStyle style; + + RelationshipStyleDslContext(RelationshipStyle style) { + this.style = style; + } + + RelationshipStyle getStyle() { + return style; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.RELATIONSHIP_STYLE_THICKNESS_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_COLOR_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_COLOUR_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_LINE_STYLE_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_ROUTING_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_FONT_SIZE_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_WIDTH_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_POSITION_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_OPACITY_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java new file mode 100644 index 000000000..6c260c3bf --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java @@ -0,0 +1,239 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.StringUtils; +import com.structurizr.view.LineStyle; +import com.structurizr.view.RelationshipStyle; +import com.structurizr.view.Routing; + +import java.util.HashMap; +import java.util.Map; + +final class RelationshipStyleParser extends AbstractParser { + + private static final int FIRST_PROPERTY_INDEX = 1; + + RelationshipStyle parseRelationshipStyle(DslContext context, Tokens tokens) { + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: relationship {"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String tag = tokens.get(1); + + if (StringUtils.isNullOrEmpty(tag)) { + throw new RuntimeException("A tag must be specified"); + } + + Workspace workspace = context.getWorkspace(); + RelationshipStyle relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle(tag); + if (relationshipStyle == null) { + relationshipStyle = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(tag); + } + + return relationshipStyle; + } else { + throw new RuntimeException("Expected: relationship {"); + } + } + + void parseThickness(RelationshipStyleDslContext context, Tokens tokens) { + // thickness + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: thickness "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String thicknessAsString = tokens.get(1); + + try { + int thickness = Integer.parseInt(thicknessAsString); + style.setThickness(thickness); + } catch (NumberFormatException e) { + throw new RuntimeException("Thickness must be a positive integer"); + } + } else { + throw new RuntimeException("Expected: thickness "); + } + } + + void parseColour(RelationshipStyleDslContext context, Tokens tokens) { + // colour #rrggbb|color name + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: colour <#rrggbb|color name>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String colour = tokens.get(1); + style.setColor(colour); + } else { + throw new RuntimeException("Expected: colour <#rrggbb|color name>"); + } + } + + void parseDashed(RelationshipStyleDslContext context, Tokens tokens) { + // dashed true|false + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: dashed "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String dashed = tokens.get(1); + + if ("true".equalsIgnoreCase(dashed)) { + style.setDashed(true); + } else if ("false".equalsIgnoreCase(dashed)) { + style.setDashed(false); + } else { + throw new RuntimeException("Dashed must be true or false"); + } + } else { + throw new RuntimeException("Expected: dashed "); + } + } + + void parseOpacity(RelationshipStyleDslContext context, Tokens tokens) { + // opacity 0-100 + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: opacity <0-100>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String opacityAsString = tokens.get(1); + + try { + int opacity = Integer.parseInt(opacityAsString); + style.setOpacity(opacity); + } catch (NumberFormatException e) { + throw new RuntimeException("Opacity must be an integer between 0 and 100"); + } + } else { + throw new RuntimeException("Expected: opacity <0-100>"); + } + } + + void parseWidth(RelationshipStyleDslContext context, Tokens tokens) { + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: width "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String widthAsString = tokens.get(1); + + try { + int width = Integer.parseInt(widthAsString); + style.setWidth(width); + } catch (NumberFormatException e) { + throw new RuntimeException("Width must be a positive integer"); + } + } else { + throw new RuntimeException("Expected: width "); + } + } + + void parseFontSize(RelationshipStyleDslContext context, Tokens tokens) { + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: fontSize "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String fontSizeAsString = tokens.get(1); + + try { + int fontSize = Integer.parseInt(fontSizeAsString); + style.setFontSize(fontSize); + } catch (NumberFormatException e) { + throw new RuntimeException("Font size must be a positive integer"); + } + } else { + throw new RuntimeException("Expected: fontSize "); + } + } + + void parsePosition(RelationshipStyleDslContext context, Tokens tokens) { + // position 0-100 + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: position <0-100>"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String positionAsString = tokens.get(1); + + try { + int opacity = Integer.parseInt(positionAsString); + style.setPosition(opacity); + } catch (NumberFormatException e) { + throw new RuntimeException("Position must be an integer between 0 and 100"); + } + } else { + throw new RuntimeException("Expected: position <0-100>"); + } + } + + void parseLineStyle(RelationshipStyleDslContext context, Tokens tokens) { + // style solid|dashed|dotted + Map lineStyles = new HashMap<>(); + for (LineStyle lineStyle : LineStyle.values()) { + lineStyles.put(lineStyle.toString().toLowerCase(), lineStyle); + } + + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: style "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String lineStyle = tokens.get(1).toLowerCase(); + + if (lineStyles.containsKey(lineStyle)) { + style.setStyle(lineStyles.get(lineStyle)); + } else { + throw new RuntimeException("The line style \"" + lineStyle + "\" is not valid"); + } + } else { + throw new RuntimeException("Expected: style "); + } + } + + void parseRouting(RelationshipStyleDslContext context, Tokens tokens) { + // routing direct|orthogonal|curved + Map routings = new HashMap<>(); + for (Routing routing : Routing.values()) { + routings.put(routing.toString().toLowerCase(), routing); + } + + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: routing "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String routing = tokens.get(1).toLowerCase(); + + if (routings.containsKey(routing)) { + style.setRouting(routings.get(routing)); + } else { + throw new RuntimeException("The routing \"" + routing + "\" is not valid"); + } + } else { + throw new RuntimeException("Expected: routing "); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java new file mode 100644 index 000000000..d066bbc44 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java @@ -0,0 +1,77 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +abstract class ScriptDslContext extends DslContext { + + private static final String CONTEXT_VARIABLE_NAME = "context"; + private static final String WORKSPACE_VARIABLE_NAME = "workspace"; + private static final String VIEW_VARIABLE_NAME = "view"; + private static final String ELEMENT_VARIABLE_NAME = "element"; + private static final String RELATIONSHIP_VARIABLE_NAME = "relationship"; + + private final DslContext parentContext; + + protected final File dslFile; + + private final Map parameters = new HashMap<>(); + + ScriptDslContext(DslContext parentContext, File dslFile) { + this.parentContext = parentContext; + this.dslFile = dslFile; + } + + void addParameter(String name, String value) { + parameters.put(name, value); + } + + void run(DslContext context, String extension, List lines) throws Exception { + StringBuilder script = new StringBuilder(); + for (String line : lines) { + script.append(line); + script.append('\n'); + } + + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByExtension(extension); + + if (engine != null) { + Bindings bindings = engine.createBindings(); + bindings.put(WORKSPACE_VARIABLE_NAME, context.getWorkspace()); + + if (parentContext instanceof ViewDslContext) { + bindings.put(VIEW_VARIABLE_NAME, ((ViewDslContext)parentContext).getView()); + } else if (parentContext instanceof ModelItemDslContext) { + ModelItemDslContext modelItemDslContext = (ModelItemDslContext)parentContext; + if (modelItemDslContext.getModelItem() instanceof Element) { + bindings.put(ELEMENT_VARIABLE_NAME, modelItemDslContext.getModelItem()); + } else if (modelItemDslContext.getModelItem() instanceof Relationship) { + bindings.put(RELATIONSHIP_VARIABLE_NAME, modelItemDslContext.getModelItem()); + } + } + + // bind a context object + StructurizrDslScriptContext scriptContext = new StructurizrDslScriptContext(dslFile, getWorkspace(), parameters); + bindings.put(CONTEXT_VARIABLE_NAME, scriptContext); + + // and any custom parameters + for (String name : parameters.keySet()) { + bindings.put(name, parameters.get(name)); + } + + engine.eval(script.toString(), bindings); + } else { + throw new RuntimeException("Could not load a scripting engine for extension \"" + extension + "\""); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptParser.java new file mode 100644 index 000000000..6e11a0764 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptParser.java @@ -0,0 +1,66 @@ +package com.structurizr.dsl; + +final class ScriptParser extends AbstractParser { + + private static final String EXTERNAL_GRAMMAR = "!script "; + private static final String INLINE_GRAMMAR = "!script "; + + private static final int FILENAME_INDEX = 1; + private static final int LANGUAGE_INDEX = 1; + + private final static int PARAMETER_NAME_INDEX = 0; + private final static int PARAMETER_VALUE_INDEX = 1; + + boolean isInlineScript(Tokens tokens) { + return + DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(tokens.get(tokens.size()-1)) && + tokens.includes(LANGUAGE_INDEX) && + InlineScriptDslContext.SUPPORTED_LANGUAGES.containsKey(tokens.get(LANGUAGE_INDEX).toLowerCase()); + } + + String parseExternal(Tokens tokens) { + // !script + + if (tokens.hasMoreThan(FILENAME_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + EXTERNAL_GRAMMAR); + } + + if (!tokens.includes(FILENAME_INDEX)) { + throw new RuntimeException("Expected: " + EXTERNAL_GRAMMAR); + } + + return tokens.get(FILENAME_INDEX); + } + + void parseParameter(ExternalScriptDslContext context, Tokens tokens) { + // + + if (tokens.hasMoreThan(PARAMETER_VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: "); + } + + if (tokens.size() != 2) { + throw new RuntimeException("Expected: "); + } + + String name = tokens.get(PARAMETER_NAME_INDEX); + String value = tokens.get(PARAMETER_VALUE_INDEX); + + context.addParameter(name, value); + } + + String parseInline(Tokens tokens) { + // !script + + if (tokens.hasMoreThan(LANGUAGE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + INLINE_GRAMMAR); + } + + if (!tokens.includes(LANGUAGE_INDEX)) { + throw new RuntimeException("Expected: " + INLINE_GRAMMAR); + } + + return tokens.get(LANGUAGE_INDEX); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java new file mode 100644 index 000000000..e85280d0c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java @@ -0,0 +1,51 @@ +package com.structurizr.dsl; + +import com.structurizr.model.GroupableElement; +import com.structurizr.model.ModelItem; +import com.structurizr.model.SoftwareSystem; + +final class SoftwareSystemDslContext extends GroupableElementDslContext { + + private SoftwareSystem softwareSystem; + + SoftwareSystemDslContext(SoftwareSystem softwareSystem) { + this(softwareSystem, null); + } + + SoftwareSystemDslContext(SoftwareSystem softwareSystem, ElementGroup group) { + super(group); + + this.softwareSystem = softwareSystem; + } + + SoftwareSystem getSoftwareSystem() { + return softwareSystem; + } + + @Override + ModelItem getModelItem() { + return getSoftwareSystem(); + } + + @Override + GroupableElement getElement() { + return softwareSystem; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DOCS_TOKEN, + StructurizrDslTokens.ADRS_TOKEN, + StructurizrDslTokens.GROUP_TOKEN, + StructurizrDslTokens.CONTAINER_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java new file mode 100644 index 000000000..9da1e6760 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java @@ -0,0 +1,42 @@ +package com.structurizr.dsl; + +import com.structurizr.model.SoftwareSystemInstance; +import com.structurizr.model.ModelItem; +import com.structurizr.model.StaticStructureElementInstance; + +final class SoftwareSystemInstanceDslContext extends StaticStructureElementInstanceDslContext { + + private SoftwareSystemInstance softwareSystemInstance; + + SoftwareSystemInstanceDslContext(SoftwareSystemInstance softwareSystemInstance) { + this.softwareSystemInstance = softwareSystemInstance; + } + + SoftwareSystemInstance getSoftwareSystemInstance() { + return softwareSystemInstance; + } + + @Override + ModelItem getModelItem() { + return getSoftwareSystemInstance(); + } + + @Override + StaticStructureElementInstance getElementInstance() { + return getSoftwareSystemInstance(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.HEALTH_CHECK_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java new file mode 100644 index 000000000..dee362c64 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java @@ -0,0 +1,65 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +import java.util.HashSet; +import java.util.Set; + +final class SoftwareSystemInstanceParser extends AbstractParser { + + private static final String GRAMMAR = "softwareSystemInstance [deploymentGroups] [tags]"; + + private static final int IDENTIFIER_INDEX = 1; + private static final int DEPLOYMENT_GROUPS_TOKEN = 2; + private static final int TAGS_INDEX = 3; + + SoftwareSystemInstance parse(DeploymentNodeDslContext context, Tokens tokens) { + // softwareSystemInstance [tags] + // softwareSystemInstance [deploymentGroup] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String softwareSystemIdentifier = tokens.get(IDENTIFIER_INDEX); + + Element element = context.getElement(softwareSystemIdentifier, SoftwareSystem.class); + if (element == null) { + throw new RuntimeException("The software system \"" + softwareSystemIdentifier + "\" does not exist"); + } + + DeploymentNode deploymentNode = context.getDeploymentNode(); + + Set deploymentGroups = new HashSet<>(); + if (tokens.includes(DEPLOYMENT_GROUPS_TOKEN)) { + String token = tokens.get(DEPLOYMENT_GROUPS_TOKEN); + + String[] deploymentGroupReferences = token.split(","); + for (String deploymentGroupReference : deploymentGroupReferences) { + Element e = context.getElement(deploymentGroupReference); + if (e instanceof DeploymentGroup) { + deploymentGroups.add(e.getName()); + } + } + } + + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add((SoftwareSystem)element, deploymentGroups.toArray(new String[]{})); + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + softwareSystemInstance.addTags(tags.split(",")); + } + + if (context.hasGroup()) { + softwareSystemInstance.setGroup(context.getGroup().getName()); + context.getGroup().addElement(softwareSystemInstance); + } + + return softwareSystemInstance; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java new file mode 100644 index 000000000..9e3214bbd --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -0,0 +1,59 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Location; +import com.structurizr.model.SoftwareSystem; + +final class SoftwareSystemParser extends AbstractParser { + + private static final String GRAMMAR = "softwareSystem [description] [tags]"; + + private final static int NAME_INDEX = 1; + private final static int DESCRIPTION_INDEX = 2; + private final static int TAGS_INDEX = 3; + + SoftwareSystem parse(GroupableDslContext context, Tokens tokens) { + // softwareSystem [description] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + SoftwareSystem softwareSystem = null; + String name = tokens.get(NAME_INDEX); + + if (context.isExtendingWorkspace()) { + softwareSystem = context.getWorkspace().getModel().getSoftwareSystemWithName(name); + } + + if (softwareSystem == null) { + softwareSystem = context.getWorkspace().getModel().addSoftwareSystem(name); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + softwareSystem.setDescription(description); + } + + if (tokens.includes(TAGS_INDEX)) { + String tags = tokens.get(TAGS_INDEX); + softwareSystem.addTags(tags.split(",")); + } + + if (context instanceof EnterpriseDslContext) { + softwareSystem.setLocation(Location.Internal); + } + + if (context.hasGroup()) { + softwareSystem.setGroup(context.getGroup().getName()); + context.getGroup().addElement(softwareSystem); + } + + return softwareSystem; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java new file mode 100644 index 000000000..1a01b973d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java @@ -0,0 +1,9 @@ +package com.structurizr.dsl; + +import com.structurizr.model.StaticStructureElementInstance; + +abstract class StaticStructureElementInstanceDslContext extends ModelItemDslContext { + + abstract StaticStructureElementInstance getElementInstance(); + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationDslContext.java new file mode 100644 index 000000000..641575692 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationDslContext.java @@ -0,0 +1,26 @@ +package com.structurizr.dsl; + +import com.structurizr.view.StaticView; + +class StaticViewAnimationDslContext extends DslContext { + + private StaticView view; + + StaticViewAnimationDslContext(StaticView view) { + super(); + + this.view = view; + } + + StaticView getView() { + return view; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ANIMATION_STEP_IN_VIEW_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java new file mode 100644 index 000000000..04f76a118 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java @@ -0,0 +1,50 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.view.StaticView; + +import java.util.ArrayList; +import java.util.List; + +final class StaticViewAnimationStepParser extends AbstractParser { + + void parse(StaticViewDslContext context, Tokens tokens) { + // animationStep [identifier...] + + if (!tokens.includes(1)) { + throw new RuntimeException("Expected: animationStep [identifier...]"); + } + + parse(context, context.getView(), tokens, 1); + } + + void parse(StaticViewAnimationDslContext context, Tokens tokens) { + // [identifier...] + + if (!tokens.includes(0)) { + throw new RuntimeException("Expected: [identifier...]"); + } + + parse(context, context.getView(), tokens, 0); + } + + void parse(DslContext context, StaticView view, Tokens tokens, int startIndex) { + // [identifier...] + + List elements = new ArrayList<>(); + + for (int i = startIndex; i < tokens.size(); i++) { + String elementIdentifier = tokens.get(i); + + Element element = context.getElement(elementIdentifier); + if (element == null) { + throw new RuntimeException("The element \"" + elementIdentifier + "\" does not exist"); + } + + elements.add(element); + } + + view.addAnimation(elements.toArray(new Element[0])); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java new file mode 100644 index 000000000..85d7dec9d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java @@ -0,0 +1,117 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.ComponentView; +import com.structurizr.view.ContainerView; +import com.structurizr.view.ElementNotPermittedInViewException; +import com.structurizr.view.StaticView; + +final class StaticViewContentParser extends ModelViewContentParser { + + private static final int FIRST_IDENTIFIER_INDEX = 1; + + void parseInclude(StaticViewDslContext context, Tokens tokens) { + if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: include <*|identifier|expression> [*|identifier|expression...]"); + } + + StaticView view = context.getView(); + + // include [identifier|expression...] + for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { + String token = tokens.get(i); + + if (token.equals(WILDCARD) || token.equals(ELEMENT_WILDCARD)) { + // include * or include element==* + view.addDefaultElements(); + } else if (isExpression(token)) { + new StaticViewExpressionParser().parseExpression(token, context).forEach(mi -> addModelItemToView(mi, view, null)); + } else { + new StaticViewExpressionParser().parseIdentifier(token, context).forEach(mi -> addModelItemToView(mi, view, token)); + } + } + } + + void parseExclude(StaticViewDslContext context, Tokens tokens) { + if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: exclude [identifier|expression...]"); + } + + StaticView view = context.getView(); + + // exclude [identifier|expression...] + for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { + String token = tokens.get(i); + + if (isExpression(token)) { + new StaticViewExpressionParser().parseExpression(token, context).forEach(mi -> removeModelItemFromView(mi, view)); + } else { + new StaticViewExpressionParser().parseIdentifier(token, context).forEach(mi -> removeModelItemFromView(mi, view)); + } + } + } + + private void addModelItemToView(ModelItem modelItem, StaticView view, String identifier) { + if (modelItem instanceof Element) { + addElementToView((Element)modelItem, view, identifier); + } else { + addRelationshipToView((Relationship)modelItem, view); + } + } + + private void addElementToView(Element element, StaticView view, String identifier) { + try { + if (element instanceof CustomElement) { + view.add((CustomElement) element); + } else if (element instanceof Person) { + view.add((Person) element); + } else if (element instanceof SoftwareSystem) { + view.add((SoftwareSystem) element); + } else if (element instanceof Container && (view instanceof ContainerView)) { + ((ContainerView) view).add((Container) element); + } else if (element instanceof Container && (view instanceof ComponentView)) { + ((ComponentView) view).add((Container) element); + } else if (element instanceof Component && (view instanceof ComponentView)) { + ((ComponentView) view).add((Component) element); + } else { + if (!StringUtils.isNullOrEmpty(identifier)) { + throw new RuntimeException("The element \"" + identifier + "\" can not be added to this type of view"); + } + } + } catch (ElementNotPermittedInViewException e) { + // ignore + } + } + + private void removeModelItemFromView(ModelItem modelItem, StaticView view) { + if (modelItem instanceof Element) { + removeElementFromView((Element)modelItem, view); + } else { + removeRelationshipFromView((Relationship)modelItem, view); + } + } + + private void removeElementFromView(Element element, StaticView view) { + if (element instanceof CustomElement) { + view.remove((CustomElement) element); + } else if (element instanceof Person) { + view.remove((Person) element); + } else if (element instanceof SoftwareSystem) { + view.remove((SoftwareSystem) element); + } else if (element instanceof Container && (view instanceof ContainerView)) { + ((ContainerView) view).remove((Container) element); + } else if (element instanceof Container && (view instanceof ComponentView)) { + ((ComponentView) view).remove((Container) element); + } else if (element instanceof Component && (view instanceof ComponentView)) { + ((ComponentView) view).remove((Component) element); + } + } + + private void addRelationshipToView(Relationship relationship, StaticView view) { + if (view.isElementInView(relationship.getSource()) && view.isElementInView(relationship.getDestination())) { + view.add(relationship); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewDslContext.java new file mode 100644 index 000000000..01c0c5307 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewDslContext.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.view.StaticView; + +class StaticViewDslContext extends ModelViewDslContext { + + StaticViewDslContext(StaticView view) { + super(view); + } + + StaticView getView() { + return (StaticView)super.getView(); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.INCLUDE_IN_VIEW_TOKEN, + StructurizrDslTokens.EXCLUDE_IN_VIEW_TOKEN, + StructurizrDslTokens.AUTOLAYOUT_VIEW_TOKEN, + StructurizrDslTokens.DEFAULT_VIEW_TOKEN, + StructurizrDslTokens.ANIMATION_IN_VIEW_TOKEN, + StructurizrDslTokens.VIEW_TITLE_TOKEN, + StructurizrDslTokens.VIEW_DESCRIPTION_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java new file mode 100644 index 000000000..756dd3799 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java @@ -0,0 +1,64 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; + +final class StaticViewExpressionParser extends AbstractExpressionParser { + + @Override + protected Set evaluateElementTypeExpression(String expr, DslContext context) { + Set elements = new LinkedHashSet<>(); + + String type = expr.substring(ELEMENT_TYPE_EQUALS_EXPRESSION.length()); + switch (type.toLowerCase()) { + case "custom": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof CustomElement).forEach(elements::add); + break; + case "person": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Person).forEach(elements::add); + break; + case "softwaresystem": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof SoftwareSystem).forEach(elements::add); + break; + case "container": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Container).forEach(elements::add); + break; + case "component": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Component).forEach(elements::add); + break; + default: + throw new RuntimeException("The element type of \"" + type + "\" is not valid for this view"); + } + + return elements; + } + + protected Set findAfferentCouplings(Element element) { + Set elements = new LinkedHashSet<>(); + + elements.addAll(findAfferentCouplings(element, CustomElement.class)); + elements.addAll(findAfferentCouplings(element, Person.class)); + elements.addAll(findAfferentCouplings(element, SoftwareSystem.class)); + elements.addAll(findAfferentCouplings(element, Container.class)); + elements.addAll(findAfferentCouplings(element, Component.class)); + + return elements; + } + + protected Set findEfferentCouplings(Element element) { + Set elements = new LinkedHashSet<>(); + + elements.addAll(findEfferentCouplings(element, CustomElement.class)); + elements.addAll(findEfferentCouplings(element, Person.class)); + elements.addAll(findEfferentCouplings(element, SoftwareSystem.class)); + elements.addAll(findEfferentCouplings(element, Container.class)); + elements.addAll(findEfferentCouplings(element, Component.class)); + + return elements; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java new file mode 100644 index 000000000..8b205bf28 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java @@ -0,0 +1,22 @@ +package com.structurizr.dsl; + +class StructurizrDslExpressions { + + static final String ELEMENT_TYPE_EQUALS_EXPRESSION = "element.type=="; + static final String ELEMENT_TAG_EQUALS_EXPRESSION = "element.tag=="; + static final String ELEMENT_TAG_NOT_EQUALS_EXPRESSION = "element.tag!="; + + static final String ELEMENT_EQUALS_EXPRESSION = "element=="; + static final String ELEMENT_PARENT_EQUALS_EXPRESSION = "element.parent=="; + + static final String RELATIONSHIP_TAG_EQUALS_EXPRESSION = "relationship.tag=="; + static final String RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION = "relationship.tag!="; + + static final String RELATIONSHIP_SOURCE_EQUALS_EXPRESSION = "relationship.source=="; + static final String RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION = "relationship.destination=="; + + static final String RELATIONSHIP_EQUALS_EXPRESSION = "relationship=="; + + static final String RELATIONSHIP = "->"; + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java new file mode 100644 index 000000000..17fbedb4d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -0,0 +1,1033 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Main DSL parser class - forms the API for using the parser. + */ +public final class StructurizrDslParser extends StructurizrDslTokens { + + private static final String BOM = "\uFEFF"; + + private static final Pattern EMPTY_LINE_PATTERN = Pattern.compile("^\\s*"); + + private static final Pattern COMMENT_PATTERN = Pattern.compile("^\\s*?(//|#).*$"); + private static final String MULTI_LINE_COMMENT_START_TOKEN = "/*"; + private static final String MULTI_LINE_COMMENT_END_TOKEN = "*/"; + private static final String MULTI_LINE_SEPARATOR = "\\"; + + private static final Pattern STRING_SUBSTITUTION_PATTERN = Pattern.compile("(\\$\\{[a-zA-Z0-9-_.]+?})"); + + private static final String STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME = "structurizr.dsl.identifier"; + + private Charset characterEncoding = StandardCharsets.UTF_8; + private IdentifierScope identifierScope = IdentifierScope.Flat; + private Stack contextStack; + private Set parsedTokens = new HashSet<>(); + private IdentifiersRegister identifiersRegister; + private Map constants; + + private List dslSourceLines = new ArrayList<>(); + private Workspace workspace; + private boolean extendingWorkspace = false; + + private boolean restricted = false; + + /** + * Creates a new instance of the parser. + */ + public StructurizrDslParser() { + contextStack = new Stack<>(); + identifiersRegister = new IdentifiersRegister(); + constants = new HashMap<>(); + } + + /** + * Provides a way to change the character encoding used by the DSL parser. + * + * @param characterEncoding a Charset instance + */ + public void setCharacterEncoding(Charset characterEncoding) { + if (characterEncoding == null) { + throw new IllegalArgumentException("A character encoding must be specified"); + } + + this.characterEncoding = characterEncoding; + } + + IdentifierScope getIdentifierScope() { + return identifierScope; + } + + void setIdentifierScope(IdentifierScope identifierScope) { + if (identifierScope == null) { + identifierScope = IdentifierScope.Flat; + } + + this.identifierScope = identifierScope; + this.identifiersRegister.setIdentifierScope(identifierScope); + } + + /** + * Sets whether to run this parser in restricted mode (this stops !include, !docs, !adrs from working). + * + * @param restricted true for restricted mode, false otherwise + */ + public void setRestricted(boolean restricted) { + this.restricted = restricted; + } + + /** + * Gets the workspace that has been created by parsing the Structurizr DSL. + * + * @return a Workspace instance + */ + public Workspace getWorkspace() { + if (workspace != null) { + DslUtils.setDsl(workspace, getParsedDsl()); + } + + return workspace; + } + + private String getParsedDsl() { + StringBuilder buf = new StringBuilder(); + + for (String line : dslSourceLines) { + buf.append(line); + buf.append(System.lineSeparator()); + } + + return buf.toString(); + } + + void parse(DslParserContext context, File path) throws StructurizrDslParserException { + parse(path); + + context.copyFrom(identifiersRegister); + } + + /** + * Parses the specified Structurizr DSL file(s), adding the parsed content to the workspace. + * If "path" represents a single file, that single file will be parsed. + * If "path" represents a directory, all files in that directory (recursively) will be parsed. + * + * @param path a File object representing a file or directory + * @throws StructurizrDslParserException when something goes wrong + */ + public void parse(File path) throws StructurizrDslParserException { + if (path == null) { + throw new StructurizrDslParserException("A file must be specified"); + } + + if (!path.exists()) { + throw new StructurizrDslParserException("The file at " + path.getAbsolutePath() + " does not exist"); + } + + List files = FileUtils.findFiles(path); + try { + for (File file : files) { + parse(Files.readAllLines(file.toPath(), characterEncoding), file); + } + } catch (IOException e) { + throw new StructurizrDslParserException(e.getMessage()); + } + } + + void parse(DslParserContext context, String dsl) throws StructurizrDslParserException { + parse(dsl); + + context.copyFrom(identifiersRegister); + } + + /** + * Parses the specified Structurizr DSL fragment, adding the parsed content to the workspace. + * + * @param dsl a DSL fragment + * @throws StructurizrDslParserException when something goes wrong + */ + public void parse(String dsl) throws StructurizrDslParserException { + if (StringUtils.isNullOrEmpty(dsl)) { + throw new StructurizrDslParserException("A DSL fragment must be specified"); + } + + List lines = Arrays.asList(dsl.split("\\r?\\n")); + parse(lines, new File(".")); + } + + private List preProcessLines(List lines) { + List dslLines = new ArrayList<>(); + + int lineNumber = 1; + StringBuilder buf = new StringBuilder(); + boolean lineComplete = true; + + for (String line : lines) { + if (line.endsWith(MULTI_LINE_SEPARATOR)) { + buf.append(line, 0, line.length()-1); + lineComplete = false; + } else { + if (lineComplete) { + buf.append(line); + } else { + buf.append(line.stripLeading()); + lineComplete = true; + } + } + + if (lineComplete) { + dslLines.add(new DslLine(buf.toString(), lineNumber)); + buf = new StringBuilder(); + } + + lineNumber++; + } + + return dslLines; + } + + void parse(List lines, File dslFile) throws StructurizrDslParserException { + List dslLines = preProcessLines(lines); + + for (DslLine dslLine : dslLines) { + boolean includeInDslSourceLines = true; + + String line = dslLine.getSource(); + + if (line.startsWith(BOM)) { + // this caters for files encoded as "UTF-8 with BOM" + line = line.substring(1); + } + + try { + if (EMPTY_LINE_PATTERN.matcher(line).matches()) { + // do nothing + } else if (COMMENT_PATTERN.matcher(line).matches()) { + // do nothing + } else if (inContext(InlineScriptDslContext.class)) { + if (DslContext.CONTEXT_END_TOKEN.equals(line.trim())) { + endContext(); + } else { + getContext(InlineScriptDslContext.class).addLine(line); + } + } else { + List listOfTokens = new Tokenizer().tokenize(line); + listOfTokens = listOfTokens.stream().map(this::substituteStrings).collect(Collectors.toList()); + + Tokens tokens = new Tokens(listOfTokens); + + String identifier = null; + if (tokens.size() > 3 && ASSIGNMENT_OPERATOR_TOKEN.equals(tokens.get(1))) { + identifier = tokens.get(0); + identifiersRegister.validateIdentifierName(identifier); + + tokens = new Tokens(listOfTokens.subList(2, listOfTokens.size())); + } + + String firstToken = tokens.get(0); + + if (line.trim().startsWith(MULTI_LINE_COMMENT_START_TOKEN) && line.trim().endsWith(MULTI_LINE_COMMENT_END_TOKEN)) { + // do nothing + } else if (firstToken.startsWith(MULTI_LINE_COMMENT_START_TOKEN)) { + startContext(new CommentDslContext()); + + } else if (inContext(CommentDslContext.class) && line.trim().endsWith(MULTI_LINE_COMMENT_END_TOKEN)) { + endContext(); + + } else if (inContext(CommentDslContext.class)) { + // do nothing + + } else if (DslContext.CONTEXT_END_TOKEN.equals(tokens.get(0))) { + endContext(); + + } else if (INCLUDE_FILE_TOKEN.equalsIgnoreCase(firstToken)) { + if (!restricted || tokens.get(1).startsWith("https://")) { + String leadingSpace = line.substring(0, line.indexOf(INCLUDE_FILE_TOKEN)); + + IncludedDslContext context = new IncludedDslContext(dslFile); + new IncludeParser().parse(context, tokens); + for (IncludedFile includedFile : context.getFiles()) { + List paddedLines = new ArrayList<>(); + for (String unpaddedLine : includedFile.getLines()) { + paddedLines.add(leadingSpace + unpaddedLine); + } + + parse(paddedLines, includedFile.getFile()); + } + + includeInDslSourceLines = false; + } + + } else if (PLUGIN_TOKEN.equalsIgnoreCase(firstToken)) { + if (!restricted) { + String fullyQualifiedClassName = new PluginParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new PluginDslContext(fullyQualifiedClassName, dslFile, this)); + if (!shouldStartContext(tokens)) { + // run the plugin immediately, without looking for parameters + endContext(); + } + } + + } else if (inContext(PluginDslContext.class)) { + new PluginParser().parseParameter(getContext(PluginDslContext.class), tokens); + + } else if (SCRIPT_TOKEN.equalsIgnoreCase(firstToken)) { + if (!restricted) { + ScriptParser scriptParser = new ScriptParser(); + if (scriptParser.isInlineScript(tokens)) { + String language = scriptParser.parseInline(tokens.withoutContextStartToken()); + startContext(new InlineScriptDslContext(getContext(), dslFile, language)); + } else { + String filename = scriptParser.parseExternal(tokens.withoutContextStartToken()); + startContext(new ExternalScriptDslContext(getContext(), dslFile, filename)); + + if (shouldStartContext(tokens)) { + // we'll wait for parameters before executing the script + } else { + endContext(); + } + } + } + + } else if (inContext(ExternalScriptDslContext.class)) { + new ScriptParser().parseParameter(getContext(ExternalScriptDslContext.class), tokens); + + } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(EnterpriseDslContext.class) || inContext(CustomElementDslContext.class) || inContext(PersonDslContext.class) || inContext(SoftwareSystemDslContext.class) || inContext(ContainerDslContext.class) || inContext(ComponentDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(DeploymentNodeDslContext.class) || inContext(InfrastructureNodeDslContext.class) || inContext(SoftwareSystemInstanceDslContext.class) || inContext(ContainerInstanceDslContext.class))) { + Relationship relationship = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipDslContext(relationship)); + } + + registerIdentifier(identifier, relationship); + + } else if (tokens.size() >= 2 && RELATIONSHIP_TOKEN.equals(tokens.get(0)) && (inContext(CustomElementDslContext.class) || inContext(PersonDslContext.class) || inContext(SoftwareSystemDslContext.class) || inContext(ContainerDslContext.class) || inContext(ComponentDslContext.class) || inContext(DeploymentNodeDslContext.class) || inContext(InfrastructureNodeDslContext.class) || inContext(SoftwareSystemInstanceDslContext.class) || inContext(ContainerInstanceDslContext.class))) { + Relationship relationship = new ImplicitRelationshipParser().parse(getContext(ModelItemDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipDslContext(relationship)); + } + + registerIdentifier(identifier, relationship); + + } else if ((REF_TOKEN.equalsIgnoreCase(firstToken) || EXTEND_TOKEN.equalsIgnoreCase(firstToken)) && (inContext(ModelDslContext.class))) { + ModelItem modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + if (modelItem instanceof Person) { + startContext(new PersonDslContext((Person)modelItem)); + } else if (modelItem instanceof SoftwareSystem) { + startContext(new SoftwareSystemDslContext((SoftwareSystem)modelItem)); + } else if (modelItem instanceof Container) { + startContext(new ContainerDslContext((Container) modelItem)); + } else if (modelItem instanceof Component) { + startContext(new ComponentDslContext((Component)modelItem)); + } else if (modelItem instanceof DeploymentEnvironment) { + startContext(new DeploymentEnvironmentDslContext(((DeploymentEnvironment)modelItem).getName())); + } else if (modelItem instanceof DeploymentNode) { + startContext(new DeploymentNodeDslContext((DeploymentNode)modelItem)); + } else if (modelItem instanceof InfrastructureNode) { + startContext(new InfrastructureNodeDslContext((InfrastructureNode)modelItem)); + } else if (modelItem instanceof SoftwareSystemInstance) { + startContext(new SoftwareSystemInstanceDslContext((SoftwareSystemInstance)modelItem)); + } else if (modelItem instanceof ContainerInstance) { + startContext(new ContainerInstanceDslContext((ContainerInstance)modelItem)); + } else if (modelItem instanceof Relationship) { + startContext(new RelationshipDslContext((Relationship)modelItem)); + } + } + + if (!StringUtils.isNullOrEmpty(identifier)) { + if (modelItem instanceof Element) { + registerIdentifier(identifier, (Element)modelItem); + } else if (modelItem instanceof Relationship) { + registerIdentifier(identifier, (Relationship)modelItem); + } + } + + } else if (CUSTOM_ELEMENT_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { + CustomElement customElement = new CustomElementParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new CustomElementDslContext(customElement)); + } + + registerIdentifier(identifier, customElement); + + } else if (PERSON_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(EnterpriseDslContext.class))) { + Person person = new PersonParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new PersonDslContext(person)); + } + + registerIdentifier(identifier, person); + + } else if (SOFTWARE_SYSTEM_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(EnterpriseDslContext.class))) { + SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new SoftwareSystemDslContext(softwareSystem)); + } + + registerIdentifier(identifier, softwareSystem); + + } else if (CONTAINER_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + Container container = new ContainerParser().parse(getContext(SoftwareSystemDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new ContainerDslContext(container)); + } + + registerIdentifier(identifier, container); + + } else if (COMPONENT_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + Component component = new ComponentParser().parse(getContext(ContainerDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new ComponentDslContext(component)); + } + + registerIdentifier(identifier, component); + + } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { + ElementGroup group = new GroupParser().parse(getContext(ModelDslContext.class), tokens); + + startContext(new ModelDslContext(group)); + registerIdentifier(identifier, group); + } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(EnterpriseDslContext.class)) { + ElementGroup group = new GroupParser().parse(getContext(EnterpriseDslContext.class), tokens); + + startContext(new EnterpriseDslContext(group)); + registerIdentifier(identifier, group); + } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + ElementGroup group = new GroupParser().parse(getContext(SoftwareSystemDslContext.class), tokens); + + SoftwareSystem softwareSystem = getContext(SoftwareSystemDslContext.class).getSoftwareSystem(); + group.setParent(softwareSystem); + startContext(new SoftwareSystemDslContext(softwareSystem, group)); + registerIdentifier(identifier, group); + } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + ElementGroup group = new GroupParser().parse(getContext(ContainerDslContext.class), tokens); + + Container container = getContext(ContainerDslContext.class).getContainer(); + group.setParent(container); + startContext(new ContainerDslContext(container, group)); + registerIdentifier(identifier, group); + } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentEnvironmentDslContext.class)) { + ElementGroup group = new GroupParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens); + + String environment = getContext(DeploymentEnvironmentDslContext.class).getEnvironment(); + startContext(new DeploymentEnvironmentDslContext(environment, group)); + registerIdentifier(identifier, group); + } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + ElementGroup group = new GroupParser().parse(getContext(DeploymentNodeDslContext.class), tokens); + + DeploymentNode deploymentNode = getContext(DeploymentNodeDslContext.class).getDeploymentNode(); + startContext(new DeploymentNodeDslContext(deploymentNode, group)); + registerIdentifier(identifier, group); + } else if (TAGS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + new ModelItemParser().parseTags(getContext(ModelItemDslContext.class), tokens); + + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && getContext(ModelItemDslContext.class).getModelItem() instanceof Element && !getContext(ModelItemDslContext.class).hasGroup()) { + new ModelItemParser().parseDescription(getContext(ModelItemDslContext.class), tokens); + + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class) && !getContext(ContainerDslContext.class).hasGroup()) { + new ContainerParser().parseTechnology(getContext(ContainerDslContext.class), tokens); + + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class) && !getContext(ComponentDslContext.class).hasGroup()) { + new ComponentParser().parseTechnology(getContext(ComponentDslContext.class), tokens); + + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + new DeploymentNodeParser().parseTechnology(getContext(DeploymentNodeDslContext.class), tokens); + + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(InfrastructureNodeDslContext.class)) { + new InfrastructureNodeParser().parseTechnology(getContext(InfrastructureNodeDslContext.class), tokens); + + } else if (INSTANCES_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + new DeploymentNodeParser().parseInstances(getContext(DeploymentNodeDslContext.class), tokens); + + } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + new ModelItemParser().parseUrl(getContext(ModelItemDslContext.class), tokens); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + startContext(new PropertiesDslContext(workspace)); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { + startContext(new PropertiesDslContext(workspace.getModel())); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ConfigurationDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + startContext(new PropertiesDslContext(getContext(ConfigurationDslContext.class).getWorkspace())); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + startContext(new PropertiesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + startContext(new PropertiesDslContext(workspace.getViews().getConfiguration())); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewDslContext.class)) { + startContext(new PropertiesDslContext(getContext(ViewDslContext.class).getView())); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + startContext(new PropertiesDslContext(getContext((ElementStyleDslContext.class)).getStyle())); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + startContext(new PropertiesDslContext(getContext((RelationshipStyleDslContext.class)).getStyle())); + + } else if (inContext(PropertiesDslContext.class)) { + new PropertyParser().parse(getContext(PropertiesDslContext.class), tokens); + + } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + startContext(new ModelItemPerspectivesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + + } else if (inContext(ModelItemPerspectivesDslContext.class)) { + new ModelItemParser().parsePerspective(getContext(ModelItemPerspectivesDslContext.class), tokens); + + } else if (WORKSPACE_TOKEN.equalsIgnoreCase(firstToken) && contextStack.empty()) { + if (parsedTokens.contains(WORKSPACE_TOKEN)) { + throw new RuntimeException("Multiple workspaces are not permitted in a DSL definition"); + } + DslParserContext dslParserContext = new DslParserContext(dslFile, restricted); + dslParserContext.setIdentifierRegister(identifiersRegister); + + workspace = new WorkspaceParser().parse(dslParserContext, tokens.withoutContextStartToken()); + extendingWorkspace = !workspace.getModel().isEmpty(); + startContext(new WorkspaceDslContext()); + parsedTokens.add(WORKSPACE_TOKEN); + } else if (IMPLIED_RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) || IMPLIED_RELATIONSHIPS_TOKEN.substring(1).equalsIgnoreCase(firstToken)) { + new ImpliedRelationshipsParser().parse(getContext(), tokens); + + } else if (NAME_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + new WorkspaceParser().parseName(getContext(), tokens); + + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + new WorkspaceParser().parseDescription(getContext(), tokens); + + } else if (MODEL_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + if (parsedTokens.contains(MODEL_TOKEN)) { + throw new RuntimeException("Multiple models are not permitted in a DSL definition"); + } + + startContext(new ModelDslContext()); + parsedTokens.add(MODEL_TOKEN); + + } else if (VIEWS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + if (parsedTokens.contains(VIEWS_TOKEN)) { + throw new RuntimeException("Multiple view sets are not permitted in a DSL definition"); + } + + startContext(new ViewsDslContext()); + parsedTokens.add(VIEWS_TOKEN); + + } else if (BRANDING_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + startContext(new BrandingDslContext(dslFile)); + + } else if (BRANDING_LOGO_TOKEN.equalsIgnoreCase(firstToken) && inContext(BrandingDslContext.class)) { + new BrandingParser().parseLogo(getContext(BrandingDslContext.class), tokens, restricted); + + } else if (BRANDING_FONT_TOKEN.equalsIgnoreCase(firstToken) && inContext(BrandingDslContext.class)) { + new BrandingParser().parseFont(getContext(BrandingDslContext.class), tokens); + + } else if (STYLES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + startContext(new StylesDslContext()); + + } else if (ELEMENT_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) { + ElementStyle elementStyle = new ElementStyleParser().parseElementStyle(getContext(), tokens.withoutContextStartToken()); + startContext(new ElementStyleDslContext(elementStyle, dslFile)); + + } else if (ELEMENT_STYLE_BACKGROUND_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseBackground(getContext(ElementStyleDslContext.class), tokens); + + } else if ((ELEMENT_STYLE_COLOUR_TOKEN.equalsIgnoreCase(firstToken) || ELEMENT_STYLE_COLOR_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseColour(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_STROKE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseStroke(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_STROKE_WIDTH_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseStrokeWidth(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_SHAPE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseShape(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_BORDER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseBorder(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_OPACITY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseOpacity(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_WIDTH_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseWidth(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_HEIGHT_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseHeight(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_FONT_SIZE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseFontSize(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_METADATA_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseMetadata(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseDescription(getContext(ElementStyleDslContext.class), tokens); + + } else if (ELEMENT_STYLE_ICON_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseIcon(getContext(ElementStyleDslContext.class), tokens, restricted); + + } else if (RELATIONSHIP_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) { + RelationshipStyle relationshipStyle = new RelationshipStyleParser().parseRelationshipStyle(getContext(), tokens.withoutContextStartToken()); + startContext(new RelationshipStyleDslContext(relationshipStyle)); + + } else if (RELATIONSHIP_STYLE_THICKNESS_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseThickness(getContext(RelationshipStyleDslContext.class), tokens); + + } else if ((RELATIONSHIP_STYLE_COLOUR_TOKEN.equalsIgnoreCase(firstToken) || RELATIONSHIP_STYLE_COLOR_TOKEN.equalsIgnoreCase(firstToken)) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseColour(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (RELATIONSHIP_STYLE_DASHED_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseDashed(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (RELATIONSHIP_STYLE_OPACITY_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseOpacity(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (RELATIONSHIP_STYLE_WIDTH_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseWidth(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (RELATIONSHIP_STYLE_FONT_SIZE_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseFontSize(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (RELATIONSHIP_STYLE_POSITION_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parsePosition(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (RELATIONSHIP_STYLE_LINE_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseLineStyle(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (RELATIONSHIP_STYLE_ROUTING_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseRouting(getContext(RelationshipStyleDslContext.class), tokens); + + } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { + new EnterpriseParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new EnterpriseDslContext()); + + } else if (DEPLOYMENT_ENVIRONMENT_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { + String environment = new DeploymentEnvironmentParser().parse(tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new DeploymentEnvironmentDslContext(environment)); + } + + registerIdentifier(identifier, new DeploymentEnvironment(environment)); + + } else if (DEPLOYMENT_GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentEnvironmentDslContext.class)) { + String group = new DeploymentGroupParser().parse(tokens.withoutContextStartToken()); + + registerIdentifier(identifier, new DeploymentGroup(group)); + + } else if (DEPLOYMENT_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentEnvironmentDslContext.class)) { + DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new DeploymentNodeDslContext(deploymentNode)); + } + + registerIdentifier(identifier, deploymentNode); + } else if (DEPLOYMENT_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new DeploymentNodeDslContext(deploymentNode)); + } + + registerIdentifier(identifier, deploymentNode); + } else if (INFRASTRUCTURE_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + InfrastructureNode infrastructureNode = new InfrastructureNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new InfrastructureNodeDslContext(infrastructureNode)); + } + + registerIdentifier(identifier, infrastructureNode); + + } else if (SOFTWARE_SYSTEM_INSTANCE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + SoftwareSystemInstance softwareSystemInstance = new SoftwareSystemInstanceParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new SoftwareSystemInstanceDslContext(softwareSystemInstance)); + } + + registerIdentifier(identifier, softwareSystemInstance); + + } else if (CONTAINER_INSTANCE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + ContainerInstance containerInstance = new ContainerInstanceParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new ContainerInstanceDslContext(containerInstance)); + } + + registerIdentifier(identifier, containerInstance); + + } else if (HEALTH_CHECK_TOKEN.equalsIgnoreCase(firstToken) && inContext(StaticStructureElementInstanceDslContext.class)) { + new HealthCheckParser().parse(getContext(StaticStructureElementInstanceDslContext.class), tokens.withoutContextStartToken()); + } else if (CUSTOM_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + CustomView view = new CustomViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new CustomViewDslContext(view)); + + } else if (SYSTEM_LANDSCAPE_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + SystemLandscapeView view = new SystemLandscapeViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new SystemLandscapeViewDslContext(view)); + + } else if (SYSTEM_CONTEXT_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + SystemContextView view = new SystemContextViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new SystemContextViewDslContext(view)); + + } else if (CONTAINER_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + ContainerView view = new ContainerViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new ContainerViewDslContext(view)); + + } else if (COMPONENT_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + ComponentView view = new ComponentViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new ComponentViewDslContext(view)); + + } else if (DYNAMIC_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + DynamicView view = new DynamicViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new DynamicViewDslContext(view)); + + } else if (DEPLOYMENT_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + DeploymentView view = new DeploymentViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new DeploymentViewDslContext(view)); + + } else if (FILTERED_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + FilteredView view = new FilteredViewParser().parse(getContext(), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new FilteredViewDslContext(view)); + } + + } else if (IMAGE_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + ImageView view = new ImageViewParser().parse(getContext(), tokens.withoutContextStartToken()); + startContext(new ImageViewDslContext(view)); + + } else if (DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(firstToken) && inContext(DynamicViewDslContext.class)) { + startContext(new DynamicViewParallelSequenceDslContext(getContext(DynamicViewDslContext.class))); + + } else if (INCLUDE_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(CustomViewDslContext.class)) { + new CustomViewContentParser().parseInclude(getContext(CustomViewDslContext.class), tokens); + + } else if (EXCLUDE_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(CustomViewDslContext.class)) { + new CustomViewContentParser().parseExclude(getContext(CustomViewDslContext.class), tokens); + + } else if (ANIMATION_STEP_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(CustomViewDslContext.class)) { + new CustomViewAnimationStepParser().parse(getContext(CustomViewDslContext.class), tokens); + + } else if (ANIMATION_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(CustomViewDslContext.class)) { + startContext(new CustomViewAnimationDslContext(getContext(CustomViewDslContext.class).getCustomView())); + + } else if (inContext(CustomViewAnimationDslContext.class)) { + new CustomViewAnimationStepParser().parse(getContext(CustomViewAnimationDslContext.class), tokens); + + } else if (INCLUDE_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(StaticViewDslContext.class)) { + new StaticViewContentParser().parseInclude(getContext(StaticViewDslContext.class), tokens); + + } else if (EXCLUDE_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(StaticViewDslContext.class)) { + new StaticViewContentParser().parseExclude(getContext(StaticViewDslContext.class), tokens); + + } else if (ANIMATION_STEP_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(StaticViewDslContext.class)) { + new StaticViewAnimationStepParser().parse(getContext(StaticViewDslContext.class), tokens); + + } else if (ANIMATION_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(StaticViewDslContext.class)) { + startContext(new StaticViewAnimationDslContext(getContext(StaticViewDslContext.class).getView())); + + } else if (inContext(StaticViewAnimationDslContext.class)) { + new StaticViewAnimationStepParser().parse(getContext(StaticViewAnimationDslContext.class), tokens); + + } else if (INCLUDE_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentViewDslContext.class)) { + new DeploymentViewContentParser().parseInclude(getContext(DeploymentViewDslContext.class), tokens); + + } else if (EXCLUDE_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentViewDslContext.class)) { + new DeploymentViewContentParser().parseExclude(getContext(DeploymentViewDslContext.class), tokens); + + } else if (ANIMATION_STEP_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentViewDslContext.class)) { + new DeploymentViewAnimationStepParser().parse(getContext(DeploymentViewDslContext.class), tokens); + + } else if (ANIMATION_IN_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentViewDslContext.class)) { + startContext(new DeploymentViewAnimationDslContext(getContext(DeploymentViewDslContext.class).getView())); + + } else if (inContext(DeploymentViewAnimationDslContext.class)) { + new DeploymentViewAnimationStepParser().parse(getContext(DeploymentViewAnimationDslContext.class), tokens); + + } else if (AUTOLAYOUT_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewDslContext.class)) { + new AutoLayoutParser().parse(getContext(ModelViewDslContext.class), tokens); + + } else if (DEFAULT_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewDslContext.class)) { + new DefaultViewParser().parse(getContext(ViewDslContext.class)); + + } else if (VIEW_TITLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewDslContext.class)) { + new ViewParser().parseTitle(getContext(ViewDslContext.class), tokens); + + } else if (VIEW_DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewDslContext.class)) { + new ViewParser().parseDescription(getContext(ViewDslContext.class), tokens); + + } else if (PLANTUML_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { + new ImageViewContentParser(restricted).parsePlantUML(getContext(ImageViewDslContext.class), dslFile, tokens); + + } else if (MERMAID_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { + new ImageViewContentParser(restricted).parseMermaid(getContext(ImageViewDslContext.class), dslFile, tokens); + + } else if (KROKI_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { + new ImageViewContentParser(restricted).parseKroki(getContext(ImageViewDslContext.class), dslFile, tokens); + + } else if (IMAGE_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { + new ImageViewContentParser(restricted).parseImage(getContext(ImageViewDslContext.class), dslFile, tokens); + + } else if (inContext(DynamicViewDslContext.class)) { + RelationshipView relationshipView = new DynamicViewContentParser().parseRelationship(getContext(DynamicViewDslContext.class), tokens); + + if (inContext(DynamicViewParallelSequenceDslContext.class)) { + getContext(DynamicViewParallelSequenceDslContext.class).hasRelationships(true); + } + + if (shouldStartContext(tokens)) { + startContext(new DynamicViewRelationshipContext(relationshipView)); + } + + } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(DynamicViewRelationshipContext.class)) { + new DynamicViewRelationshipParser().parseUrl(getContext(DynamicViewRelationshipContext.class), tokens.withoutContextStartToken()); + + } else if (THEME_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { + new ThemeParser().parseTheme(getContext(), tokens); + + } else if (THEMES_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { + new ThemeParser().parseThemes(getContext(), tokens); + + } else if (TERMINOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + startContext(new TerminologyDslContext()); + + } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseEnterprise(getContext(), tokens); + + } else if (PERSON_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parsePerson(getContext(), tokens); + + } else if (SOFTWARE_SYSTEM_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseSoftwareSystem(getContext(), tokens); + + } else if (CONTAINER_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseContainer(getContext(), tokens); + + } else if (COMPONENT_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseComponent(getContext(), tokens); + + } else if (DEPLOYMENT_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseDeploymentNode(getContext(), tokens); + + } else if (INFRASTRUCTURE_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseInfrastructureNode(getContext(), tokens); + + } else if (TERMINOLOGY_RELATIONSHIP_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseRelationship(getContext(), tokens); + + } else if (CONFIGURATION_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + startContext(new ConfigurationDslContext()); + + } else if (SCOPE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ConfigurationDslContext.class)) { + new ConfigurationParser().parseScope(getContext(), tokens); + + } else if (VISIBILITY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ConfigurationDslContext.class)) { + new ConfigurationParser().parseVisibility(getContext(), tokens); + + } else if (USERS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ConfigurationDslContext.class)) { + startContext(new UsersDslContext()); + + } else if (inContext(UsersDslContext.class)) { + new UserRoleParser().parse(getContext(), tokens); + + } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + if (!restricted) { + new DocsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); + } + + } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + if (!restricted) { + new DocsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); + } + + } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + if (!restricted) { + new DocsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); + } + + } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) { + if (!restricted) { + new DocsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); + } + + } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + if (!restricted) { + new AdrsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); + } + + } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + if (!restricted) { + new AdrsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); + } + + } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + if (!restricted) { + new AdrsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); + } + + } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) { + if (!restricted) { + new AdrsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); + } + + } else if (CONSTANT_TOKEN.equalsIgnoreCase(firstToken)) { + Constant constant = new ConstantParser().parse(getContext(), tokens); + constants.put(constant.getName(), constant); + + } else if (IDENTIFIERS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + setIdentifierScope(new IdentifierScopeParser().parse(getContext(), tokens)); + + } else { + String[] expectedTokens; + if (getContext() == null) { + if (getWorkspace() == null) { + // the workspace hasn't yet been created + expectedTokens = new String[]{ + StructurizrDslTokens.WORKSPACE_TOKEN + }; + } else { + expectedTokens = new String[0]; + } + } else { + expectedTokens = getContext().getPermittedTokens(); + } + + if (expectedTokens.length > 0) { + StringBuilder buf = new StringBuilder(); + for (String expectedToken : expectedTokens) { + buf.append(expectedToken); + buf.append(", "); + } + throw new StructurizrDslParserException("Unexpected tokens (expected: " + buf.substring(0, buf.length() - 2) + ")"); + } else { + throw new StructurizrDslParserException("Unexpected tokens"); + } + } + } + + if (includeInDslSourceLines) { + dslSourceLines.add(line); + } + } catch (Exception e) { + if (e.getMessage() != null) { + throw new StructurizrDslParserException(e.getMessage(), dslFile, dslLine.getLineNumber(), line); + } else { + throw new StructurizrDslParserException(e.getClass().getSimpleName(), dslFile, dslLine.getLineNumber(), line); + } + } + } + } + + private String substituteStrings(String token) { + Matcher m = STRING_SUBSTITUTION_PATTERN.matcher(token); + while (m.find()) { + String before = m.group(0); + String after = null; + String name = before.substring(2, before.length()-1); + if (constants.containsKey(name)) { + after = constants.get(name).getValue(); + } else { + if (!restricted) { + String environmentVariable = System.getenv().get(name); + if (environmentVariable != null) { + after = environmentVariable; + } + } + } + + if (after != null) { + token = token.replace(before, after); + } + } + + return token; + } + + private boolean shouldStartContext(Tokens tokens) { + return DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(tokens.get(tokens.size()-1)); + } + + private void startContext(DslContext context) { + context.setWorkspace(workspace); + context.setIdentifierRegister(identifiersRegister); + context.setExtendingWorkspace(extendingWorkspace); + contextStack.push(context); + } + + private DslContext getContext() { + if (!contextStack.empty()) { + return contextStack.peek(); + } else { + return null; + } + } + + private T getContext(Class clazz) throws StructurizrDslParserException { + if (inContext(clazz)) { + return (T)contextStack.peek(); + } else { + throw new StructurizrDslParserException("Expected " + clazz.getName() + " but got " + contextStack.peek().getClass().getName()); + } + } + + private void endContext() throws StructurizrDslParserException { + if (!contextStack.empty()) { + DslContext context = contextStack.pop(); + context.end(); + } else { + throw new StructurizrDslParserException("Unexpected end of context"); + } + } + + /** + * Gets the identifier register in use (this is the mapping of DSL identifiers to elements/relationships). + * + * @return an IdentifiersRegister object + */ + public IdentifiersRegister getIdentifiersRegister() { + return identifiersRegister; + } + + private void registerIdentifier(String identifier, Element element) { + identifiersRegister.register(identifier, element); + element.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(element)); + } + + private void registerIdentifier(String identifier, Relationship relationship) { + identifiersRegister.register(identifier, relationship); + relationship.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(relationship)); + } + + private boolean inContext(Class clazz) { + if (contextStack.empty()) { + return false; + } + + return clazz.isAssignableFrom(contextStack.peek().getClass()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java new file mode 100644 index 000000000..9868ba0a0 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java @@ -0,0 +1,49 @@ +package com.structurizr.dsl; + +import java.io.File; + +/** + * Throw when there are parsing errors. + */ +public final class StructurizrDslParserException extends Exception { + + /** line number */ + private int lineNumber; + + /** line */ + private String line; + + /** + * Creates a new instance with the specified message. + * + * @param message the message + */ + StructurizrDslParserException(String message) { + super(message); + } + + StructurizrDslParserException(String message, File dslFile, int lineNumber, String line) { + super((message.endsWith(".") ? message.substring(0, message.length()-1) : message) + " at line " + lineNumber + (dslFile != null ? " of " + dslFile.getAbsolutePath() : "") + ": " + line.trim()); + this.lineNumber = lineNumber; + this.line = line; + } + + /** + * Gets the line number associated with the parsing exception. + * + * @return the line number, an integer + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Gets the line associated with the parsing exception. + * + * @return the line, as a String + */ + public String getLine() { + return line; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPlugin.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPlugin.java new file mode 100644 index 000000000..cd73e109c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPlugin.java @@ -0,0 +1,15 @@ +package com.structurizr.dsl; + +/** + * An interface implemented by DSL plugins. + */ +public interface StructurizrDslPlugin { + + /** + * Called to execute the plugin. + * + * @param context a StructurizrDslPluginContext instance + */ + void run(StructurizrDslPluginContext context); + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPluginContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPluginContext.java new file mode 100644 index 000000000..10f104907 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslPluginContext.java @@ -0,0 +1,88 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.StringUtils; + +import java.io.File; +import java.util.Map; + +/** + * Used to pass contextual information to DSL plugins when they are executed. + */ +public class StructurizrDslPluginContext { + + private final StructurizrDslParser dslParser; + private final File dslFile; + private final Workspace workspace; + private final Map parameters; + + /** + * Creates a new instance. + * + * @param dslParser a reference to the DSL parser that loaded the plugin + * @param dslFile a reference to the DSL file that loaded the plugin + * @param workspace the workspace + * @param parameters a map of name/value pairs representing parameters + */ + public StructurizrDslPluginContext(StructurizrDslParser dslParser, File dslFile, Workspace workspace, Map parameters) { + this.dslParser = dslParser; + this.dslFile = dslFile; + this.workspace = workspace; + this.parameters = parameters; + } + + /** + * Gets a reference to the DSL parser that initiated this plugin context. + * + * @return a StructurizrDslParser instance + */ + public StructurizrDslParser getDslParser() { + return dslParser; + } + + /** + * Gets a reference to the DSL file that initiated this plugin context. + * + * @return a File instance + */ + public File getDslFile() { + return dslFile; + } + + /** + * Gets the current workspace. + * + * @return a Workspace instance + */ + public Workspace getWorkspace() { + return workspace; + } + + /** + * Gets the named parameter. + * + * @param name the parameter name + * @return the parameter value (null if unset) + */ + public String getParameter(String name) { + return parameters.get(name); + } + + /** + * Gets the named parameter, with a default value if unset. + * + * @param name the parameter name + * @param defaultValue the default value + * @return the parameter value, or defaultValue if unset + */ + public String getParameter(String name, String defaultValue) { + String value = parameters.get(name); + + if (StringUtils.isNullOrEmpty(value)) { + value = defaultValue; + } + + return value; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslScriptContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslScriptContext.java new file mode 100644 index 000000000..737c6ed6b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslScriptContext.java @@ -0,0 +1,76 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.StringUtils; + +import java.io.File; +import java.util.Map; + +/** + * Used to pass contextual information to DSL scripts when they are executed. + */ +public class StructurizrDslScriptContext { + + private final File dslFile; + private final Workspace workspace; + private final Map parameters; + + /** + * Creates a new instance. + * + * @param dslFile a reference to the DSL file that loaded the script + * @param workspace the workspace + * @param parameters a map of name/value pairs representing parameters + */ + public StructurizrDslScriptContext(File dslFile, Workspace workspace, Map parameters) { + this.dslFile = dslFile; + this.workspace = workspace; + this.parameters = parameters; + } + + /** + * Gets a reference to the DSL file that initiated this script context. + * + * @return a File instance + */ + public File getDslFile() { + return dslFile; + } + + /** + * Gets the current workspace. + * + * @return a Workspace instance + */ + public Workspace getWorkspace() { + return workspace; + } + + /** + * Gets the named parameter. + * + * @param name the parameter name + * @return the parameter value (null if unset) + */ + public String getParameter(String name) { + return parameters.get(name); + } + + /** + * Gets the named parameter, with a default value if unset. + * + * @param name the parameter name + * @param defaultValue the default value + * @return the parameter value, or defaultValue if unset + */ + public String getParameter(String name, String defaultValue) { + String value = parameters.get(name); + + if (StringUtils.isNullOrEmpty(value)) { + value = defaultValue; + } + + return value; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java new file mode 100644 index 000000000..946185e21 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -0,0 +1,109 @@ +package com.structurizr.dsl; + +/** + * Main DSL parser class - forms the API for using the parser. + */ +class StructurizrDslTokens { + + static final String ASSIGNMENT_OPERATOR_TOKEN = "="; + + static final String CUSTOM_ELEMENT_TOKEN = "element"; + static final String PERSON_TOKEN = "person"; + static final String SOFTWARE_SYSTEM_TOKEN = "softwareSystem"; + static final String RELATIONSHIP_TOKEN = "->"; + static final String CONTAINER_TOKEN = "container"; + static final String COMPONENT_TOKEN = "component"; + static final String GROUP_TOKEN = "group"; + static final String NAME_TOKEN = "name"; + static final String DESCRIPTION_TOKEN = "description"; + static final String TECHNOLOGY_TOKEN = "technology"; + static final String INSTANCES_TOKEN = "instances"; + static final String TAGS_TOKEN = "tags"; + static final String URL_TOKEN = "url"; + static final String PROPERTIES_TOKEN = "properties"; + static final String PERSPECTIVES_TOKEN = "perspectives"; + static final String WORKSPACE_TOKEN = "workspace"; + static final String EXTENDS_TOKEN = "extends"; + static final String SCOPE_TOKEN = "scope"; + static final String MODEL_TOKEN = "model"; + static final String VIEWS_TOKEN = "views"; + static final String ENTERPRISE_TOKEN = "enterprise"; + static final String DEPLOYMENT_ENVIRONMENT_TOKEN = "deploymentEnvironment"; + static final String DEPLOYMENT_GROUP_TOKEN = "deploymentGroup"; + static final String DEPLOYMENT_NODE_TOKEN = "deploymentNode"; + static final String INFRASTRUCTURE_NODE_TOKEN = "infrastructureNode"; + static final String SOFTWARE_SYSTEM_INSTANCE_TOKEN = "softwareSystemInstance"; + static final String CONTAINER_INSTANCE_TOKEN = "containerInstance"; + static final String HEALTH_CHECK_TOKEN = "healthCheck"; + static final String CUSTOM_VIEW_TOKEN = "custom"; + static final String SYSTEM_LANDSCAPE_VIEW_TOKEN = "systemLandscape"; + static final String SYSTEM_CONTEXT_VIEW_TOKEN = "systemContext"; + static final String CONTAINER_VIEW_TOKEN = "container"; + static final String COMPONENT_VIEW_TOKEN = "component"; + static final String DYNAMIC_VIEW_TOKEN = "dynamic"; + static final String DEPLOYMENT_VIEW_TOKEN = "deployment"; + static final String FILTERED_VIEW_TOKEN = "filtered"; + static final String IMAGE_VIEW_TOKEN = "image"; + static final String INCLUDE_IN_VIEW_TOKEN = "include"; + static final String EXCLUDE_IN_VIEW_TOKEN = "exclude"; + static final String ANIMATION_IN_VIEW_TOKEN = "animation"; + static final String ANIMATION_STEP_IN_VIEW_TOKEN = "animationStep"; + static final String AUTOLAYOUT_VIEW_TOKEN = "autolayout"; + static final String DEFAULT_VIEW_TOKEN = "default"; + static final String VIEW_TITLE_TOKEN = "title"; + static final String VIEW_DESCRIPTION_TOKEN = "description"; + static final String PLANTUML_TOKEN = "plantuml"; + static final String MERMAID_TOKEN = "mermaid"; + static final String KROKI_TOKEN = "kroki"; + static final String IMAGE_TOKEN = "image"; + static final String STYLES_TOKEN = "styles"; + static final String BRANDING_TOKEN = "branding"; + static final String BRANDING_LOGO_TOKEN = "logo"; + static final String BRANDING_FONT_TOKEN = "font"; + static final String ELEMENT_STYLE_TOKEN = "element"; + static final String ELEMENT_STYLE_SHAPE_TOKEN = "shape"; + static final String ELEMENT_STYLE_BACKGROUND_TOKEN = "background"; + static final String ELEMENT_STYLE_STROKE_TOKEN = "stroke"; + static final String ELEMENT_STYLE_STROKE_WIDTH_TOKEN = "strokeWidth"; + static final String ELEMENT_STYLE_COLOUR_TOKEN = "colour"; + static final String ELEMENT_STYLE_COLOR_TOKEN = "color"; + static final String ELEMENT_STYLE_ICON_TOKEN = "icon"; + static final String ELEMENT_STYLE_OPACITY_TOKEN = "opacity"; + static final String ELEMENT_STYLE_BORDER_TOKEN = "border"; + static final String ELEMENT_STYLE_FONT_SIZE_TOKEN = "fontSize"; + static final String ELEMENT_STYLE_WIDTH_TOKEN = "width"; + static final String ELEMENT_STYLE_HEIGHT_TOKEN = "height"; + static final String ELEMENT_STYLE_METADATA_TOKEN = "metadata"; + static final String ELEMENT_STYLE_DESCRIPTION_TOKEN = "description"; + static final String RELATIONSHIP_STYLE_TOKEN = "relationship"; + static final String RELATIONSHIP_STYLE_THICKNESS_TOKEN = "thickness"; + static final String RELATIONSHIP_STYLE_COLOUR_TOKEN = "colour"; + static final String RELATIONSHIP_STYLE_COLOR_TOKEN = "color"; + static final String RELATIONSHIP_STYLE_DASHED_TOKEN = "dashed"; + static final String RELATIONSHIP_STYLE_OPACITY_TOKEN = "opacity"; + static final String RELATIONSHIP_STYLE_ROUTING_TOKEN = "routing"; + static final String RELATIONSHIP_STYLE_LINE_STYLE_TOKEN = "style"; + static final String RELATIONSHIP_STYLE_FONT_SIZE_TOKEN = "fontSize"; + static final String RELATIONSHIP_STYLE_WIDTH_TOKEN = "width"; + static final String RELATIONSHIP_STYLE_POSITION_TOKEN = "position"; + static final String THEME_TOKEN = "theme"; + static final String THEMES_TOKEN = "themes"; + static final String CONFIGURATION_TOKEN = "configuration"; + static final String VISIBILITY_TOKEN = "visibility"; + static final String TERMINOLOGY_TOKEN = "terminology"; + static final String TERMINOLOGY_RELATIONSHIP_TOKEN = "relationship"; + static final String USERS_TOKEN = "users"; + static final String THIS_TOKEN = "this"; + + static final String INCLUDE_FILE_TOKEN = "!include"; + static final String DOCS_TOKEN = "!docs"; + static final String ADRS_TOKEN = "!adrs"; + static final String CONSTANT_TOKEN = "!constant"; + static final String IDENTIFIERS_TOKEN = "!identifiers"; + static final String IMPLIED_RELATIONSHIPS_TOKEN = "!impliedRelationships"; + static final String REF_TOKEN = "!ref"; + static final String EXTEND_TOKEN = "!extend"; + static final String PLUGIN_TOKEN = "!plugin"; + static final String SCRIPT_TOKEN = "!script"; + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java new file mode 100644 index 000000000..a27234faf --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java @@ -0,0 +1,13 @@ +package com.structurizr.dsl; + +final class StylesDslContext extends DslContext { + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ELEMENT_STYLE_TOKEN, + StructurizrDslTokens.RELATIONSHIP_STYLE_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewDslContext.java new file mode 100644 index 000000000..8fd9e05ae --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewDslContext.java @@ -0,0 +1,11 @@ +package com.structurizr.dsl; + +import com.structurizr.view.SystemContextView; + +final class SystemContextViewDslContext extends StaticViewDslContext { + + SystemContextViewDslContext(SystemContextView view) { + super(view); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java new file mode 100644 index 000000000..4dc004a95 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java @@ -0,0 +1,62 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.SystemContextView; + +final class SystemContextViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "systemContext [key] [description] {"; + + private static final String VIEW_TYPE = "SystemContext"; + + private static final int SOFTWARE_SYSTEM_IDENTIFIER_INDEX = 1; + private static final int KEY_INDEX = 2; + private static final int DESCRIPTION_INDEX = 3; + + SystemContextView parse(DslContext context, Tokens tokens) { + // systemContext [key] [description] { + + Workspace workspace = context.getWorkspace(); + SoftwareSystem softwareSystem; + String key = ""; + String description = ""; + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(SOFTWARE_SYSTEM_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String softwareSystemIdentifier = tokens.get(SOFTWARE_SYSTEM_IDENTIFIER_INDEX); + Element element = context.getElement(softwareSystemIdentifier); + if (element == null) { + throw new RuntimeException("The software system \"" + softwareSystemIdentifier + "\" does not exist"); + } + if (element instanceof SoftwareSystem) { + softwareSystem = (SoftwareSystem)element; + } else { + throw new RuntimeException("The element \"" + softwareSystemIdentifier + "\" is not a software system"); + } + + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, key, description); + view.setEnterpriseBoundaryVisible(true); + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewDslContext.java new file mode 100644 index 000000000..1ca6c6cff --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewDslContext.java @@ -0,0 +1,11 @@ +package com.structurizr.dsl; + +import com.structurizr.view.SystemLandscapeView; + +final class SystemLandscapeViewDslContext extends StaticViewDslContext { + + SystemLandscapeViewDslContext(SystemLandscapeView view) { + super(view); + } + +} diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java new file mode 100644 index 000000000..e03919f72 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java @@ -0,0 +1,43 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.view.SystemLandscapeView; + +final class SystemLandscapeViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "systemLandscape [key] [description] {"; + + private static final String VIEW_TYPE = "SystemLandscape"; + + private static final int KEY_INDEX = 1; + private static final int DESCRIPTION_INDEX = 2; + + SystemLandscapeView parse(DslContext context, Tokens tokens) { + // systemLandscape [key] [description] + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + Workspace workspace = context.getWorkspace(); + String key = ""; + String description = ""; + + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + } else { + key = workspace.getViews().generateViewKey(VIEW_TYPE); + } + validateViewKey(key); + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView(key, description); + view.setEnterpriseBoundaryVisible(true); + + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java new file mode 100644 index 000000000..27e69e8fb --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java @@ -0,0 +1,19 @@ +package com.structurizr.dsl; + +final class TerminologyDslContext extends DslContext { + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ENTERPRISE_TOKEN, + StructurizrDslTokens.PERSON_TOKEN, + StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, + StructurizrDslTokens.CONTAINER_TOKEN, + StructurizrDslTokens.COMPONENT_TOKEN, + StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, + StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, + StructurizrDslTokens.TERMINOLOGY_RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java new file mode 100644 index 000000000..b189f5d86 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java @@ -0,0 +1,79 @@ +package com.structurizr.dsl; + +final class TerminologyParser extends AbstractParser { + + private final static int TERM_INDEX = 1; + + void parseEnterprise(DslContext context, Tokens tokens) { + // enterprise + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: enterprise "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setEnterprise(tokens.get(TERM_INDEX)); + } + + void parsePerson(DslContext context, Tokens tokens) { + // person + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: person "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setPerson(tokens.get(TERM_INDEX)); + } + + void parseSoftwareSystem(DslContext context, Tokens tokens) { + // softwareSystem + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: softwareSystem "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setSoftwareSystem(tokens.get(TERM_INDEX)); + } + + void parseContainer(DslContext context, Tokens tokens) { + // container + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: container "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setContainer(tokens.get(TERM_INDEX)); + } + + void parseComponent(DslContext context, Tokens tokens) { + // component + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: component "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setComponent(tokens.get(TERM_INDEX)); + } + + void parseDeploymentNode(DslContext context, Tokens tokens) { + // deploymentNode + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: deploymentNode "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setDeploymentNode(tokens.get(TERM_INDEX)); + } + + void parseInfrastructureNode(DslContext context, Tokens tokens) { + // infrastructureNode + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: infrastructureNode "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setInfrastructureNode(tokens.get(TERM_INDEX)); + } + + void parseRelationship(DslContext context, Tokens tokens) { + // relationship + if (!tokens.includes(TERM_INDEX)) { + throw new RuntimeException("Expected: relationship "); + } + + context.getWorkspace().getViews().getConfiguration().getTerminology().setRelationship(tokens.get(TERM_INDEX)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java new file mode 100644 index 000000000..9cc5286af --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java @@ -0,0 +1,41 @@ +package com.structurizr.dsl; + +final class ThemeParser extends AbstractParser { + + private static final String DEFAULT_THEME_URL = "https://static.structurizr.com/themes/default/theme.json"; + + private final static int FIRST_THEME_INDEX = 1; + + void parseTheme(DslContext context, Tokens tokens) { + // theme + if (tokens.hasMoreThan(FIRST_THEME_INDEX)) { + throw new RuntimeException("Too many tokens, expected: theme "); + } + + if (!tokens.includes(FIRST_THEME_INDEX)) { + throw new RuntimeException("Expected: theme "); + } + + addTheme(context, tokens.get(FIRST_THEME_INDEX)); + } + + void parseThemes(DslContext context, Tokens tokens) { + // themes [url] ... [url] + if (!tokens.includes(FIRST_THEME_INDEX)) { + throw new RuntimeException("Expected: themes [url] ... [url]"); + } + + for (int i = FIRST_THEME_INDEX; i < tokens.size(); i++) { + addTheme(context, tokens.get(i)); + } + } + + private void addTheme(DslContext context, String url) { + if ("default".equalsIgnoreCase(url)) { + url = DEFAULT_THEME_URL; + } + + context.getWorkspace().getViews().getConfiguration().addTheme(url); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokenizer.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokenizer.java new file mode 100644 index 000000000..cd371def3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokenizer.java @@ -0,0 +1,58 @@ +package com.structurizr.dsl; + +import java.util.ArrayList; +import java.util.List; + +class Tokenizer { + + List tokenize(String line) { + List tokens = new ArrayList<>(); + line = line.trim(); + + boolean tokenStarted = false; + boolean quoted = false; + StringBuilder token = new StringBuilder(); + + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + + if (!tokenStarted) { + if (c == '"') { + quoted = true; + tokenStarted = true; + token = new StringBuilder(); + } else if (Character.isWhitespace(c)) { + // skip + } else { + quoted = false; + tokenStarted = true; + token = new StringBuilder(); + token.append(c); + } + } else { + if (c == '"' && line.charAt(i-1) == '\\') { + // escaped quote + token.append(c); + } else if (quoted && c == '"') { + // this is the end of the token + tokens.add(token.toString()); + tokenStarted = false; + quoted = false; + } else if (!quoted && Character.isWhitespace(c)) { + tokens.add(token.toString()); + tokenStarted = false; + quoted = false; + } else { + token.append(c); + } + } + } + + if (tokenStarted) { + tokens.add(token.toString()); + } + + return tokens; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java new file mode 100644 index 000000000..f00c1cfc7 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java @@ -0,0 +1,41 @@ +package com.structurizr.dsl; + +import java.util.List; + +final class Tokens { + + private List tokens; + + Tokens(List tokens) { + this.tokens = tokens; + } + + String get(int index) { + return tokens.get(index).trim().replaceAll("\\\\\"", "\"").trim().replaceAll("\\\\n", "\n"); + } + + int size() { + return tokens.size(); + } + + boolean contains(String token) { + return tokens.contains(token.trim()); + } + + Tokens withoutContextStartToken() { + if (tokens.get(tokens.size()-1).equals(DslContext.CONTEXT_START_TOKEN)) { + return new Tokens(tokens.subList(0, tokens.size()-1)); + } else { + return this; + } + } + + boolean includes(int index) { + return tokens.size() - 1 >= index; + } + + boolean hasMoreThan(int index) { + return includes(index + 1); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/UserRoleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/UserRoleParser.java new file mode 100644 index 000000000..e0db37ef8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/UserRoleParser.java @@ -0,0 +1,39 @@ +package com.structurizr.dsl; + +import com.structurizr.configuration.Role; + +final class UserRoleParser extends AbstractParser { + + private static final String GRAMMAR = " "; + + private final static int USERNAME_INDEX = 0; + private final static int ROLE_INDEX = 1; + + void parse(DslContext context, Tokens tokens) { + // + + if (tokens.hasMoreThan(ROLE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (tokens.size() != 2) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String username = tokens.get(USERNAME_INDEX); + String roleAsString = tokens.get(ROLE_INDEX); + + Role role; + + if (roleAsString.equalsIgnoreCase("write")) { + role = Role.ReadWrite; + } else if (roleAsString.equalsIgnoreCase("read")) { + role = Role.ReadOnly; + } else { + throw new RuntimeException("The role should be \"read\" or \"write\""); + } + + context.getWorkspace().getConfiguration().addUser(username, role); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/UsersDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/UsersDslContext.java new file mode 100644 index 000000000..e8b6a0170 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/UsersDslContext.java @@ -0,0 +1,10 @@ +package com.structurizr.dsl; + +final class UsersDslContext extends DslContext { + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewDslContext.java new file mode 100644 index 000000000..37a5660b8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewDslContext.java @@ -0,0 +1,17 @@ +package com.structurizr.dsl; + +import com.structurizr.view.View; + +abstract class ViewDslContext extends DslContext { + + private final View view; + + ViewDslContext(View view) { + this.view = view; + } + + View getView() { + return view; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewParser.java new file mode 100644 index 000000000..b632cbc87 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewParser.java @@ -0,0 +1,51 @@ +package com.structurizr.dsl; + +import com.structurizr.view.View; + +final class ViewParser extends AbstractParser { + + private static final String TITLE_GRAMMAR = "title "; + private static final String DESCRIPTION_GRAMMAR = "description <description>"; + + private static final int TITLE_INDEX = 1; + private static final int DESCRIPTION_INDEX = 1; + + void parseTitle(ViewDslContext context, Tokens tokens) { + // title <title> + + if (tokens.hasMoreThan(TITLE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + TITLE_GRAMMAR); + } + + View view = context.getView(); + if (view != null) { + if (tokens.size() == 2) { + String title = tokens.get(TITLE_INDEX); + + view.setTitle(title); + } else { + throw new RuntimeException("Expected: " + TITLE_GRAMMAR); + } + } + } + + void parseDescription(ViewDslContext context, Tokens tokens) { + // description <description> + + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + DESCRIPTION_GRAMMAR); + } + + View view = context.getView(); + if (view != null) { + if (tokens.size() == 2) { + String description = tokens.get(DESCRIPTION_INDEX); + + view.setDescription(description); + } else { + throw new RuntimeException("Expected: " + DESCRIPTION_GRAMMAR); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewsDslContext.java new file mode 100644 index 000000000..bfa78b285 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ViewsDslContext.java @@ -0,0 +1,25 @@ +package com.structurizr.dsl; + +final class ViewsDslContext extends DslContext { + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.SYSTEM_LANDSCAPE_VIEW_TOKEN, + StructurizrDslTokens.SYSTEM_CONTEXT_VIEW_TOKEN, + StructurizrDslTokens.CONTAINER_VIEW_TOKEN, + StructurizrDslTokens.COMPONENT_VIEW_TOKEN, + StructurizrDslTokens.FILTERED_VIEW_TOKEN, + StructurizrDslTokens.DYNAMIC_VIEW_TOKEN, + StructurizrDslTokens.DEPLOYMENT_VIEW_TOKEN, + StructurizrDslTokens.CUSTOM_VIEW_TOKEN, + StructurizrDslTokens.STYLES_TOKEN, + StructurizrDslTokens.THEME_TOKEN, + StructurizrDslTokens.THEMES_TOKEN, + StructurizrDslTokens.BRANDING_TOKEN, + StructurizrDslTokens.TERMINOLOGY_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java new file mode 100644 index 000000000..5a5ab7c4e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class WorkspaceDslContext extends DslContext { + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.NAME_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.DOCS_TOKEN, + StructurizrDslTokens.ADRS_TOKEN, + StructurizrDslTokens.IDENTIFIERS_TOKEN, + StructurizrDslTokens.IMPLIED_RELATIONSHIPS_TOKEN, + StructurizrDslTokens.MODEL_TOKEN, + StructurizrDslTokens.VIEWS_TOKEN, + StructurizrDslTokens.CONFIGURATION_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java new file mode 100644 index 000000000..6218a4a86 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java @@ -0,0 +1,138 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.WorkspaceUtils; + +import java.io.File; + +final class WorkspaceParser extends AbstractParser { + + private static final String GRAMMAR_STANDALONE = "workspace [name] [description]"; + private static final String GRAMMAR_EXTENDS = "workspace extends <file|url>"; + + private static final String STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME = "structurizr.dsl.identifier"; + + private static final int FIRST_INDEX = 1; + private static final int SECOND_INDEX = 2; + + Workspace parse(DslParserContext context, Tokens tokens) { + // workspace [name] [description] + // workspace extends <file|url> + + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + if (tokens.hasMoreThan(SECOND_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_STANDALONE + " or " + GRAMMAR_EXTENDS); + } + + if (tokens.includes(FIRST_INDEX)) { + String firstToken = tokens.get(FIRST_INDEX); + + if (StructurizrDslTokens.EXTENDS_TOKEN.equals(firstToken)) { + if (tokens.includes(SECOND_INDEX)) { + String source = tokens.get(SECOND_INDEX); + + try { + if (source.startsWith("https://")) { + if (source.endsWith(".json")) { + String json = readFromUrl(source); + workspace = WorkspaceUtils.fromJson(json); + registerIdentifiers(workspace, context); + } else { + String dsl = readFromUrl(source); + StructurizrDslParser structurizrDslParser = new StructurizrDslParser(); + structurizrDslParser.parse(context, dsl); + workspace = structurizrDslParser.getWorkspace(); + } + } else { + if (context.isRestricted()) { + throw new RuntimeException("Cannot import workspace from a file when running in restricted mode"); + } + + if (context.getFile() != null) { + File file = new File(context.getFile().getParent(), source); + if (!file.exists()) { + throw new RuntimeException(file.getCanonicalPath() + " could not be found"); + } + + if (file.isDirectory()) { + throw new RuntimeException(file.getCanonicalPath() + " should be a single file"); + } + + if (source.endsWith(".json")) { + workspace = WorkspaceUtils.loadWorkspaceFromJson(file); + registerIdentifiers(workspace, context); + } else { + StructurizrDslParser structurizrDslParser = new StructurizrDslParser(); + structurizrDslParser.parse(context, file); + workspace = structurizrDslParser.getWorkspace(); + } + } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } else { + throw new RuntimeException("Expected: " + GRAMMAR_EXTENDS); + } + } else { + workspace.setName(firstToken); + + if (tokens.includes(SECOND_INDEX)) { + workspace.setDescription(tokens.get(SECOND_INDEX)); + } + } + } + + return workspace; + } + + private void registerIdentifiers(Workspace workspace, DslParserContext context) { + for (Element element : workspace.getModel().getElements()) { + if (element.getProperties().containsKey(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME)) { + String identifier = element.getProperties().get(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME); + context.identifiersRegister.register(identifier, element); + } + } + + for (Relationship relationship : workspace.getModel().getRelationships()) { + if (relationship.getProperties().containsKey(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME)) { + String identifier = relationship.getProperties().get(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME); + context.identifiersRegister.register(identifier, relationship); + } + } + } + + void parseName(DslContext context, Tokens tokens) { + // name <name> + if (tokens.hasMoreThan(FIRST_INDEX)) { + throw new RuntimeException("Too many tokens, expected: name <name>"); + } + + if (!tokens.includes(FIRST_INDEX)) { + throw new RuntimeException("Expected: name <name>"); + } + + String name = tokens.get(FIRST_INDEX); + context.getWorkspace().setName(name); + } + + void parseDescription(DslContext context, Tokens tokens) { + // description <description> + if (tokens.hasMoreThan(FIRST_INDEX)) { + throw new RuntimeException("Too many tokens, expected: description <description>"); + } + + if (!tokens.includes(FIRST_INDEX)) { + throw new RuntimeException("Expected: description <description>"); + } + + String description = tokens.get(FIRST_INDEX); + context.getWorkspace().setDescription(description); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java new file mode 100644 index 000000000..7a81051ce --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java @@ -0,0 +1,506 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import com.structurizr.view.ContainerView; +import com.structurizr.view.DeploymentView; +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class AbstractExpressionParserTests extends AbstractTests { + + private StaticViewExpressionParser parser = new StaticViewExpressionParser(); + + @Test + void test_parseExpression_ThrowsAnException_WhenTheRelationshipSourceIsSpecifiedUsingLongSyntaxButDoesNotExist() { + try { + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + parser.parseExpression("relationship.source==a", context); + fail(); + } catch (Exception e) { + assertEquals("The element \"a\" does not exist", e.getMessage()); + } + } + + @Test + void test_parseExpression_ThrowsAnException_WhenTheRelationshipSourceIsSpecifiedUsingShortSyntaxButDoesNotExist() { + try { + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + parser.parseExpression("relationship==a->*", context); + fail(); + } catch (Exception e) { + assertEquals("The element \"a\" does not exist", e.getMessage()); + } + } + + @Test + void test_parseExpression_ThrowsAnException_WhenTheRelationshipDestinationIsSpecifiedUsingLongSyntaxButDoesNotExist() { + try { + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + parser.parseExpression("relationship.destination==a", context); + fail(); + } catch (Exception e) { + assertEquals("The element \"a\" does not exist", e.getMessage()); + } + } + + @Test + void test_parseExpression_ThrowsAnException_WhenTheRelationshipDestinationIsSpecifiedUsingShortSyntaxButDoesNotExist() { + try { + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + parser.parseExpression("relationship==*->a", context); + fail(); + } catch (Exception e) { + assertEquals("The element \"a\" does not exist", e.getMessage()); + } + } + + @Test + void test_parseExpression_ReturnsARelationship_WhenTheRelationshipSourceIsSpecifiedUsingLongSyntax() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", a); + elements.register("b", b); + elements.register("c", c); + context.setIdentifierRegister(elements); + + Set<ModelItem> relationships = parser.parseExpression("relationship.source==a", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + } + + @Test + void test_parseExpression_ReturnsARelationship_WhenTheRelationshipSourceIsSpecifiedUsingAnExpression() { + SoftwareSystem a = model.addSoftwareSystem("A"); + a.addTags("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + b.addTags("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + c.addTags("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + context.setIdentifierRegister(new IdentifiersRegister()); + + Set<ModelItem> relationships = parser.parseExpression("* -> element.tag==B", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + + relationships = parser.parseExpression("element.tag==A -> *", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + + relationships = parser.parseExpression("element.tag==A -> element.tag==B", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + } + + @Test + void test_parseExpression_ReturnsARelationship_WhenTheRelationshipSourceIsSpecifiedUsingShortSyntax() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", a); + elements.register("b", b); + elements.register("c", c); + context.setIdentifierRegister(elements); + + Set<ModelItem> relationships = parser.parseExpression("relationship==a->*", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + + relationships = parser.parseExpression("a -> *", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + } + + @Test + void test_parseExpression_ReturnsARelationship_WhenTheRelationshipDestinationIsSpecifiedUsingLongSyntax() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", a); + elements.register("b", b); + elements.register("c", c); + context.setIdentifierRegister(elements); + + Set<ModelItem> relationships = parser.parseExpression("relationship.destination==b", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + } + + @Test + void test_parseExpression_ReturnsARelationship_WhenTheRelationshipDestinationIsSpecifiedUsingShortSyntax() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", a); + elements.register("b", b); + elements.register("c", c); + context.setIdentifierRegister(elements); + + Set<ModelItem> relationships = parser.parseExpression("relationship==*->b", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + + relationships = parser.parseExpression("* -> b", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + } + + @Test + void test_parseExpression_ReturnsARelationship_WhenTheRelationshipSourceAndDestinationAreSpecifiedUsingLongSyntax() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", a); + elements.register("b", b); + elements.register("c", c); + context.setIdentifierRegister(elements); + + Set<ModelItem> relationships = parser.parseExpression("relationship.source==a && relationship.destination==b", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + } + + @Test + void test_parseExpression_ReturnsARelationship_WhenTheRelationshipSourceAndDestinationAreSpecifiedUsingShortSyntax() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", a); + elements.register("b", b); + elements.register("c", c); + context.setIdentifierRegister(elements); + + Set<ModelItem> relationships = parser.parseExpression("relationship==a->b", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(aToB)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingAnAfferentCouplingExpressionWithAnElementIdentifier() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("a", a); + map.register("b", b); + map.register("c", c); + context.setIdentifierRegister(map); + + Set<ModelItem> elements = parser.parseExpression("->b", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(a)); + assertTrue(elements.contains(b)); + + elements = parser.parseExpression("element==->b", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(a)); + assertTrue(elements.contains(b)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingAnEfferentCouplingExpressionWithAnElementIdentifier() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("a", a); + map.register("b", b); + map.register("c", c); + context.setIdentifierRegister(map); + + Set<ModelItem> elements = parser.parseExpression("b->", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(b)); + assertTrue(elements.contains(c)); + + elements = parser.parseExpression("element==b->", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(b)); + assertTrue(elements.contains(c)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingAnAfferentAndEfferentCouplingExpressionWithAnElementIdentifier() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("a", a); + map.register("b", b); + map.register("c", c); + context.setIdentifierRegister(map); + + Set<ModelItem> elements = parser.parseExpression("->b->", context); + assertEquals(3, elements.size()); + assertTrue(elements.contains(a)); + assertTrue(elements.contains(b)); + assertTrue(elements.contains(c)); + + elements = parser.parseExpression("element==->b->", context); + assertEquals(3, elements.size()); + assertTrue(elements.contains(a)); + assertTrue(elements.contains(b)); + assertTrue(elements.contains(c)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingAnAfferentAndEfferentCouplingExpressionWithAnElementExpression() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + b.addTags("Tag 1"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("a", a); + map.register("b", b); + map.register("c", c); + context.setIdentifierRegister(map); + + Set<ModelItem> elements = parser.parseExpression("->element.tag==Tag 1->", context); + assertEquals(3, elements.size()); + assertTrue(elements.contains(a)); + assertTrue(elements.contains(b)); + assertTrue(elements.contains(c)); + + elements = parser.parseExpression("element==->element.tag==Tag 1->", context); + assertEquals(3, elements.size()); + assertTrue(elements.contains(a)); + assertTrue(elements.contains(b)); + assertTrue(elements.contains(c)); + } + + @Test + void test_parseExpression_ReturnsAllRelationships_WhenUsingTheWildcardRelationshipExpression() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("a", a); + map.register("b", b); + map.register("c", c); + context.setIdentifierRegister(map); + + Set<ModelItem> relationships = parser.parseExpression("relationship==*->*", context); + assertEquals(2, relationships.size()); + assertTrue(relationships.contains(aToB)); + assertTrue(relationships.contains(bToC)); + + relationships = parser.parseExpression("* -> *", context); + assertEquals(2, relationships.size()); + assertTrue(relationships.contains(aToB)); + assertTrue(relationships.contains(bToC)); + + relationships = parser.parseExpression("relationship==*", context); + assertEquals(2, relationships.size()); + assertTrue(relationships.contains(aToB)); + assertTrue(relationships.contains(bToC)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingAnElementTagExpression() { + model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(new IdentifiersRegister()); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Software System", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(softwareSystem)); + } + + @Test + void test_parseExpression_ReturnsElementInstances_WhenUsingAnElementTagExpression() { + model.addPerson("User"); + SoftwareSystem ss = model.addSoftwareSystem("Software System"); + SoftwareSystemInstance ssi = model.addDeploymentNode("DN").add(ss); + + DeploymentView view = views.createDeploymentView("key", "Description"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(new IdentifiersRegister()); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Software System", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(ss)); // this is tagged "Software System" + assertTrue(elements.contains(ssi)); // this is not tagged "Software System", but the element it's based upon is + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingAnElementTypeExpression() { + model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(new IdentifiersRegister()); + + Set<ModelItem> elements = parser.parseExpression("element.type==SoftwareSystem", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(softwareSystem)); + } + + @Test + void test_parseExpression_ThrowsAnException_WhenUsingAnElementParentExpressionAndTheElementDoesNotExist() { + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + context.setIdentifierRegister(new IdentifiersRegister()); + + try { + parser.parseExpression("element.parent==a", context); + fail(); + } catch (Exception e) { + assertEquals("The parent element \"a\" does not exist", e.getMessage()); + } + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingAnElementParentExpression() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A"); + Container container1 = softwareSystemA.addContainer("Container 1"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B"); + Container container2 = softwareSystemB.addContainer("Container 2"); + + ContainerView view = views.createContainerView(softwareSystemA, "key", "Description"); + ContainerViewDslContext context = new ContainerViewDslContext(view); + context.setWorkspace(workspace); + IdentifiersRegister identifiersRegister = new IdentifiersRegister(); + identifiersRegister.register("b", softwareSystemB); + context.setIdentifierRegister(identifiersRegister); + + Set<ModelItem> elements = parser.parseExpression("element.parent==b", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(container2)); + } + + @Test + void test_parseExpression_ReturnsRelationships_WhenUsingARelationshipTagExpression() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship r = a.uses(b, "Uses"); + r.addTags("Tag 1"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(new IdentifiersRegister()); + + Set<ModelItem> relationships = parser.parseExpression("relationship.tag==Tag 1", context); + assertEquals(1, relationships.size()); + assertTrue(relationships.contains(r)); + } + + @Test + void test_parseExpression_ReturnsRelationships_WhenUsingARelationshipTagExpressionAndTheTagIsSetOnTheLinkedRelationship() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship r = a.uses(b, "Uses"); + r.addTags("Tag 1"); + + DeploymentNode dn = model.addDeploymentNode("DN"); + SoftwareSystemInstance ai = dn.add(a); + SoftwareSystemInstance bi = dn.add(b); + + DeploymentView view = views.createDeploymentView("key", "Description"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(new IdentifiersRegister()); + + Set<ModelItem> relationships = parser.parseExpression("relationship.tag==Tag 1", context); + assertEquals(2, relationships.size()); + assertTrue(relationships.contains(r)); + assertTrue(relationships.contains(ai.getRelationships().iterator().next())); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java new file mode 100644 index 000000000..3408890d4 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java @@ -0,0 +1,30 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy; +import com.structurizr.model.Model; +import com.structurizr.view.ViewSet; + +import java.util.ArrayList; +import java.util.Arrays; + +abstract class AbstractTests { + + protected Workspace workspace = new Workspace("Name", "Description"); + protected Model model = workspace.getModel(); + protected ViewSet views = workspace.getViews(); + + protected ModelDslContext context() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + ModelDslContext context = new ModelDslContext(); + context.setWorkspace(workspace); + + return context; + } + + protected Tokens tokens(String... tokens) { + return new Tokens(new ArrayList<>(Arrays.asList(tokens))); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AdrsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AdrsParserTests.java new file mode 100644 index 000000000..240a3a621 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AdrsParserTests.java @@ -0,0 +1,22 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class AdrsParserTests extends AbstractTests { + + private AdrsParser parser = new AdrsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new WorkspaceDslContext(), null, tokens("adrs", "path", "fqn", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !adrs <path> <fqn>", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AutoLayoutParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AutoLayoutParserTests.java new file mode 100644 index 000000000..ea6e1e68f --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AutoLayoutParserTests.java @@ -0,0 +1,111 @@ +package com.structurizr.dsl; + +import com.structurizr.view.AutomaticLayout; +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AutoLayoutParserTests extends AbstractTests { + + private AutoLayoutParser parser = new AutoLayoutParser(); + + @Test + void test_parse_EnablesAutoLayoutWithSomeDefaults() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(view.getAutomaticLayout()); + parser.parse(context, tokens("autoLayout")); + assertEquals(AutomaticLayout.Implementation.Graphviz, view.getAutomaticLayout().getImplementation()); + assertEquals(AutomaticLayout.RankDirection.TopBottom, view.getAutomaticLayout().getRankDirection()); + assertEquals(300, view.getAutomaticLayout().getRankSeparation()); + assertEquals(300, view.getAutomaticLayout().getNodeSeparation()); + } + + @Test + void test_parse_EnablesAutoLayoutWithAValidRankDirection() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(view.getAutomaticLayout()); + parser.parse(context, tokens("autoLayout", "lr")); + assertEquals(AutomaticLayout.Implementation.Graphviz, view.getAutomaticLayout().getImplementation()); + assertEquals(AutomaticLayout.RankDirection.LeftRight, view.getAutomaticLayout().getRankDirection()); + assertEquals(300, view.getAutomaticLayout().getRankSeparation()); + assertEquals(300, view.getAutomaticLayout().getNodeSeparation()); + } + + @Test + void test_parse_ThrowsAnException_WhenAnInvalidRankDirectionIsSpecified() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parse(context, tokens("autoLayout", "hello")); + fail(); + } catch (Exception e) { + assertEquals("Valid rank directions are: tb|bt|lr|rl", e.getMessage()); + } + } + + @Test + void test_parse_EnablesAutoLayoutWithAValidRankSeparation() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(view.getAutomaticLayout()); + parser.parse(context, tokens("autoLayout", "tb", "123")); + assertEquals(AutomaticLayout.Implementation.Graphviz, view.getAutomaticLayout().getImplementation()); + assertEquals(AutomaticLayout.RankDirection.TopBottom, view.getAutomaticLayout().getRankDirection()); + assertEquals(123, view.getAutomaticLayout().getRankSeparation()); + assertEquals(300, view.getAutomaticLayout().getNodeSeparation()); + } + + @Test + void test_parse_ThrowsAnException_WhenAnInvalidRankSeparationIsSpecified() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parse(context, tokens("autoLayout", "tb", "hello")); + fail(); + } catch (Exception e) { + assertEquals("Rank separation must be positive integer in pixels", e.getMessage()); + } + } + + @Test + void test_parse_EnablesAutoLayoutWithAValidNodeSeparation() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(view.getAutomaticLayout()); + parser.parse(context, tokens("autoLayout", "tb", "123", "456")); + assertEquals(AutomaticLayout.Implementation.Graphviz, view.getAutomaticLayout().getImplementation()); + assertEquals(AutomaticLayout.RankDirection.TopBottom, view.getAutomaticLayout().getRankDirection()); + assertEquals(123, view.getAutomaticLayout().getRankSeparation()); + assertEquals(456, view.getAutomaticLayout().getNodeSeparation()); + } + + @Test + void test_parse_ThrowsAnException_WhenAnInvalidNodeSeparationIsSpecified() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parse(context, tokens("autoLayout", "tb", "300", "hello")); + fail(); + } catch (Exception e) { + assertEquals("Node separation must be positive integer in pixels", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java new file mode 100644 index 000000000..dec579440 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java @@ -0,0 +1,140 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +class BrandingParserTests extends AbstractTests { + + private BrandingParser parser = new BrandingParser(); + + @Test + void test_parseLogo_ThrowsAnException_WhenThereAreTooManyTokens() { + BrandingDslContext context = new BrandingDslContext(null); + + try { + parser.parseLogo(context, tokens("logo", "path", "extra"), false); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: logo <path|url>", e.getMessage()); + } + } + + @Test + void test_parseLogo_ThrowsAnException_WhenNoPathIsSpecified() { + BrandingDslContext context = new BrandingDslContext(null); + + try { + parser.parseLogo(context, tokens("logo"), false); + fail(); + } catch (Exception e) { + assertEquals("Expected: logo <path|url>", e.getMessage()); + } + } + + @Test + void test_parseLogo_ThrowsAnException_WhenTheLogoDoesNotExist() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + + try { + parser.parseLogo(context, tokens("logo", "hello.png"), false); + fail(); + } catch (Exception e) { + assertEquals("hello.png does not exist", e.getMessage()); + } + } + + @Test + void test_parseLogo_ThrowsAnException_WhenTheFileIsNotSupported() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + + try { + parser.parseLogo(context, tokens("logo", "src/test/resources/dsl/getting-started.dsl"), false); + fail(); + } catch (Exception e) { + e.printStackTrace(); + assertTrue(e.getMessage().endsWith("is not a supported image file.")); + } + } + + @Test + void test_parseLogo_SetsTheLogo_WhenTheLogoDoesExist() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + context.setWorkspace(workspace); + + parser.parseLogo(context, tokens("logo", "src/test/resources/dsl/logo.png"), false); + assertTrue(workspace.getViews().getConfiguration().getBranding().getLogo().startsWith("data:image/png;base64,")); + } + + @Test + void test_parseLogo_SetsTheLogoFromADataUri() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + context.setWorkspace(workspace); + + parser.parseLogo(context, tokens("logo", "data:image/png;base64,123456789012345678901234567890"), true); + assertTrue(workspace.getViews().getConfiguration().getBranding().getLogo().startsWith("data:image/png;base64,123456789012345678901234567890")); + } + + @Test + void test_parseLogo_SetsTheLogoFromAHttpUrl() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + context.setWorkspace(workspace); + + parser.parseLogo(context, tokens("logo", "http://structurizr.com/logo.png"), true); + assertEquals("http://structurizr.com/logo.png", workspace.getViews().getConfiguration().getBranding().getLogo()); + } + + @Test + void test_parseLogo_SetsTheLogoFromAHttpsUrl() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + context.setWorkspace(workspace); + + parser.parseLogo(context, tokens("logo", "https://structurizr.com/logo.png"), true); + assertEquals("https://structurizr.com/logo.png", workspace.getViews().getConfiguration().getBranding().getLogo()); + } + + @Test + void test_parseFont_ThrowsAnException_WhenThereAreTooManyTokens() { + BrandingDslContext context = new BrandingDslContext(null); + + try { + parser.parseFont(context, tokens("font", "name", "url", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: font <name> [url]", e.getMessage()); + } + } + + @Test + void test_parseFont_ThrowsAnException_WhenNoNameIsSpecified() { + BrandingDslContext context = new BrandingDslContext(null); + + try { + parser.parseFont(context, tokens("font")); + fail(); + } catch (Exception e) { + assertEquals("Expected: font <name> [url]", e.getMessage()); + } + } + + @Test + void test_parseFont_SetsTheFontName() { + BrandingDslContext context = new BrandingDslContext(null); + context.setWorkspace(workspace); + + parser.parseFont(context, tokens("font", "Times New Roman")); + assertEquals("Times New Roman", workspace.getViews().getConfiguration().getBranding().getFont().getName()); + } + + @Test + void test_parseFont_SetsTheFontUrl() { + BrandingDslContext context = new BrandingDslContext(null); + context.setWorkspace(workspace); + + parser.parseFont(context, tokens("font", "Open Sans", "https://fonts.googleapis.com/css2?family=Open+Sans")); + assertEquals("https://fonts.googleapis.com/css2?family=Open+Sans", workspace.getViews().getConfiguration().getBranding().getFont().getUrl()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java new file mode 100644 index 000000000..77e4993f2 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java @@ -0,0 +1,127 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ComponentParserTests extends AbstractTests { + + private ComponentParser parser = new ComponentParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new ContainerDslContext(null), tokens("container", "name", "description", "technology", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: component <name> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { + try { + parser.parse(new ContainerDslContext(null), tokens("container")); + fail(); + } catch (Exception e) { + assertEquals("Expected: component <name> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAComponent() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parse(context, tokens("component", "Name")); + + assertEquals(3, model.getElements().size()); + Component component = container.getComponentWithName("Name"); + assertNotNull(component); + assertEquals("", component.getDescription()); + assertEquals(null, component.getTechnology()); + assertEquals("Element,Component", component.getTags()); + } + + @Test + void test_parse_CreatesAComponentWithADescription() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parse(context, tokens("component", "Name", "Description")); + + assertEquals(3, model.getElements().size()); + Component component = container.getComponentWithName("Name"); + assertNotNull(component); + assertEquals("Description", component.getDescription()); + assertEquals(null, component.getTechnology()); + assertEquals("Element,Component", component.getTags()); + } + + @Test + void test_parse_CreatesAComponentWithADescriptionAndTechnology() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parse(context, tokens("component", "Name", "Description", "Technology")); + + assertEquals(3, model.getElements().size()); + Component component = container.getComponentWithName("Name"); + assertNotNull(component); + assertEquals("Description", component.getDescription()); + assertEquals("Technology", component.getTechnology()); + assertEquals("Element,Component", component.getTags()); + } + + @Test + void test_parse_CreatesAComponentWithADescriptionAndTechnologyAndTags() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parse(context, tokens("component", "Name", "Description", "Technology", "Tag 1, Tag 2")); + + assertEquals(3, model.getElements().size()); + Component component = container.getComponentWithName("Name"); + assertNotNull(component); + assertEquals("Description", component.getDescription()); + assertEquals("Technology", component.getTechnology()); + assertEquals("Element,Component,Tag 1,Tag 2", component.getTags()); + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + Component component = model.addSoftwareSystem("Software System").addContainer("Container").addComponent("Component"); + ComponentDslContext context = new ComponentDslContext(component); + parser.parseTechnology(context, tokens("technology", "technology", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + Component component = model.addSoftwareSystem("Software System").addContainer("Container").addComponent("Component"); + ComponentDslContext context = new ComponentDslContext(component); + parser.parseTechnology(context, tokens("technology")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_SetsTheDescription_WhenADescriptionIsSpecified() { + Component component = model.addSoftwareSystem("Software System").addContainer("Container").addComponent("Component"); + ComponentDslContext context = new ComponentDslContext(component); + parser.parseTechnology(context, tokens("technology", "Technology")); + + assertEquals("Technology", component.getTechnology()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java new file mode 100644 index 000000000..a67d1d6d9 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java @@ -0,0 +1,111 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ComponentView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ComponentViewParserTests extends AbstractTests { + + private ComponentViewParser parser = new ComponentViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("component", "container", "key", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: component <container identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheContainerIdentifierIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("component")); + fail(); + } catch (Exception e) { + assertEquals("Expected: component <container identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheContainerIsNotDefined() { + DslContext context = context(); + try { + parser.parse(context, tokens("component", "container", "key")); + fail(); + } catch (Exception e) { + assertEquals("The container \"container\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotAContainer() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("component", "container", "key")); + fail(); + } catch (Exception e) { + assertEquals("The element \"container\" is not a container", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAComponentView() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", model.addSoftwareSystem("Name", "Description").addContainer("Container", "Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("component", "container")); + List<ComponentView> views = new ArrayList<>(context.getWorkspace().getViews().getComponentViews()); + + assertEquals(1, views.size()); + assertEquals("Component-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertTrue(views.get(0).getExternalContainerBoundariesVisible()); + } + + @Test + void test_parse_CreatesAComponentViewWithAKey() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", model.addSoftwareSystem("Name", "Description").addContainer("container", "Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("component", "container", "key")); + List<ComponentView> views = new ArrayList<>(context.getWorkspace().getViews().getComponentViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertTrue(views.get(0).getExternalContainerBoundariesVisible()); + } + + @Test + void test_parse_CreatesAComponentViewWithAKeyAndDescription() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", model.addSoftwareSystem("Name", "Description").addContainer("container", "Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("component", "container", "key", "Description")); + List<ComponentView> views = new ArrayList<>(context.getWorkspace().getViews().getComponentViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + assertTrue(views.get(0).getExternalContainerBoundariesVisible()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ConfigurationParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ConfigurationParserTests.java new file mode 100644 index 000000000..157c9bc61 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ConfigurationParserTests.java @@ -0,0 +1,86 @@ +package com.structurizr.dsl; + +import com.structurizr.configuration.Visibility; +import com.structurizr.configuration.WorkspaceScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ConfigurationParserTests extends AbstractTests { + + private final ConfigurationParser parser = new ConfigurationParser(); + + @Test + void test_parseScope_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseScope(context(), tokens("scope", "landscape", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: scope <landscape|softwaresystem|none>", e.getMessage()); + } + } + + @Test + void test_parseScope_ThrowsAnException_WhenTheScopeIsMissing() { + try { + parser.parseScope(context(), tokens("scope")); + fail(); + } catch (Exception e) { + assertEquals("Expected: scope <landscape|softwaresystem|none>", e.getMessage()); + } + } + + @Test + void test_parseScope_ThrowsAnException_WhenTheScopeIsNotValid() { + try { + parser.parseScope(context(), tokens("scope", "container")); + fail(); + } catch (Exception e) { + assertEquals("The scope \"container\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseScope_SetsTheScope() { + parser.parseScope(context(), tokens("scope", "softwaresystem")); + assertEquals(WorkspaceScope.SoftwareSystem, workspace.getConfiguration().getScope()); + } + + @Test + void test_parseVisibility_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseVisibility(context(), tokens("visibility", "public", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: visibility <private|public>", e.getMessage()); + } + } + + @Test + void test_parseVisibility_ThrowsAnException_WhenTheVisibilityIsMissing() { + try { + parser.parseVisibility(context(), tokens("visibility")); + fail(); + } catch (Exception e) { + assertEquals("Expected: visibility <private|public>", e.getMessage()); + } + } + + @Test + void test_parseVisibility_ThrowsAnException_WhenTheVisibilityIsNotValid() { + try { + parser.parseVisibility(context(), tokens("visibility", "shared")); + fail(); + } catch (Exception e) { + assertEquals("The visibility \"shared\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseVisibility_SetsTheVisibility() { + parser.parseVisibility(context(), tokens("visibility", "public")); + assertEquals(Visibility.Public, workspace.getConfiguration().getVisibility()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ConstantParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ConstantParserTests.java new file mode 100644 index 000000000..46d645e4f --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ConstantParserTests.java @@ -0,0 +1,59 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ConstantParserTests extends AbstractTests { + + private ConstantParser parser = new ConstantParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!constant", "name", "value", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !constant <name> <value>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoNameOrValueIsSpecified() { + try { + parser.parse(context(), tokens("!constant")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !constant <name> <value>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoValueIsSpecified() { + try { + parser.parse(context(), tokens("!constant", "name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !constant <name> <value>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNameContainsDisallowedCharacters() { + try { + parser.parse(context(), tokens("!constant", "${NAME}", "value")); + fail(); + } catch (Exception e) { + assertEquals("Constant names must only contain the following characters: a-zA-Z0-9-_.", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAConstant() { + Constant constant = parser.parse(context(), tokens("!constant", "name", "value")); + assertEquals("name", constant.getName()); + assertEquals("value", constant.getValue()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java new file mode 100644 index 000000000..073602d61 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java @@ -0,0 +1,147 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ContainerInstanceParserTests extends AbstractTests { + + private ContainerInstanceParser parser = new ContainerInstanceParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("containerInstance", "identifier", "deploymentGroups", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: containerInstance <identifier> [deploymentGroups] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierIsNotSpecified() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("containerInstance")); + fail(); + } catch (Exception e) { + assertEquals("Expected: containerInstance <identifier> [deploymentGroups] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementDoesNotExist() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("containerInstance", "container")); + fail(); + } catch (Exception e) { + assertEquals("The container \"container\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotAContainer() { + DeploymentNodeDslContext context = new DeploymentNodeDslContext(null); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("containerInstance", "container")); + fail(); + } catch (Exception e) { + assertEquals("The container \"container\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAContainerInstanceInTheDefaultDeploymentGroup() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("containerInstance", "container")); + + assertEquals(4, model.getElements().size()); + assertEquals(1, deploymentNode.getContainerInstances().size()); + ContainerInstance containerInstance = deploymentNode.getContainerInstances().iterator().next(); + assertSame(container, containerInstance.getContainer()); + assertEquals("Container Instance", containerInstance.getTags()); + assertEquals("Live", containerInstance.getEnvironment()); + assertEquals(1, containerInstance.getDeploymentGroups().size()); + assertEquals("Default", containerInstance.getDeploymentGroups().iterator().next()); + } + + @Test + void test_parse_CreatesAContainerInstanceInTheDefaultDeploymentGroupWithTags() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("containerInstance", "container", "", "Tag 1, Tag 2")); + + assertEquals(4, model.getElements().size()); + assertEquals(1, deploymentNode.getContainerInstances().size()); + ContainerInstance containerInstance = deploymentNode.getContainerInstances().iterator().next(); + assertSame(container, containerInstance.getContainer()); + assertEquals("Container Instance,Tag 1,Tag 2", containerInstance.getTags()); + assertEquals("Live", containerInstance.getEnvironment()); + assertEquals(1, containerInstance.getDeploymentGroups().size()); + assertEquals("Default", containerInstance.getDeploymentGroups().iterator().next()); + } + + @Test + void test_parse_CreatesAContainerInstanceInTheSpecifiedDeploymentGroup() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + elements.register("group", new DeploymentGroup("Group")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("containerInstance", "container", "group")); + + assertEquals(4, model.getElements().size()); + assertEquals(1, deploymentNode.getContainerInstances().size()); + ContainerInstance containerInstance = deploymentNode.getContainerInstances().iterator().next(); + assertSame(container, containerInstance.getContainer()); + assertEquals("Container Instance", containerInstance.getTags()); + assertEquals("Live", containerInstance.getEnvironment()); + assertEquals(1, containerInstance.getDeploymentGroups().size()); + assertEquals("Group", containerInstance.getDeploymentGroups().iterator().next()); + } + + @Test + void test_parse_CreatesAContainerInstanceInTheSpecifiedDeploymentGroupWithTags() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + elements.register("group", new DeploymentGroup("Group")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("containerInstance", "container", "group", "Tag 1, Tag 2")); + + assertEquals(4, model.getElements().size()); + assertEquals(1, deploymentNode.getContainerInstances().size()); + ContainerInstance containerInstance = deploymentNode.getContainerInstances().iterator().next(); + assertSame(container, containerInstance.getContainer()); + assertEquals("Container Instance,Tag 1,Tag 2", containerInstance.getTags()); + assertEquals("Live", containerInstance.getEnvironment()); + assertEquals(1, containerInstance.getDeploymentGroups().size()); + assertEquals("Group", containerInstance.getDeploymentGroups().iterator().next()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java new file mode 100644 index 000000000..79fc43a77 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java @@ -0,0 +1,122 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ContainerParserTests extends AbstractTests { + + private ContainerParser parser = new ContainerParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new SoftwareSystemDslContext(null), tokens("container", "name", "description", "technology", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: container <name> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { + try { + parser.parse(new SoftwareSystemDslContext(null), tokens("container")); + fail(); + } catch (Exception e) { + assertEquals("Expected: container <name> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAContainer() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parse(context, tokens("container", "Name")); + + assertEquals(2, model.getElements().size()); + Container container = softwareSystem.getContainerWithName("Name"); + assertNotNull(container); + assertEquals("", container.getDescription()); + assertEquals("", container.getTechnology()); + assertEquals("Element,Container", container.getTags()); + } + + @Test + void test_parse_CreatesAContainerWithADescription() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parse(context, tokens("container", "Name", "Description")); + + assertEquals(2, model.getElements().size()); + Container container = softwareSystem.getContainerWithName("Name"); + assertNotNull(container); + assertEquals("Description", container.getDescription()); + assertEquals("", container.getTechnology()); + assertEquals("Element,Container", container.getTags()); + } + + @Test + void test_parse_CreatesAContainerWithADescriptionAndTechnology() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parse(context, tokens("container", "Name", "Description", "Technology")); + + assertEquals(2, model.getElements().size()); + Container container = softwareSystem.getContainerWithName("Name"); + assertNotNull(container); + assertEquals("Description", container.getDescription()); + assertEquals("Technology", container.getTechnology()); + assertEquals("Element,Container", container.getTags()); + } + + @Test + void test_parse_CreatesAContainerWithADescriptionAndTechnologyAndTags() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parse(context, tokens("container", "Name", "Description", "Technology", "Tag 1, Tag 2")); + + assertEquals(2, model.getElements().size()); + Container container = softwareSystem.getContainerWithName("Name"); + assertNotNull(container); + assertEquals("Description", container.getDescription()); + assertEquals("Technology", container.getTechnology()); + assertEquals("Element,Container,Tag 1,Tag 2", container.getTags()); + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + Container container = model.addSoftwareSystem("Software System").addContainer("Container"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parseTechnology(context, tokens("technology", "technology", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + Container container = model.addSoftwareSystem("Software System").addContainer("Container"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parseTechnology(context, tokens("technology")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_SetsTheDescription_WhenADescriptionIsSpecified() { + Container container = model.addSoftwareSystem("Software System").addContainer("Container"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parseTechnology(context, tokens("technology", "Technology")); + + assertEquals("Technology", container.getTechnology()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java new file mode 100644 index 000000000..3044d5900 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java @@ -0,0 +1,111 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ContainerView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ContainerViewParserTests extends AbstractTests { + + private ContainerViewParser parser = new ContainerViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("container", "identifier", "key", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: container <software system identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheSoftwareSystemIdentifierIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("container")); + fail(); + } catch (Exception e) { + assertEquals("Expected: container <software system identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheSoftwareSystemIsNotDefined() { + DslContext context = context(); + try { + parser.parse(context, tokens("container", "softwareSystem", "key")); + fail(); + } catch (Exception e) { + assertEquals("The software system \"softwareSystem\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotASoftwareSystem() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("container", "softwareSystem", "key")); + fail(); + } catch (Exception e) { + assertEquals("The element \"softwareSystem\" is not a software system", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAContainerView() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addSoftwareSystem("Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("container", "softwareSystem")); + List<ContainerView> views = new ArrayList<>(context.getWorkspace().getViews().getContainerViews()); + + assertEquals(1, views.size()); + assertEquals("Container-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertTrue(views.get(0).getExternalSoftwareSystemBoundariesVisible()); + } + + @Test + void test_parse_CreatesAContainerViewWithAKey() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addSoftwareSystem("Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("container", "softwareSystem", "key")); + List<ContainerView> views = new ArrayList<>(context.getWorkspace().getViews().getContainerViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertTrue(views.get(0).getExternalSoftwareSystemBoundariesVisible()); + } + + @Test + void test_parse_CreatesAContainerViewWithAKeyAndDescription() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addSoftwareSystem("Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("container", "softwareSystem", "key", "Description")); + List<ContainerView> views = new ArrayList<>(context.getWorkspace().getViews().getContainerViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + assertTrue(views.get(0).getExternalSoftwareSystemBoundariesVisible()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CookbookTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CookbookTests.java new file mode 100644 index 000000000..f5c666ecf --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CookbookTests.java @@ -0,0 +1,33 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +class CookbookTests extends AbstractTests { + + @Test + void test() throws Exception { + File cookbookDirectory = new File("docs/cookbook"); + parseDslFiles(cookbookDirectory); + } + + private void parseDslFiles(File directory) throws Exception { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + parseDslFiles(file); + } else if (file.getName().startsWith("example-") && file.getName().endsWith(".dsl")) { + parseDslFile(file); + } + } + } + } + + private void parseDslFile(File file) throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(file); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java new file mode 100644 index 000000000..37c108b19 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java @@ -0,0 +1,79 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CustomElementParserTests extends AbstractTests { + + private CustomElementParser parser = new CustomElementParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("element", "name", "metadata", "description", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: element <name> [metadata] [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { + try { + parser.parse(context(), tokens("element")); + fail(); + } catch (Exception e) { + assertEquals("Expected: element <name> [metadata] [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesACustomElement() { + parser.parse(context(), tokens("element", "Name")); + + assertEquals(1, model.getElements().size()); + CustomElement element = model.getCustomElementWithName("Name"); + assertNotNull(element); + assertEquals("", element.getDescription()); + assertEquals("Element", element.getTags()); + } + + @Test + void test_parse_CreatesACustomElementWithMetadata() { + parser.parse(context(), tokens("element", "Name", "Box")); + + assertEquals(1, model.getElements().size()); + CustomElement element = model.getCustomElementWithName("Name"); + assertNotNull(element); + assertEquals("Box", element.getMetadata()); + assertEquals("", element.getDescription()); + assertEquals("Element", element.getTags()); + } + + @Test + void test_parse_CreatesACustomElementWithMetadataAndDescription() { + parser.parse(context(), tokens("element", "Name", "Box", "Description")); + + assertEquals(1, model.getElements().size()); + CustomElement element = model.getCustomElementWithName("Name"); + assertNotNull(element); + assertEquals("Box", element.getMetadata()); + assertEquals("Description", element.getDescription()); + assertEquals("Element", element.getTags()); + } + + @Test + void test_parse_CreatesACustomElementWithMetadataAndDescriptionAndTags() { + parser.parse(context(), tokens("element", "Name", "Box", "Description", "Tag 1, Tag 2")); + + assertEquals(1, model.getElements().size()); + CustomElement element = model.getCustomElementWithName("Name"); + assertNotNull(element); + assertEquals("Box", element.getMetadata()); + assertEquals("Description", element.getDescription()); + assertEquals("Element,Tag 1,Tag 2", element.getTags()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java new file mode 100644 index 000000000..b820ae888 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java @@ -0,0 +1,32 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class CustomViewAnimationStepParserTests extends AbstractTests { + + private CustomViewAnimationStepParser parser = new CustomViewAnimationStepParser(); + + @Test + void test_parseExplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((CustomViewDslContext)null, tokens("animationStep")); + fail(); + } catch (Exception e) { + assertEquals("Expected: animationStep <identifier> [identifier...]", e.getMessage()); + } + } + + @Test + void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((CustomViewAnimationDslContext) null, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier> [identifier...]", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewContentParserTests.java new file mode 100644 index 000000000..25e616b5d --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewContentParserTests.java @@ -0,0 +1,357 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import com.structurizr.view.CustomView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CustomViewContentParserTests extends AbstractTests { + + private CustomViewContentParser parser = new CustomViewContentParser(); + + @Test + void test_parseInclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { + try { + parser.parseInclude(new CustomViewDslContext(null), tokens("include")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: include <*|identifier> [*|identifier...]", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTheSpecifiedElementDoesNotExist() { + try { + parser.parseInclude(new CustomViewDslContext(null), tokens("include", "box")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element/relationship \"box\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTryingToAddAStaticStructureElementToACustomView() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("element", softwareSystem); + context.setIdentifierRegister(elements); + + try { + parser.parseInclude(context, tokens("include", "element")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element \"element\" can not be added to this type of view", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTryingToAddADeploymentElementToACustomView() { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("element", deploymentNode); + context.setIdentifierRegister(elements); + + try { + parser.parseInclude(context, tokens("include", "element")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element \"element\" can not be added to this type of view", iae.getMessage()); + } + } + + @Test + void test_parseInclude_AddsAllCustomElementsToA_WhenTheWildcardIsSpecified() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "*")); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + } + + @Test + void test_parseInclude_AddsTheSpecifiedPeopleAndSoftwareSystemsToASystemLandscapeView() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + CustomElement box3 = model.addCustomElement("Box 3"); + box1.uses(box2, ""); + box2.uses(box3, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + elements.register("box2", box2); + elements.register("box3", box3); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "box1", "box2")); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(box1))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(box2))); + assertEquals(1, view.getRelationships().size()); + } + + @Test + void test_parseInclude_IncludesTheSpecifiedRelationship_WhenARelationshipExpressionIsSpecified() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + Relationship relationship = box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + elements.register("box2", box2); + context.setIdentifierRegister(elements); + + view.add(box1); + view.add(box2); + view.remove(relationship); + assertEquals(2, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + + parser.parseInclude(context, tokens("include", "relationship.source==box1 && relationship.destination==box2")); + assertEquals(1, view.getRelationships().size()); + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parseExclude(context, tokens("include")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: exclude <identifier> [identifier...]", iae.getMessage()); + } + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheSpecifiedElementDoesNotExist() { + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parseExclude(context, tokens("exclude", "box")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element/relationship \"box\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parseExclude_RemovesTheSpecifiedElementsFromAView() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + view.add(box1); + view.add(box2); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + elements.register("box2", box2); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "box2")); + + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().stream().noneMatch(ev -> ev.getElement().equals(box2))); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExplicitIdentifierIsSpecified() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + Relationship relationship = box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + view.add(box1); + view.add(box2); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister identifersRegister = new IdentifiersRegister(); + identifersRegister.register("rel", relationship); + context.setIdentifierRegister(identifersRegister); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "rel")); + + assertEquals(2, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheRelationshipSourceElementDoesNotExistInTheModel() { + try { + CustomView view = views.createCustomView("key", "Title", "Description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "relationship.source==box1 && relationship.destination==box2")); + + fail(); + } catch (RuntimeException re) { + assertEquals("The element \"box1\" does not exist", re.getMessage()); + } + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheRelationshipDestinationElementDoesNotExistInTheModel() { + try { + CustomElement box1 = model.addCustomElement("Box 1"); + + CustomView view = views.createCustomView("key", "Title", "Description"); + view.add(box1); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship.source==box1 && relationship.source==box2")); + + fail(); + } catch (RuntimeException re) { + assertEquals("The element \"box2\" does not exist", re.getMessage()); + } + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithSourceAndDestination() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + view.add(box1); + view.add(box2); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + elements.register("box2", box2); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship.source==box1 && relationship.destination==box2")); + + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithSource() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + view.add(box1); + view.add(box2); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + elements.register("box2", box2); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship.source==box1")); + + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithADestination() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + view.add(box1); + view.add(box2); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + elements.register("box2", box2); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship.destination==box2")); + + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesAllRelationshipsFromAView_WhenAnExpressionIsSpecifiedWithAWildcard() { + CustomElement box1 = model.addCustomElement("Box 1"); + CustomElement box2 = model.addCustomElement("Box 2"); + box1.uses(box2, ""); + + CustomView view = views.createCustomView("key", "Title", "Description"); + view.add(box1); + view.add(box2); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("box1", box1); + elements.register("box2", box2); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship==*")); + + assertEquals(0, view.getRelationships().size()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewParserTests.java new file mode 100644 index 000000000..1b6c644ce --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewParserTests.java @@ -0,0 +1,75 @@ +package com.structurizr.dsl; + +import com.structurizr.view.CustomView; +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class CustomViewParserTests extends AbstractTests { + + private CustomViewParser parser = new CustomViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("custom", "key", "title", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: custom [key] [title] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_CreatesACustomView() { + DslContext context = context(); + parser.parse(context, tokens("custom")); + List<CustomView> views = new ArrayList<>(context.getWorkspace().getViews().getCustomViews()); + + assertEquals(1, views.size()); + assertEquals("Custom-001", views.get(0).getKey()); + assertEquals("", views.get(0).getTitle()); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesACustomViewWithAKey() { + DslContext context = context(); + parser.parse(context, tokens("custom", "key")); + List<CustomView> views = new ArrayList<>(context.getWorkspace().getViews().getCustomViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getTitle()); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesACustomViewWithAKeyAndTitle() { + DslContext context = context(); + parser.parse(context, tokens("custom", "key", "Title")); + List<CustomView> views = new ArrayList<>(context.getWorkspace().getViews().getCustomViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Title", views.get(0).getTitle()); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesACustomViewWithAKeyAndTitleAndDescription() { + DslContext context = context(); + parser.parse(context, tokens("custom", "key", "Title", "Description")); + List<CustomView> views = new ArrayList<>(context.getWorkspace().getViews().getCustomViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Title", views.get(0).getTitle()); + assertEquals("Description", views.get(0).getDescription()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DefaultViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DefaultViewParserTests.java new file mode 100644 index 000000000..df7eeeffa --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DefaultViewParserTests.java @@ -0,0 +1,24 @@ +package com.structurizr.dsl; + +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class DefaultViewParserTests extends AbstractTests { + + private final DefaultViewParser parser = new DefaultViewParser(); + + @Test + void test_parse_SetsTheDefaultView() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(context.getWorkspace().getViews().getConfiguration().getDefaultView()); + parser.parse(context); + assertEquals("key", context.getWorkspace().getViews().getConfiguration().getDefaultView()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentEnvironmentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentEnvironmentParserTests.java new file mode 100644 index 000000000..ebb1cb16c --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentEnvironmentParserTests.java @@ -0,0 +1,38 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class DeploymentEnvironmentParserTests extends AbstractTests { + + private DeploymentEnvironmentParser parser = new DeploymentEnvironmentParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(tokens("deploymentEnvironment", "name", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: deploymentEnvironment <name> {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsMissing() { + try { + parser.parse(tokens("deploymentEnvironment")); + fail(); + } catch (Exception e) { + assertEquals("Expected: deploymentEnvironment <name> {", e.getMessage()); + } + } + + @Test + void test_parse() { + String environment = parser.parse(tokens("deploymentEnvironment", "Live")); + assertEquals("Live", environment); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentGroupParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentGroupParserTests.java new file mode 100644 index 000000000..71ff1e2ed --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentGroupParserTests.java @@ -0,0 +1,38 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class DeploymentGroupParserTests extends AbstractTests { + + private DeploymentGroupParser parser = new DeploymentGroupParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(tokens("deploymentGroup", "name", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: deploymentGroup <name>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsMissing() { + try { + parser.parse(tokens("deploymentGroup")); + fail(); + } catch (Exception e) { + assertEquals("Expected: deploymentGroup <name>", e.getMessage()); + } + } + + @Test + void test_parse() { + String service1 = parser.parse(tokens("deploymentGroup", "Service 1")); + assertEquals("Service 1", service1); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java new file mode 100644 index 000000000..d9ec9be15 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java @@ -0,0 +1,222 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Component; +import com.structurizr.model.DeploymentNode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class DeploymentNodeParserTests extends AbstractTests { + + private DeploymentNodeParser parser = new DeploymentNodeParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new DeploymentEnvironmentDslContext("env"), tokens("deploymentNode", "name", "description", "technology", "tags", "instances", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: deploymentNode <name> [description] [technology] [tags] [instances] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { + try { + parser.parse(new DeploymentEnvironmentDslContext("env"), tokens("deploymentNode")); + fail(); + } catch (Exception e) { + assertEquals("Expected: deploymentNode <name> [description] [technology] [tags] [instances] {", e.getMessage()); + } + } + + @Test + void test_parse_CreatesADeploymentNode() { + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name")); + + assertEquals(1, model.getElements().size()); + DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); + assertNotNull(deploymentNode); + assertEquals("", deploymentNode.getDescription()); + assertEquals("", deploymentNode.getTechnology()); + assertEquals("Element,Deployment Node", deploymentNode.getTags()); + assertEquals("1", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + + @Test + void test_parse_CreatesADeploymentNodeWithADescription() { + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name", "Description")); + + assertEquals(1, model.getElements().size()); + DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); + assertNotNull(deploymentNode); + assertEquals("Description", deploymentNode.getDescription()); + assertEquals("", deploymentNode.getTechnology()); + assertEquals("Element,Deployment Node", deploymentNode.getTags()); + assertEquals("1", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + + @Test + void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnology() { + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology")); + + assertEquals(1, model.getElements().size()); + DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); + assertNotNull(deploymentNode); + assertEquals("Description", deploymentNode.getDescription()); + assertEquals("Technology", deploymentNode.getTechnology()); + assertEquals("Element,Deployment Node", deploymentNode.getTags()); + assertEquals("1", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + + @Test + void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnologyAndTags() { + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2")); + + assertEquals(1, model.getElements().size()); + DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); + assertNotNull(deploymentNode); + assertEquals("Description", deploymentNode.getDescription()); + assertEquals("Technology", deploymentNode.getTechnology()); + assertEquals("Element,Deployment Node,Tag 1,Tag 2", deploymentNode.getTags()); + assertEquals("1", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + + @Test + void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnologyAndTagsAndInstances() { + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "8")); + + assertEquals(1, model.getElements().size()); + DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); + assertNotNull(deploymentNode); + assertEquals("Description", deploymentNode.getDescription()); + assertEquals("Technology", deploymentNode.getTechnology()); + assertEquals("Element,Deployment Node,Tag 1,Tag 2", deploymentNode.getTags()); + assertEquals("8", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + + @Test + void test_parse_ThrowsAnException_WhenTheNumberOfInstancesIsNotValid() { + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); + context.setWorkspace(workspace); + + try { + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "abc")); + System.out.println(model.getDeploymentNodes().iterator().next().getInstances()); + fail(); + } catch (Exception e) { + assertEquals("Number of instances must be a positive integer or a range.", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAChildDeploymentNode() { + DeploymentNode parent = model.addDeploymentNode("Live", "Parent", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(parent); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name")); + + assertEquals(2, model.getElements().size()); + DeploymentNode deploymentNode = parent.getDeploymentNodeWithName("Name"); + assertNotNull(deploymentNode); + assertEquals("", deploymentNode.getDescription()); + assertEquals("", deploymentNode.getTechnology()); + assertEquals("Element,Deployment Node", deploymentNode.getTags()); + assertEquals("1", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + parser.parseTechnology(context, tokens("technology", "technology", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + parser.parseTechnology(context, tokens("technology")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_SetsTheTechnology_WhenADescriptionIsSpecified() { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + parser.parseTechnology(context, tokens("technology", "Technology")); + + assertEquals("Technology", deploymentNode.getTechnology()); + } + + @Test + void test_parseInstances_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + parser.parseInstances(context, tokens("instances", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: instances <number|range>", e.getMessage()); + } + } + + @Test + void test_parseInstances_ThrowsAnException_WhenNoNumberIsSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + parser.parseInstances(context, tokens("instances")); + fail(); + } catch (Exception e) { + assertEquals("Expected: instances <number|range>", e.getMessage()); + } + } + + @Test + void test_parseInstances_ThrowsAnException_WhenAnInvalidNumberIsSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + parser.parseInstances(context, tokens("instances", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Number of instances must be a positive integer or a range.", e.getMessage()); + } + } + + @Test + void test_parseInstances_SetsTheInstances_WhenANumberIsSpecified() { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + parser.parseInstances(context, tokens("instances", "123")); + + assertEquals("123", deploymentNode.getInstances()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java new file mode 100644 index 000000000..359931273 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java @@ -0,0 +1,32 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class DeploymentViewAnimationStepParserTests extends AbstractTests { + + private DeploymentViewAnimationStepParser parser = new DeploymentViewAnimationStepParser(); + + @Test + void test_parseExplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((DeploymentViewDslContext)null, tokens("animationStep")); + fail(); + } catch (Exception e) { + assertEquals("Expected: animationStep <identifier> [identifier...]", e.getMessage()); + } + } + + @Test + void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((DeploymentViewAnimationDslContext)null, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier> [identifier...]", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java new file mode 100644 index 000000000..729ce189e --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java @@ -0,0 +1,576 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import com.structurizr.view.DeploymentView; +import com.structurizr.view.RelationshipView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class DeploymentViewContentParserTests extends AbstractTests { + + private DeploymentViewContentParser parser = new DeploymentViewContentParser(); + + @Test + void test_parseInclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { + try { + parser.parseInclude(new DeploymentViewDslContext(null), tokens("include")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: include <*|identifier> [*|identifier...]", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTheSpecifiedElementDoesNotExist() { + try { + parser.parseInclude(new DeploymentViewDslContext(null), tokens("include", "user")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element/relationship \"user\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parseInclude_AddsAllDeploymentNodesAndChildrenInTheDeploymentEnvironment_WhenTheWildcardIsSpecifiedAndTheViewHasNoSoftwareSystemScope() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + Container c1 = ss1.addContainer("C1", "Description", "Technology"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Container c2 = ss2.addContainer("C2", "Description", "Technology"); + + DeploymentNode dev1 = model.addDeploymentNode("Dev", "Dev 1", "Description", "Technology"); + DeploymentNode dev2 = dev1.addDeploymentNode("Dev 2", "Description", "Technology"); + InfrastructureNode dev3 = dev2.addInfrastructureNode("Dev 3", "Description", "Technology"); + ContainerInstance dev4 = dev2.add(c1); + ContainerInstance dev5 = dev2.add(c2); + + DeploymentNode live1 = model.addDeploymentNode("Live", "Live 1", "Description", "Technology"); + DeploymentNode live2 = live1.addDeploymentNode("Live 2", "Description", "Technology"); + InfrastructureNode live3 = live2.addInfrastructureNode("Live 3", "Description", "Technology"); + ContainerInstance live4 = live2.add(c1); + ContainerInstance live5 = live2.add(c2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "*")); + + assertEquals(5, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live1))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live2))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live3))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live4))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live5))); + } + + @Test + void test_parseInclude_AddsAllDeploymentNodesAndChildrenInTheDeploymentEnvironment_WhenTheWildcardIsSpecifiedAndTheViewHasASoftwareSystemScope() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + Container c1 = ss1.addContainer("C1", "Description", "Technology"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Container c2 = ss2.addContainer("C2", "Description", "Technology"); + + DeploymentNode dev1 = model.addDeploymentNode("Dev", "Dev 1", "Description", "Technology"); + DeploymentNode dev2 = dev1.addDeploymentNode("Dev 2", "Description", "Technology"); + InfrastructureNode dev3 = dev2.addInfrastructureNode("Dev 3", "Description", "Technology"); + ContainerInstance dev4 = dev2.add(c1); + ContainerInstance dev5 = dev2.add(c2); + + DeploymentNode live1 = model.addDeploymentNode("Live", "Live 1", "Description", "Technology"); + DeploymentNode live2 = live1.addDeploymentNode("Live 2", "Description", "Technology"); + InfrastructureNode live3 = live2.addInfrastructureNode("Live 3", "Description", "Technology"); + ContainerInstance live4 = live2.add(c1); + ContainerInstance live5 = live2.add(c2); + + DeploymentView view = views.createDeploymentView(ss1, "key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "*")); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live1))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live2))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live3))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live4))); + } + + @Test + void test_parseInclude_AddsTheSpecifiedElements() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + Container c1 = ss1.addContainer("C1", "Description", "Technology"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Container c2 = ss2.addContainer("C2", "Description", "Technology"); + CustomElement box1 = model.addCustomElement("Box 1"); + + DeploymentNode dev1 = model.addDeploymentNode("Dev", "Dev 1", "Description", "Technology"); + DeploymentNode dev2 = dev1.addDeploymentNode("Dev 2", "Description", "Technology"); + InfrastructureNode dev3 = dev2.addInfrastructureNode("Dev 3", "Description", "Technology"); + ContainerInstance dev4 = dev2.add(c1); + ContainerInstance dev5 = dev2.add(c2); + + DeploymentNode live1 = model.addDeploymentNode("Live", "Live 1", "Description", "Technology"); + DeploymentNode live2 = live1.addDeploymentNode("Live 2", "Description", "Technology"); + InfrastructureNode live3 = live2.addInfrastructureNode("Live 3", "Description", "Technology"); + ContainerInstance live4 = live2.add(c1); + ContainerInstance live5 = live2.add(c2); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("element", live1); + elements.register("box1", box1); + + DeploymentView view = views.createDeploymentView(ss1, "key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "element", "box1")); + + assertEquals(5, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live1))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live2))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live3))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(live4))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(box1))); + } + + @Test + void test_parseInclude_AddsTheElement_WhenTheElementIsAnInfrastructureNode() { + DeploymentNode dn = model.addDeploymentNode("Live", "DN", "Description", "Technology"); + InfrastructureNode in = dn.addInfrastructureNode("IN", "Description", "Technology"); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("element", in); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "element")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(in)); + } + + @Test + void test_parseInclude_AddsTheElement_WhenTheElementIsASoftwareSystemInstance() { + DeploymentNode dn = model.addDeploymentNode("Live", "DN", "Description", "Technology"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + SoftwareSystemInstance softwareSystemInstance = dn.add(softwareSystem); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("element", softwareSystemInstance); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "element")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(softwareSystemInstance)); + } + + @Test + void test_parseInclude_AddsTheElement_WhenTheElementIsAContainerInstance() { + DeploymentNode dn = model.addDeploymentNode("Live", "DN", "Description", "Technology"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + ContainerInstance containerInstance = dn.add(container); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("element", containerInstance); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "element")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(containerInstance)); + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { + try { + parser.parseExclude(new DeploymentViewDslContext(null), tokens("exclude")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: exclude <identifier> [identifier...]", iae.getMessage()); + } + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheSpecifiedElementDoesNotExist() { + try { + parser.parseExclude(new DeploymentViewDslContext(null), tokens("exclude", "user")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element/relationship \"user\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parseExclude_RemovesTheSpecifiedElement() { + DeploymentNode dn = model.addDeploymentNode("Live", "DN", "Description", "Technology"); + InfrastructureNode in = dn.addInfrastructureNode("IN", "Description", "Technology"); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("element", in); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + view.addAllDeploymentNodes(); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(dn))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(in))); + + parser.parseExclude(context, tokens("exclude", "element")); + + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(dn))); + } + + @Test + void test_parseExclude_ExcludesReplicatedVersionsOfTheSpecifiedRelationship() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Relationship rel = ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + dn.add(ss1); + dn.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister identifersRegister = new IdentifiersRegister(); + identifersRegister.register("rel", rel); + context.setIdentifierRegister(identifersRegister); + + view.addDefaultElements(); + assertEquals(1, view.getRelationships().stream().map(RelationshipView::getRelationship).filter(r -> r.getLinkedRelationshipId().equals(rel.getId())).count()); + + parser.parseExclude(context, tokens("exclude", "rel")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheRelationshipSourceElementDoesNotExistInTheModel() { + try { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Relationship rel = ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + dn.add(ss1); + dn.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("ss2", ss2); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship.source==ss1 && relationship.destination==ss2")); + + fail(); + } catch (RuntimeException re) { + assertEquals("The element \"ss1\" does not exist", re.getMessage()); + } + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheRelationshipDestinationElementDoesNotExistInTheModel() { + try { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Relationship rel = ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + dn.add(ss1); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + view.add(dn); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("ss1", ss1); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship.source==ss1 && relationship.destination==ss2")); + + fail(); + } catch (RuntimeException re) { + assertEquals("The element \"ss2\" does not exist", re.getMessage()); + } + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithSourceAndDestination() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Relationship rel = ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + dn.add(ss1); + dn.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + view.add(dn); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("ss1", ss1); + elements.register("ss2", ss2); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship.source==ss1 && relationship.destination==ss2")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithSource() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Relationship rel = ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + dn.add(ss1); + dn.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + view.add(dn); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("ss1", ss1); + elements.register("ss2", ss2); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship.source==ss1")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithADestination() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Relationship rel = ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + dn.add(ss1); + dn.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + view.add(dn); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("ss1", ss1); + elements.register("ss2", ss2); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship.destination==ss2")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesAllRelationshipsFromAView_WhenAnExpressionIsSpecifiedWithAWildcard() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + Relationship rel = ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + dn.add(ss1); + dn.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + view.add(dn); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("ss1", ss1); + elements.register("ss2", ss2); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship==*")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseInclude_AddsAllDeploymentNodesWithTheSpecifiedTag() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + DeploymentNode dn1 = dn.addDeploymentNode("DN 1"); + dn1.addTags("Tag 1"); + SoftwareSystemInstance ss1Instance = dn1.add(ss1); + DeploymentNode dn2 = dn.addDeploymentNode("DN 2"); + SoftwareSystemInstance ss2Instance = dn2.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(3, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(dn1)); + assertNotNull(view.getElementView(ss1Instance)); + } + + @Test + void test_parseInclude_AddsAllInfrastructureNodesWithTheSpecifiedTag() { + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + InfrastructureNode in1 = dn.addInfrastructureNode("Infrastructure Node 1"); + in1.addTags("Tag 1"); + InfrastructureNode in2 = dn.addInfrastructureNode("Infrastructure Node 2"); + in2.addTags("Tag 2"); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(in1)); + } + + @Test + void test_parseInclude_AddsAllInstancesOfSoftwareSystemsWithTheSpecifiedTag() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + ss1.addTags("Tag 1"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + ss2.addTags("Tag 2"); + ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + SoftwareSystemInstance ss1Instance = dn.add(ss1); + SoftwareSystemInstance ss2Instance = dn.add(ss2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(ss1Instance)); + } + + @Test + void test_parseInclude_AddsAllSoftwareSystemInstancesWithTheSpecifiedTag() { + SoftwareSystem ss1 = model.addSoftwareSystem("SS1", "Description"); + SoftwareSystem ss2 = model.addSoftwareSystem("SS2", "Description"); + ss1.uses(ss2, "Uses"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + SoftwareSystemInstance ss1Instance = dn.add(ss1); + ss1Instance.addTags("Tag 1"); + SoftwareSystemInstance ss2Instance = dn.add(ss2); + ss2Instance.addTags("Tag 2"); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(ss1Instance)); + } + + @Test + void test_parseInclude_AddsAllInstancesOfContainersWithTheSpecifiedTag() { + SoftwareSystem ss = model.addSoftwareSystem("SS", "Description"); + Container c1 = ss.addContainer("Container 1"); + c1.addTags("Tag 1"); + Container c2 = ss.addContainer("Container 2"); + c2.addTags("Tag 2"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + ContainerInstance c1Instance = dn.add(c1); + ContainerInstance c2Instance = dn.add(c2); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(c1Instance)); + } + + @Test + void test_parseInclude_AddsAllContainerInstancesWithTheSpecifiedTag() { + SoftwareSystem ss = model.addSoftwareSystem("SS", "Description"); + Container c1 = ss.addContainer("Container 1"); + Container c2 = ss.addContainer("Container 2"); + + DeploymentNode dn = model.addDeploymentNode("Live", "Live", "Description", "Technology"); + ContainerInstance c1Instance = dn.add(c1); + c1Instance.addTags("Tag 1"); + ContainerInstance c2Instance = dn.add(c2); + c2Instance.addTags("Tag 2"); + + DeploymentView view = views.createDeploymentView("key", "Description"); + view.setEnvironment("Live"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(dn)); + assertNotNull(view.getElementView(c1Instance)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java new file mode 100644 index 000000000..bf4a417a0 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java @@ -0,0 +1,220 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class DeploymentViewExpressionParserTests extends AbstractTests { + + private DeploymentViewExpressionParser parser = new DeploymentViewExpressionParser(); + + @Test + void test_parseExpression_ThrowsAnException_WhenElementTypeIsNotSupported() { + try { + parser.parseExpression("element.type==Component", null); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element type of \"Component\" is not valid for this view", iae.getMessage()); + } + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsDeploymentNode() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==DeploymentNode", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(deploymentNode)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsInfrastructureNode() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==InfrastructureNode", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(infrastructureNode)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsSoftwareSystem() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==SoftwareSystem", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(softwareSystem)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsSoftwareSystemInstance() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==SoftwareSystemInstance", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(softwareSystemInstance)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsContainer() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==Container", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(container)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsContainerInstance() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==ContainerInstance", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(containerInstance)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementHasTag() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Infrastructure Node", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(infrastructureNode)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementDoesNotHaveTag() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag!=Infrastructure Node", context); + assertEquals(7, elements.size()); + assertFalse(elements.contains(infrastructureNode)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenBooleanAndUsed() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Element && element.type==Container", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(container)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenBooleanOrUsed() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode.add(container); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Software System Instance || element.type==ContainerInstance", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(softwareSystemInstance)); + assertTrue(elements.contains(containerInstance)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewParserTests.java new file mode 100644 index 000000000..4d3e1ccf8 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewParserTests.java @@ -0,0 +1,209 @@ +package com.structurizr.dsl; + +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.DeploymentView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class DeploymentViewParserTests extends AbstractTests { + + private DeploymentViewParser parser = new DeploymentViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("deployment", "identifier", "environment", "key", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: deployment <*|software system identifier> <environment> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIdentifierIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("deployment")); + fail(); + } catch (Exception e) { + assertEquals("Expected: deployment <*|software system identifier> <environment> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDeploymentEnvironmentIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("deployment", "*")); + fail(); + } catch (Exception e) { + assertEquals("Expected: deployment <*|software system identifier> <environment> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheEnvironmentDoesNotExist() { + DslContext context = context(); + + try { + parser.parse(context, tokens("deployment", "softwareSystem", "Live", "key")); + fail(); + } catch (Exception e) { + assertEquals("The environment \"Live\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotDefined() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + try { + parser.parse(context, tokens("deployment", "softwareSystem", "Live", "key")); + fail(); + } catch (Exception e) { + assertEquals("The software system \"softwareSystem\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotASoftwareSystem() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("deployment", "softwareSystem", "Live", "key")); + fail(); + } catch (Exception e) { + assertEquals("The element \"softwareSystem\" is not a software system", e.getMessage()); + } + } + + @Test + void test_parse_CreatesADeploymentViewWithNoScope() { + context().getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + parser.parse(context(), tokens("deployment", "*", "Live")); + List<DeploymentView> views = new ArrayList<>(this.views.getDeploymentViews()); + + assertEquals(1, views.size()); + assertEquals("Deployment-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertNull(views.get(0).getSoftwareSystem()); + } + + @Test + void test_parse_CreatesADeploymentViewWithNoScopeAndKey_ViaEnvironmentName() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + parser.parse(context, tokens("deployment", "*", "Live", "key")); + List<DeploymentView> views = new ArrayList<>(this.views.getDeploymentViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertNull(views.get(0).getSoftwareSystem()); + assertEquals("Live", views.get(0).getEnvironment()); + } + + @Test + void test_parse_CreatesADeploymentViewWithNoScopeAndKey_ViaEnvironmentReference() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("env", new DeploymentEnvironment("Live")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("deployment", "*", "env", "key")); + List<DeploymentView> views = new ArrayList<>(this.views.getDeploymentViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertNull(views.get(0).getSoftwareSystem()); + assertEquals("Live", views.get(0).getEnvironment()); + } + + @Test + void test_parse_CreatesADeploymentViewWithNoScopeAndKeyAndDescription() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + parser.parse(context, tokens("deployment", "*", "Live", "key", "Description")); + List<DeploymentView> views = new ArrayList<>(this.views.getDeploymentViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + assertNull(views.get(0).getSoftwareSystem()); + } + + @Test + void test_parse_CreatesADeploymentViewWithSoftwareSystemScope() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + IdentifiersRegister elements = new IdentifiersRegister(); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("deployment", "softwareSystem", "Live")); + List<DeploymentView> views = new ArrayList<>(this.views.getDeploymentViews()); + + assertEquals(1, views.size()); + assertEquals("Deployment-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertSame(softwareSystem, views.get(0).getSoftwareSystem()); + } + + @Test + void test_parse_CreatesADeploymentViewWithSoftwareSystemScopeAndKey() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + IdentifiersRegister elements = new IdentifiersRegister(); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("deployment", "softwareSystem", "Live", "key")); + List<DeploymentView> views = new ArrayList<>(this.views.getDeploymentViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertSame(softwareSystem, views.get(0).getSoftwareSystem()); + } + + @Test + void test_parse_CreatesADeploymentViewWithSoftwareSystemScopeAndKeyAndDescription() { + DslContext context = context(); + context.getWorkspace().getModel().addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + + IdentifiersRegister elements = new IdentifiersRegister(); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("deployment", "softwareSystem", "Live", "key", "Description")); + List<DeploymentView> views = new ArrayList<>(this.views.getDeploymentViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + assertSame(softwareSystem, views.get(0).getSoftwareSystem()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DocsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DocsParserTests.java new file mode 100644 index 000000000..50660421e --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DocsParserTests.java @@ -0,0 +1,22 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class DocsParserTests extends AbstractTests { + + private DocsParser parser = new DocsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new WorkspaceDslContext(), null, tokens("docs", "path", "fqn", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !docs <path> <fqn>", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java new file mode 100644 index 000000000..b8f2002b8 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -0,0 +1,1049 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.model.*; +import com.structurizr.view.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class DslTests extends AbstractTests { + + @Test + void test_test() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/test.dsl")); + + assertFalse(parser.getWorkspace().isEmpty()); + assertEquals("Organisation - Group", parser.getWorkspace().getModel().getEnterprise().getName()); + } + + @Test + void test_utf8() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/utf8.dsl")); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + + assertEquals(1, model.getPeople().size()); + Person user = model.getPersonWithName("你好 Usér \uD83D\uDE42"); + assertNotNull(user); + } + + @Test + void test_gettingstarted() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/getting-started.dsl")); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + ViewSet views = workspace.getViews(); + + assertEquals(1, model.getPeople().size()); + Person user = model.getPersonWithName("User"); + + assertEquals(1, workspace.getModel().getSoftwareSystems().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Software System"); + + assertEquals(1, workspace.getModel().getRelationships().size()); + Relationship relationship = user.getRelationships().iterator().next(); + assertEquals("Uses", relationship.getDescription()); + assertSame(softwareSystem, relationship.getDestination()); + + assertEquals(1, views.getViews().size()); + assertEquals(1, views.getSystemContextViews().size()); + SystemContextView view = views.getSystemContextViews().iterator().next(); + assertEquals("SystemContext-001", view.getKey()); + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + } + + @Test + void test_aws() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/amazon-web-services.dsl")); + + Workspace workspace = parser.getWorkspace(); + + assertEquals(13, workspace.getModel().getElements().size()); + assertEquals(0, workspace.getModel().getPeople().size()); + assertEquals(1, workspace.getModel().getSoftwareSystems().size()); + assertEquals(2, workspace.getModel().getSoftwareSystemWithName("Spring PetClinic").getContainers().size()); + assertEquals(1, workspace.getModel().getDeploymentNodes().size()); + assertEquals(6, workspace.getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).count()); + assertEquals(2, workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).count()); + assertEquals(2, workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).count()); + + assertEquals(4, workspace.getModel().getRelationships().size()); + + assertEquals(0, workspace.getViews().getSystemLandscapeViews().size()); + assertEquals(0, workspace.getViews().getSystemContextViews().size()); + assertEquals(0, workspace.getViews().getContainerViews().size()); + assertEquals(0, workspace.getViews().getComponentViews().size()); + assertEquals(0, workspace.getViews().getDynamicViews().size()); + assertEquals(1, workspace.getViews().getDeploymentViews().size()); + + DeploymentView deploymentView = workspace.getViews().getDeploymentViews().iterator().next(); + assertEquals(10, deploymentView.getElements().size()); + assertEquals(3, deploymentView.getRelationships().size()); + assertEquals(4, deploymentView.getAnimations().size()); + + assertEquals(3, workspace.getViews().getConfiguration().getStyles().getElements().size()); + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getRelationships().size()); + + assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); + } + + @Test + void test_awsLocal() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/amazon-web-services-local.dsl")); + + Workspace workspace = parser.getWorkspace(); + + assertEquals(13, workspace.getModel().getElements().size()); + assertEquals(0, workspace.getModel().getPeople().size()); + assertEquals(1, workspace.getModel().getSoftwareSystems().size()); + assertEquals(2, workspace.getModel().getSoftwareSystemWithName("Spring PetClinic").getContainers().size()); + assertEquals(1, workspace.getModel().getDeploymentNodes().size()); + assertEquals(6, workspace.getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).count()); + assertEquals(2, workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).count()); + assertEquals(2, workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).count()); + + assertEquals(4, workspace.getModel().getRelationships().size()); + + assertEquals(0, workspace.getViews().getSystemLandscapeViews().size()); + assertEquals(0, workspace.getViews().getSystemContextViews().size()); + assertEquals(0, workspace.getViews().getContainerViews().size()); + assertEquals(0, workspace.getViews().getComponentViews().size()); + assertEquals(0, workspace.getViews().getDynamicViews().size()); + assertEquals(1, workspace.getViews().getDeploymentViews().size()); + + DeploymentView deploymentView = workspace.getViews().getDeploymentViews().iterator().next(); + assertEquals(10, deploymentView.getElements().size()); + assertEquals(3, deploymentView.getRelationships().size()); + assertEquals(4, deploymentView.getAnimations().size()); + + assertEquals(3, workspace.getViews().getConfiguration().getStyles().getElements().size()); + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getRelationships().size()); + + assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); + } + + @Test + void test_bigbankplc() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/big-bank-plc.dsl")); + + Workspace workspace = parser.getWorkspace(); + + assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Customer Service Staff").getLocation()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); + + assertEquals(51, workspace.getModel().getElements().size()); + assertEquals(3, workspace.getModel().getPeople().size()); + assertEquals(4, workspace.getModel().getSoftwareSystems().size()); + assertEquals(5, workspace.getModel().getSoftwareSystemWithName("Internet Banking System").getContainers().size()); + assertEquals(6, workspace.getModel().getSoftwareSystemWithName("Internet Banking System").getContainerWithName("API Application").getComponents().size()); + assertEquals(5, workspace.getModel().getDeploymentNodes().size()); + assertEquals(21, workspace.getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).count()); + assertEquals(2, workspace.getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).count()); + assertEquals(10, workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).count()); + assertEquals(0, workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).count()); + + assertEquals(42, workspace.getModel().getRelationships().size()); + + assertEquals(1, workspace.getViews().getSystemLandscapeViews().size()); + assertEquals(1, workspace.getViews().getSystemContextViews().size()); + assertEquals(1, workspace.getViews().getContainerViews().size()); + assertEquals(1, workspace.getViews().getComponentViews().size()); + assertEquals(1, workspace.getViews().getDynamicViews().size()); + assertEquals(2, workspace.getViews().getDeploymentViews().size()); + + assertEquals(7, workspace.getViews().getSystemLandscapeViews().iterator().next().getElements().size()); + assertEquals(9, workspace.getViews().getSystemLandscapeViews().iterator().next().getRelationships().size()); + + assertEquals(4, workspace.getViews().getSystemContextViews().iterator().next().getElements().size()); + assertEquals(4, workspace.getViews().getSystemContextViews().iterator().next().getRelationships().size()); + + assertEquals(8, workspace.getViews().getContainerViews().iterator().next().getElements().size()); + assertEquals(10, workspace.getViews().getContainerViews().iterator().next().getRelationships().size()); + + assertEquals(11, workspace.getViews().getComponentViews().iterator().next().getElements().size()); + assertEquals(13, workspace.getViews().getComponentViews().iterator().next().getRelationships().size()); + + assertEquals(4, workspace.getViews().getDynamicViews().iterator().next().getElements().size()); + assertEquals(6, workspace.getViews().getDynamicViews().iterator().next().getRelationships().size()); + + assertEquals(13, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("DevelopmentDeployment")).findFirst().get().getElements().size()); + assertEquals(4, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("DevelopmentDeployment")).findFirst().get().getRelationships().size()); + + assertEquals(20, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("LiveDeployment")).findFirst().get().getElements().size()); + assertEquals(7, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("LiveDeployment")).findFirst().get().getRelationships().size()); + + assertEquals(11, workspace.getViews().getConfiguration().getStyles().getElements().size()); + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getRelationships().size()); + + assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); + + assertEquals(0, workspace.getDocumentation().getSections().size()); + assertEquals(0, workspace.getDocumentation().getDecisions().size()); + } + + @Test + void test_bigbankplc_systemlandscape() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/big-bank-plc/system-landscape.dsl")); + + Workspace workspace = parser.getWorkspace(); + + assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Customer Service Staff").getLocation()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); + + assertEquals(7, workspace.getModel().getElements().size()); + assertEquals(3, workspace.getModel().getPeople().size()); + assertEquals(4, workspace.getModel().getSoftwareSystems().size()); + + assertEquals(9, workspace.getModel().getRelationships().size()); + + assertEquals(1, workspace.getViews().getSystemLandscapeViews().size()); + assertEquals(0, workspace.getViews().getSystemContextViews().size()); + assertEquals(0, workspace.getViews().getContainerViews().size()); + assertEquals(0, workspace.getViews().getComponentViews().size()); + assertEquals(0, workspace.getViews().getDynamicViews().size()); + assertEquals(0, workspace.getViews().getDeploymentViews().size()); + + assertEquals(7, workspace.getViews().getSystemLandscapeViews().iterator().next().getElements().size()); + assertEquals(9, workspace.getViews().getSystemLandscapeViews().iterator().next().getRelationships().size()); + + assertEquals(4, workspace.getViews().getConfiguration().getStyles().getElements().size()); + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getRelationships().size()); + + assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); + } + + @Test + void test_bigbankplc_internetbankingsystem() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl")); + + Workspace workspace = parser.getWorkspace(); + + assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Customer Service Staff").getLocation()); + assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); + + assertEquals(51, workspace.getModel().getElements().size()); + assertEquals(3, workspace.getModel().getPeople().size()); + assertEquals(4, workspace.getModel().getSoftwareSystems().size()); + assertEquals(5, workspace.getModel().getSoftwareSystemWithName("Internet Banking System").getContainers().size()); + assertEquals(6, workspace.getModel().getSoftwareSystemWithName("Internet Banking System").getContainerWithName("API Application").getComponents().size()); + assertEquals(5, workspace.getModel().getDeploymentNodes().size()); + assertEquals(21, workspace.getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).count()); + assertEquals(2, workspace.getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).count()); + assertEquals(10, workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).count()); + assertEquals(0, workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).count()); + + assertEquals(42, workspace.getModel().getRelationships().size()); + + assertEquals(0, workspace.getViews().getSystemLandscapeViews().size()); + assertEquals(1, workspace.getViews().getSystemContextViews().size()); + assertEquals(1, workspace.getViews().getContainerViews().size()); + assertEquals(1, workspace.getViews().getComponentViews().size()); + assertEquals(1, workspace.getViews().getDynamicViews().size()); + assertEquals(2, workspace.getViews().getDeploymentViews().size()); + + assertEquals(4, workspace.getViews().getSystemContextViews().iterator().next().getElements().size()); + assertEquals(4, workspace.getViews().getSystemContextViews().iterator().next().getRelationships().size()); + + assertEquals(8, workspace.getViews().getContainerViews().iterator().next().getElements().size()); + assertEquals(10, workspace.getViews().getContainerViews().iterator().next().getRelationships().size()); + + assertEquals(11, workspace.getViews().getComponentViews().iterator().next().getElements().size()); + assertEquals(13, workspace.getViews().getComponentViews().iterator().next().getRelationships().size()); + + assertEquals(4, workspace.getViews().getDynamicViews().iterator().next().getElements().size()); + assertEquals(6, workspace.getViews().getDynamicViews().iterator().next().getRelationships().size()); + + assertEquals(13, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("DevelopmentDeployment")).findFirst().get().getElements().size()); + assertEquals(4, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("DevelopmentDeployment")).findFirst().get().getRelationships().size()); + + assertEquals(20, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("LiveDeployment")).findFirst().get().getElements().size()); + assertEquals(7, workspace.getViews().getDeploymentViews().stream().filter(v -> v.getKey().equals("LiveDeployment")).findFirst().get().getRelationships().size()); + + assertEquals(11, workspace.getViews().getConfiguration().getStyles().getElements().size()); + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getRelationships().size()); + + assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); + + assertEquals(4, workspace.getModel().getSoftwareSystemWithName("Internet Banking System").getDocumentation().getSections().size()); + assertEquals(1, workspace.getModel().getSoftwareSystemWithName("Internet Banking System").getDocumentation().getDecisions().size()); + } + + @Test + void test_frs() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/financial-risk-system.dsl")); + + + Workspace workspace = parser.getWorkspace(); + + assertEquals(9, workspace.getModel().getElements().size()); + assertEquals(2, workspace.getModel().getPeople().size()); + assertEquals(7, workspace.getModel().getSoftwareSystems().size()); + assertEquals(0, workspace.getModel().getDeploymentNodes().size()); + assertEquals(0, workspace.getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).count()); + assertEquals(0, workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).count()); + assertEquals(0, workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).count()); + + assertEquals(9, workspace.getModel().getRelationships().size()); + + assertEquals(0, workspace.getViews().getSystemLandscapeViews().size()); + assertEquals(1, workspace.getViews().getSystemContextViews().size()); + assertEquals(0, workspace.getViews().getContainerViews().size()); + assertEquals(0, workspace.getViews().getComponentViews().size()); + assertEquals(0, workspace.getViews().getDynamicViews().size()); + assertEquals(0, workspace.getViews().getDeploymentViews().size()); + + assertEquals(9, workspace.getViews().getSystemContextViews().iterator().next().getElements().size()); + assertEquals(9, workspace.getViews().getSystemContextViews().iterator().next().getRelationships().size()); + + assertEquals(5, workspace.getViews().getConfiguration().getStyles().getElements().size()); + assertEquals(4, workspace.getViews().getConfiguration().getStyles().getRelationships().size()); + + assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); + } + + @Test + void test_includeLocalFile() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/include-file.dsl")); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + + assertEquals(1, model.getSoftwareSystems().size()); + assertNotNull(model.getSoftwareSystemWithName("Software System")); + + assertEquals("workspace {\n" + + "\n" + + " model {\n" + + " softwareSystem = softwareSystem \"Software System\" {\n" + + " !docs docs\n" + + " }\n" + + " }\n" + + "\n" + + "}\n", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + } + + @Test + void test_includeLocalDirectory() throws Exception { + File hiddenFile = new File("src/test/resources/dsl/include/model/software-system/.DS_Store"); + if (hiddenFile.exists()) { + hiddenFile.delete(); + } + + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/include-directory.dsl")); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + + assertEquals(3, workspace.getModel().getSoftwareSystems().size()); + SoftwareSystem softwareSystem1 = model.getSoftwareSystemWithName("Software System 1"); + assertNotNull(softwareSystem1); + assertEquals(1, softwareSystem1.getDocumentation().getSections().size()); + + SoftwareSystem softwareSystem2 = model.getSoftwareSystemWithName("Software System 2"); + assertNotNull(softwareSystem2); + assertEquals(1, softwareSystem2.getDocumentation().getSections().size()); + + SoftwareSystem softwareSystem3 = model.getSoftwareSystemWithName("Software System 3"); + assertNotNull(softwareSystem3); + assertEquals(1, softwareSystem3.getDocumentation().getSections().size()); + + assertEquals("workspace {\n" + + "\n" + + " model {\n" + + " !constant SOFTWARE_SYSTEM_NAME \"Software System 1\"\n" + + " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + + " !docs ../../docs\n" + + " }\n" + + "\n" + + " !constant SOFTWARE_SYSTEM_NAME \"Software System 2\"\n" + + " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + + " !docs ../../docs\n" + + " }\n" + + "\n" + + " !constant SOFTWARE_SYSTEM_NAME \"Software System 3\"\n" + + " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + + " !docs ../../docs\n" + + " }\n" + + " }\n" + + "\n" + + "}\n", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + } + + @Test + void test_includeLocalDirectory_WhenThereAreHiddenFiles() throws Exception { + File hiddenFile = new File("src/test/resources/dsl/include/model/software-system/.DS_Store"); + if (hiddenFile.exists()) { + hiddenFile.delete(); + } + Files.writeString(hiddenFile.toPath(), "hello world"); + + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/include-directory.dsl")); + } + + @Test + void test_includeUrl() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/include-url.dsl")); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + ViewSet views = workspace.getViews(); + + assertEquals(1, workspace.getModel().getSoftwareSystems().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Software System"); + + assertEquals("workspace {\n" + + "\n" + + " model {\n" + + " softwareSystem = softwareSystem \"Software System\" {\n" + + " !docs docs\n" + + " }\n" + + " }\n" + + "\n" + + "}\n", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + } + + @Test + void test_include_WhenRunningInRestrictedMode() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + + // the model include will be ignored, so no software systems + parser.parse(new File("src/test/resources/dsl/include-file.dsl")); + assertEquals(0, model.getSoftwareSystems().size()); + } + + @ParameterizedTest + @ValueSource(strings = { "src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl", "src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl" }) + void test_extendWorkspaceFromJson(String dslFile) throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File(dslFile)); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + + assertEquals(1, model.getPeople().size()); + Person user = model.getPersonWithName("User"); + + assertEquals(3, workspace.getModel().getSoftwareSystems().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Software System 1"); + assertTrue(user.hasEfferentRelationshipWith(softwareSystem, "Uses")); + + assertEquals(2, softwareSystem.getContainers().size()); + assertNotNull(softwareSystem.getContainers().stream().filter(c -> c.getName().equals("Web Application 1")).findFirst()); + assertNotNull(softwareSystem.getContainers().stream().filter(c -> c.getName().equals("Web Application 2")).findFirst()); + } + + @Test + void test_extendWorkspaceFromJsonFile_WhenRunningInRestrictedMode() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + + File dslFile = new File("src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl"); + + try { + // this will fail, because the model import will be ignored + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Cannot import workspace from a file when running in restricted mode at line 1 of " + dslFile.getAbsolutePath() + ": workspace extends workspace.json {", e.getMessage()); + } + } + + @ParameterizedTest + @ValueSource(strings = { "src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl", "src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl" }) + void test_extendWorkspaceFromDsl(String dslFile) throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File(dslFile)); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + + assertEquals(1, model.getPeople().size()); + Person user = model.getPersonWithName("User"); + + assertEquals(3, workspace.getModel().getSoftwareSystems().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Software System 1"); + assertTrue(user.hasEfferentRelationshipWith(softwareSystem, "Uses")); + + assertEquals(1, softwareSystem.getContainers().size()); + assertEquals("Web Application", softwareSystem.getContainers().iterator().next().getName()); + } + + @Test + void test_extendWorkspaceFromDslFile_WhenRunningInRestrictedMode() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + + File dslFile = new File("src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl"); + try { + // this will fail, because the model import will be ignored + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Cannot import workspace from a file when running in restricted mode at line 1 of " + dslFile.getAbsolutePath() +": workspace extends workspace.dsl {", e.getMessage()); + } + } + + @Test + void test_extendWorkspaceFromDslFiles() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/extend/4.dsl")); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + ViewSet views = workspace.getViews(); + + assertEquals(3, model.getPeople().size()); + assertEquals(1, views.getViews().size()); + } + + @Test + void test_ref() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/ref.dsl")); + + assertNotNull(parser.getWorkspace().getModel().getElementWithCanonicalName("InfrastructureNode://Live/Amazon Web Services/New deployment node/New infrastructure node")); + assertNotNull(parser.getWorkspace().getModel().getElementWithCanonicalName("InfrastructureNode://Live/Amazon Web Services/US-East-1/New deployment node 1/New infrastructure node 1")); + assertNotNull(parser.getWorkspace().getModel().getElementWithCanonicalName("InfrastructureNode://Live/Amazon Web Services/US-East-1/New deployment node 2/New infrastructure node 2")); + } + + @Test + void test_parallel1() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/parallel1.dsl")); + + assertFalse(parser.getWorkspace().isEmpty()); + DynamicView view = parser.getWorkspace().getViews().getDynamicViews().iterator().next(); + List<RelationshipView> relationships = new ArrayList<>(view.getRelationships()); + assertEquals(4, relationships.size()); + assertEquals("1", relationships.get(0).getOrder()); + assertEquals("2", relationships.get(1).getOrder()); + assertEquals("3", relationships.get(2).getOrder()); + assertEquals("3", relationships.get(3).getOrder()); + } + + @Test + void test_parallel2() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/parallel2.dsl")); + + assertFalse(parser.getWorkspace().isEmpty()); + DynamicView view = parser.getWorkspace().getViews().getDynamicViews().iterator().next(); + List<RelationshipView> relationships = new ArrayList<>(view.getRelationships()); + assertEquals(4, relationships.size()); + assertEquals("1", relationships.get(0).getOrder()); + assertEquals("2", relationships.get(1).getOrder()); + assertEquals("2", relationships.get(2).getOrder()); + assertEquals("3", relationships.get(3).getOrder()); + } + + @Test + void test_groups() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/groups.dsl")); + + ContainerView containerView = parser.getWorkspace().getViews().getContainerViews().iterator().next(); + assertEquals(4, containerView.getElements().size()); + + DeploymentView deploymentView = parser.getWorkspace().getViews().getDeploymentViews().iterator().next(); + assertEquals(6, deploymentView.getElements().size()); + } + + @Test + void test_nested_groups() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/groups-nested.dsl")); + + SoftwareSystem a = parser.getWorkspace().getModel().getSoftwareSystemWithName("A"); + assertEquals("Organisation/Department A", a.getGroup()); + + Container aApi = a.getContainerWithName("A API"); + assertEquals("Capability 1/Service A", aApi.getGroup()); + + Container aDatabase = a.getContainerWithName("A Database"); + assertEquals("Capability 1/Service A", aDatabase.getGroup()); + + Container bApi = a.getContainerWithName("B API"); + assertEquals("Capability 1/Service B", bApi.getGroup()); + + Container bDatabase = a.getContainerWithName("B Database"); + assertEquals("Capability 1/Service B", bDatabase.getGroup()); + + SoftwareSystem b = parser.getWorkspace().getModel().getSoftwareSystemWithName("B"); + assertEquals("Organisation/Department B", b.getGroup()); + + SoftwareSystem c = parser.getWorkspace().getModel().getSoftwareSystemWithName("C"); + assertEquals("Organisation", c.getGroup()); + + SoftwareSystem d = parser.getWorkspace().getModel().getSoftwareSystemWithName("D"); + assertEquals("Department A/Team 1", d.getGroup()); + } + + @Test + void test_hierarchicalIdentifiers() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/hierarchical-identifiers.dsl")); + + Workspace workspace = parser.getWorkspace(); + assertEquals(0, workspace.getModel().getSoftwareSystemWithName("B").getRelationships().size()); + } + + @Test + void test_hierarchicalIdentifiersWhenUnassigned() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/hierarchical-identifiers-when-unassigned.dsl")); + + Workspace workspace = parser.getWorkspace(); + IdentifiersRegister identifiersRegister = parser.getIdentifiersRegister(); + + assertEquals(6, identifiersRegister.getElementIdentifiers().size()); + for (String identifier : identifiersRegister.getElementIdentifiers()) { + assertFalse(identifier.startsWith("null")); + } + } + + @Test + void test_hierarchicalIdentifiersAndDeploymentNodes() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-1.dsl")); + } + + @Test + void test_hierarchicalIdentifiersAndDeploymentNodes_WhenSoftwareSystemNameClashes() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-2.dsl")); + } + + @Test + void test_hierarchicalIdentifiersAndDeploymentNodes_WhenSoftwareContainerClashes() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-3.dsl")); + } + + @Test + void test_pluginWithoutParameters() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/plugin-without-parameters.dsl")); + + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Java")); + } + + @Test + void test_pluginWithParameters() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/plugin-with-parameters.dsl")); + + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Java")); + } + + @Test + void test_script() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/script-external.dsl")); + + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Groovy")); + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Kotlin")); + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Ruby")); + } + + @Test + void test_scriptWithParameters() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/script-external-with-parameters.dsl")); + + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Groovy")); + } + + @Test + void test_inlineScript() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/script-inline.dsl")); + + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Groovy")); + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Kotlin")); + assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Ruby")); + + assertTrue(parser.getWorkspace().getModel().getPersonWithName("User").hasTag("Groovy")); + assertTrue(parser.getWorkspace().getModel().getPersonWithName("User").getRelationships().iterator().next().hasTag("Groovy")); + assertEquals("Groovy", parser.getWorkspace().getViews().getSystemLandscapeViews().iterator().next().getDescription()); + } + + @Test + void test_docs() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/docs/workspace.dsl")); + + SoftwareSystem softwareSystem = parser.getWorkspace().getModel().getSoftwareSystemWithName("Software System"); + Container container = softwareSystem.getContainerWithName("Container"); + Component component = container.getComponentWithName("Component"); + + assertEquals(1, parser.getWorkspace().getDocumentation().getSections().size()); + assertEquals(1, softwareSystem.getDocumentation().getSections().size()); + assertEquals(1, container.getDocumentation().getSections().size()); + assertEquals(1, component.getDocumentation().getSections().size()); + } + + @Test + void test_adrs() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/adrs/workspace.dsl")); + + SoftwareSystem softwareSystem = parser.getWorkspace().getModel().getSoftwareSystemWithName("Software System"); + Container container = softwareSystem.getContainerWithName("Container"); + Component component = container.getComponentWithName("Component"); + + assertEquals(10, parser.getWorkspace().getDocumentation().getDecisions().size()); + assertEquals(10, softwareSystem.getDocumentation().getDecisions().size()); + assertEquals(10, container.getDocumentation().getDecisions().size()); + assertEquals(10, component.getDocumentation().getDecisions().size()); + } + + @Test + void test_this() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/this.dsl")); + } + + @Test + void test_workspaceWithControlCharacters() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/workspace-with-bom.dsl")); + } + + @Test + void test_excludeRelationships() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/exclude-relationships.dsl")); + } + + @Test + void test_unexpectedTokensBeforeWorkspace() { + File dslFile = new File("src/test/resources/dsl/unexpected-tokens-before-workspace.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Unexpected tokens (expected: workspace) at line 1 of " + dslFile.getAbsolutePath() + ": hello world", e.getMessage()); + } + } + + @Test + void test_unexpectedTokensAfterWorkspace() { + File dslFile = new File("src/test/resources/dsl/unexpected-tokens-after-workspace.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Unexpected tokens at line 4 of " + dslFile.getAbsolutePath() + ": hello world", e.getMessage()); + } + } + + @Test + void test_unexpectedTokensInWorkspace() { + File dslFile = new File("src/test/resources/dsl/unexpected-tokens-in-workspace.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Unexpected tokens (expected: name, description, properties, !docs, !adrs, !identifiers, !impliedRelationships, model, views, configuration) at line 3 of " + dslFile.getAbsolutePath() + ": softwareSystem \"Name\"", e.getMessage()); + } + } + + @Test + void test_urlNotPermittedInGroup() { + File dslFile = new File("src/test/resources/dsl/group-url.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Unexpected tokens (expected: !docs, !adrs, group, container, description, tags, url, properties, perspectives, ->) at line 6 of " + dslFile.getAbsolutePath() + ": url \"https://example.com\"", e.getMessage()); + } + } + + @Test + void test_multipleWorkspaceTokens_ThrowsAnException() { + File dslFile = new File("src/test/resources/dsl/multiple-workspace-tokens.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Multiple workspaces are not permitted in a DSL definition at line 9 of " + dslFile.getAbsolutePath() + ": workspace {", e.getMessage()); + } + } + + @Test + void test_multipleModelTokens_ThrowsAnException() { + File dslFile = new File("src/test/resources/dsl/multiple-model-tokens.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Multiple models are not permitted in a DSL definition at line 7 of " + dslFile.getAbsolutePath() + ": model {", e.getMessage()); + } + } + + @Test + void test_multipleViewTokens_ThrowsAnException() { + File dslFile = new File("src/test/resources/dsl/multiple-view-tokens.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Multiple view sets are not permitted in a DSL definition at line 13 of " + dslFile.getAbsolutePath() + ": views {", e.getMessage()); + } + } + + @Test + void test_dynamicViewWithExplicitRelationships() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/dynamic-view-with-explicit-relationships.dsl")); + } + + @Test + void test_dynamicViewWithCustomElements() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/dynamic-view-with-custom-elements.dsl")); + } + + @Test + void test_workspaceProperties() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/workspace-properties.dsl")); + + assertEquals("false", parser.getWorkspace().getProperties().get("structurizr.dslEditor")); + } + + @Test + void test_viewsWithoutKeys() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/views-without-keys.dsl")); + + assertTrue(parser.getWorkspace().getViews().getSystemLandscapeViews().stream().anyMatch(view -> view.getKey().equals("SystemLandscape-001"))); + assertTrue(parser.getWorkspace().getViews().getSystemLandscapeViews().stream().anyMatch(view -> view.getKey().equals("SystemLandscape-002"))); + assertTrue(parser.getWorkspace().getViews().getSystemLandscapeViews().stream().anyMatch(view -> view.getKey().equals("SystemLandscape-003"))); + } + + @Test + void test_identifiers() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/identifiers.dsl")); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + + Person user = model.getPersonWithName("User"); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Software System"); + Container container = softwareSystem.getContainerWithName("Container"); + Relationship relationship = user.getEfferentRelationshipWith(container); + Relationship impliedRelationship = user.getEfferentRelationshipWith(softwareSystem); + + IdentifiersRegister register = parser.getIdentifiersRegister(); + assertEquals("user", register.findIdentifier(user)); + assertEquals("softwaresystem", register.findIdentifier(softwareSystem)); + assertEquals("softwaresystem.container", register.findIdentifier(container)); + assertEquals("rel", register.findIdentifier(relationship)); + + assertSame(user, register.getElement("user")); + assertSame(softwareSystem, register.getElement("softwareSystem")); + assertSame(container, register.getElement("softwareSystem.container")); + assertSame(relationship, register.getRelationship("rel")); + + assertEquals("user", user.getProperties().get("structurizr.dsl.identifier")); + assertEquals("softwaresystem", softwareSystem.getProperties().get("structurizr.dsl.identifier")); + assertEquals("softwaresystem.container", container.getProperties().get("structurizr.dsl.identifier")); + assertEquals("rel", relationship.getProperties().get("structurizr.dsl.identifier")); + assertNull(impliedRelationship.getProperties().get("structurizr.dsl.identifier")); + } + + @Test + void test_imageViews_ViaFiles() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/image-views/workspace-via-file.dsl")); + + Workspace workspace = parser.getWorkspace(); + assertEquals(4, workspace.getViews().getImageViews().size()); + + ImageView plantumlView = (ImageView)workspace.getViews().getViewWithKey("plantuml"); + assertEquals("diagram.puml", plantumlView.getTitle()); + assertEquals("http://localhost:7777/png/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", plantumlView.getContent()); + assertEquals("image/png", plantumlView.getContentType()); + + ImageView mermaidView = (ImageView)workspace.getViews().getViewWithKey("mermaid"); + assertEquals("diagram.mmd", mermaidView.getTitle()); + assertEquals("http://localhost:8888/img/Zmxvd2NoYXJ0IFRECiAgICBTdGFydCAtLT4gU3RvcA==?type=png", mermaidView.getContent()); + assertEquals("image/png", mermaidView.getContentType()); + + ImageView krokiView = (ImageView)workspace.getViews().getViewWithKey("kroki"); + assertEquals("diagram.dot", krokiView.getTitle()); + assertEquals("http://localhost:9999/graphviz/png/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", krokiView.getContent()); + assertEquals("image/png", krokiView.getContentType()); + + ImageView imageView = (ImageView)workspace.getViews().getViewWithKey("image"); + assertEquals("logo.png", imageView.getTitle()); + assertEquals("data:image/png;base64,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", imageView.getContent()); + assertEquals("image/png", imageView.getContentType()); + } + + @Test + void test_imageViews_ViaUrls() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/image-views/workspace-via-url.dsl")); + + Workspace workspace = parser.getWorkspace(); + assertEquals(4, workspace.getViews().getImageViews().size()); + + ImageView plantumlView = (ImageView)workspace.getViews().getViewWithKey("plantuml"); + assertEquals("diagram.puml", plantumlView.getTitle()); + assertEquals("http://localhost:7777/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", plantumlView.getContent()); + assertEquals("image/svg+xml", plantumlView.getContentType()); + + ImageView mermaidView = (ImageView)workspace.getViews().getViewWithKey("mermaid"); + assertEquals("diagram.mmd", mermaidView.getTitle()); + assertEquals("http://localhost:8888/svg/Zmxvd2NoYXJ0IFRECiAgICBTdGFydCAtLT4gU3RvcA==", mermaidView.getContent()); + assertEquals("image/svg+xml", mermaidView.getContentType()); + + ImageView krokiView = (ImageView)workspace.getViews().getViewWithKey("kroki"); + assertEquals("diagram.dot", krokiView.getTitle()); + assertEquals("http://localhost:9999/graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", krokiView.getContent()); + assertEquals("image/svg+xml", krokiView.getContentType()); + + ImageView imageView = (ImageView)workspace.getViews().getViewWithKey("image"); + assertEquals("logo.png", imageView.getTitle()); + assertEquals("https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/image-views/logo.png", imageView.getContent()); + assertEquals("image/png", imageView.getContentType()); + } + + @Test + void test_EmptyDeploymentEnvironment() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/deployment-environment-empty.dsl")); + + assertEquals(1, parser.getWorkspace().getModel().getDeploymentNodes().size()); + } + + @Test + void test_MultiLineSupport() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/multi-line.dsl")); + + assertNotNull(parser.getWorkspace().getModel().getSoftwareSystemWithName("Software System")); + } + + @Test + void test_MultiLineWithError() { + File dslFile = new File("src/test/resources/dsl/multi-line-with-error.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + // check that the error message includes the original line number + assertEquals("Unexpected tokens (expected: !docs, !adrs, group, container, description, tags, url, properties, perspectives, ->) at line 8 of " + dslFile.getAbsolutePath() + ": component \"Component\" // components not permitted inside software systems", e.getMessage()); + } + } + + @Test + void test_RelationshipAlreadyExists() { + File dslFile = new File("src/test/resources/dsl/relationship-already-exists.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("A relationship between \"SoftwareSystem://B\" and \"SoftwareSystem://A\" already exists at line 10 of " + dslFile.getAbsolutePath() + ": b -> a", e.getMessage()); + } + } + + @Test + void test_ExcludeImpliedRelationship() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/exclude-implied-relationship.dsl")); + + // check the system landscape view doesn't include any relationships + assertEquals(0, parser.getWorkspace().getViews().getSystemLandscapeViews().iterator().next().getRelationships().size()); + } + + @Test + void test_IncludeImpliedRelationship() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/include-implied-relationship.dsl")); + + // check the system landscape view includes a relationship + assertEquals(1, parser.getWorkspace().getViews().getSystemLandscapeViews().iterator().next().getRelationships().size()); + } + + @Test + void test_GroupWithoutBrace() throws Exception { + File dslFile = new File("src/test/resources/dsl/group-without-brace.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Expected: group <name> { at line 4 of " + dslFile.getAbsolutePath() + ": group \"Name\"", e.getMessage()); + } + } + + @Test + void test_ISO8859Encoding() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setCharacterEncoding(StandardCharsets.ISO_8859_1); + parser.parse(new File("src/test/resources/dsl/iso-8859.dsl")); + assertNotNull(parser.getWorkspace().getModel().getSoftwareSystemWithName("Namé")); + } + + @Test + void test_ScriptInDynamicView() throws Exception { + File dslFile = new File("src/test/resources/dsl/script-in-dynamic-view.dsl"); + + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java new file mode 100644 index 000000000..70bba7b50 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java @@ -0,0 +1,187 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Person; +import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.DynamicView; +import com.structurizr.view.RelationshipView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class DynamicViewContentParserTests extends AbstractTests { + + private DynamicViewContentParser parser = new DynamicViewContentParser(); + + @Test + void test_parseRelationship_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseRelationship(new DynamicViewDslContext(null), tokens("source", "->", "destination", "description", "technology", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: <identifier> -> <identifier> [description] [technology]", e.getMessage()); + } + } + + @Test + void test_parseRelationship_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { + try { + parser.parseRelationship(new DynamicViewDslContext(null), tokens("source", "->")); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier> -> <identifier> [description] [technology]", e.getMessage()); + } + } + + @Test + void test_parseRelationship_ThrowsAnException_WhenTheSourceElementIsNotDefined() { + DynamicViewDslContext context = new DynamicViewDslContext(null); + + try { + parser.parseRelationship(context, tokens("source", "->", "destination")); + fail(); + } catch (Exception e) { + assertEquals("The source element \"source\" does not exist", e.getMessage()); + } + } + + @Test + void test_parseRelationship_ThrowsAnException_WhenTheSourceElementIsNotAStaticStructureElement() { + DynamicViewDslContext context = new DynamicViewDslContext(null); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", model.addDeploymentNode("Deployment Node")); + context.setIdentifierRegister(elements); + + try { + parser.parseRelationship(context, tokens("source", "->", "destination")); + fail(); + } catch (Exception e) { + assertEquals("The source element \"source\" should be a static structure or custom element", e.getMessage()); + } + } + + @Test + void test_parseRelationship_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { + DynamicViewDslContext context = new DynamicViewDslContext(null); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", model.addPerson("User", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parseRelationship(context, tokens("source", "->", "destination")); + fail(); + } catch (Exception e) { + assertEquals("The destination element \"destination\" does not exist", e.getMessage()); + } + } + + @Test + void test_parseRelationship_ThrowsAnException_WhenTheDestinationElementIsNotAStaticStructureElement() { + DynamicViewDslContext context = new DynamicViewDslContext(null); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", model.addPerson("User", "Description")); + elements.register("destination", model.addDeploymentNode("Deployment Node")); + context.setIdentifierRegister(elements); + + try { + parser.parseRelationship(context, tokens("source", "->", "destination")); + fail(); + } catch (Exception e) { + assertEquals("The destination element \"destination\" should be a static structure or custom element", e.getMessage()); + } + } + + @Test + void test_parseRelationship_AddsTheRelationshipToTheView_WhenItAlreadyExistsInTheModel() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + user.uses(softwareSystem, "Uses"); + DynamicView view = views.createDynamicView("key", "Description"); + DynamicViewDslContext context = new DynamicViewDslContext(view); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parseRelationship(context, tokens("source", "->", "destination")); + + assertEquals(1, view.getRelationships().size()); + RelationshipView rv = view.getRelationships().iterator().next(); + assertSame(user, rv.getRelationship().getSource()); + assertSame(softwareSystem, rv.getRelationship().getDestination()); + assertEquals("", rv.getDescription()); + assertEquals("1", rv.getOrder()); + } + + @Test + void test_parseRelationship_AddsTheRelationshipToTheViewWithAnOverriddenDescription_WhenItAlreadyExistsInTheModel() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + user.uses(softwareSystem, "Uses"); + DynamicView view = views.createDynamicView("key", "Description"); + DynamicViewDslContext context = new DynamicViewDslContext(view); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parseRelationship(context, tokens("source", "->", "destination", "Does something with")); + + assertEquals(1, view.getRelationships().size()); + RelationshipView rv = view.getRelationships().iterator().next(); + assertSame(user, rv.getRelationship().getSource()); + assertSame(softwareSystem, rv.getRelationship().getDestination()); + assertEquals("Does something with", rv.getDescription()); + assertEquals("1", rv.getOrder()); + } + + @Test + void test_parseRelationship_AddsTheRelationshipWithTheSpecifiedTechnologyToTheView_WhenItAlreadyExistsInTheModel() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Relationship r1 = user.uses(softwareSystem, "Uses 1", "Tech 1"); + Relationship r2 = user.uses(softwareSystem, "Uses 2", "Tech 2"); + DynamicView view = views.createDynamicView("key", "Description"); + DynamicViewDslContext context = new DynamicViewDslContext(view); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parseRelationship(context, tokens("source", "->", "destination", "Description", "Tech 2")); + + assertEquals(1, view.getRelationships().size()); + RelationshipView rv = view.getRelationships().iterator().next(); + assertSame(r2, rv.getRelationship()); + assertSame(user, rv.getRelationship().getSource()); + assertSame(softwareSystem, rv.getRelationship().getDestination()); + assertEquals("Description", rv.getDescription()); + assertEquals("1", rv.getOrder()); + } + + @Test + void test_parseRelationship_ThrowsAnException_WhenItDoesNotAlreadyExistInTheModel() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DynamicView view = views.createDynamicView("key", "Description"); + DynamicViewDslContext context = new DynamicViewDslContext(view); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + try { + parser.parseRelationship(context, tokens("source", "->", "destination", "Uses")); + fail(); + } catch (Exception e) { + assertEquals("A relationship between User and Software System does not exist in model.", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewParserTests.java new file mode 100644 index 000000000..f367ff064 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewParserTests.java @@ -0,0 +1,197 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.DynamicView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class DynamicViewParserTests extends AbstractTests { + + private DynamicViewParser parser = new DynamicViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("dynamic", "identifier", "key", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: dynamic <*|software system identifier|container identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIdentifierIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("dynamic")); + fail(); + } catch (Exception e) { + assertEquals("Expected: dynamic <*|software system identifier|container identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_GeneratesAKey_WhenTheKeyIsMissing() { + DslContext context = context(); + DynamicView view = parser.parse(context, tokens("dynamic", "*")); + + assertEquals("Dynamic-001", view.getKey()); + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotDefined() { + DslContext context = context(); + try { + parser.parse(context, tokens("dynamic", "softwareSystem", "key")); + fail(); + } catch (Exception e) { + assertEquals("The software system or container \"softwareSystem\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotASoftwareSystemOrContainer() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("person", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("dynamic", "person", "key")); + fail(); + } catch (Exception e) { + assertEquals("The element \"person\" is not a software system or container", e.getMessage()); + } + } + + @Test + void test_parse_CreatesADynamicViewWithNoScope() { + parser.parse(context(), tokens("dynamic", "*", "key")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertNull(views.get(0).getElement()); + } + + @Test + void test_parse_CreatesADynamicViewWithNoScopeAndADescription() { + parser.parse(context(), tokens("dynamic", "*", "key", "Description")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + assertNull(views.get(0).getElement()); + } + + @Test + void test_parse_CreatesADynamicViewWithSoftwareSystemScope() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("dynamic", "softwareSystem")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("Dynamic-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertSame(softwareSystem, views.get(0).getElement()); + } + + @Test + void test_parse_CreatesADynamicViewWithSoftwareSystemScopeAndKey() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("dynamic", "softwareSystem", "key")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertSame(softwareSystem, views.get(0).getElement()); + } + + @Test + void test_parse_CreatesADynamicViewWithSoftwareSystemScopeAndKeyAndDescription() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("dynamic", "softwareSystem", "key", "Description")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + assertSame(softwareSystem, views.get(0).getElement()); + } + + @Test + void test_parse_CreatesADynamicViewWithContainerScope() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + Container container = model.addSoftwareSystem("Name", "Description").addContainer("Container", "Description", "Technology"); + elements.register("container", container); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("dynamic", "container")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("Dynamic-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertSame(container, views.get(0).getElement()); + } + + @Test + void test_parse_CreatesADynamicViewWithContainerScopeAndKey() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + Container container = model.addSoftwareSystem("Name", "Description").addContainer("Container", "Description", "Technology"); + elements.register("container", container); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("dynamic", "container", "key")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + assertSame(container, views.get(0).getElement()); + } + + @Test + void test_parse_CreatesADynamicViewWithContainerScopeAndKeyAndDescription() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + Container container = model.addSoftwareSystem("Name", "Description").addContainer("Container", "Description", "Technology"); + elements.register("container", container); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("dynamic", "container", "key", "Description")); + List<DynamicView> views = new ArrayList<>(this.views.getDynamicViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + assertSame(container, views.get(0).getElement()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewRelationshipParserTests.java new file mode 100644 index 000000000..2bd6cf066 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewRelationshipParserTests.java @@ -0,0 +1,52 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.DynamicView; +import com.structurizr.view.RelationshipView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class DynamicViewRelationshipParserTests extends AbstractTests { + + private final DynamicViewRelationshipParser parser = new DynamicViewRelationshipParser(); + + @Test + void test_parseUrl_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + DynamicViewRelationshipContext context = new DynamicViewRelationshipContext(null); + parser.parseUrl(context, tokens("url", "url", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: url <url>", e.getMessage()); + } + } + + @Test + void test_parseUrl_ThrowsAnException_WhenNoUrlIsSpecified() { + try { + DynamicViewRelationshipContext context = new DynamicViewRelationshipContext(null); + parser.parseUrl(context, tokens("url")); + fail(); + } catch (Exception e) { + assertEquals("Expected: url <url>", e.getMessage()); + } + } + + @Test + void test_parseUrl_SetsTheUrl_WhenAUrlIsSpecified() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship r = a.uses(b, "Uses"); + + DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); + RelationshipView rv = dynamicView.add(r); + DynamicViewRelationshipContext context = new DynamicViewRelationshipContext(rv); + parser.parseUrl(context, tokens("url", "http://example.com")); + + assertEquals("http://example.com", rv.getUrl()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java new file mode 100644 index 000000000..50086e8dc --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -0,0 +1,546 @@ +package com.structurizr.dsl; + +import com.structurizr.view.Border; +import com.structurizr.view.ElementStyle; +import com.structurizr.view.Shape; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +class ElementStyleParserTests extends AbstractTests { + + private ElementStyleParser parser = new ElementStyleParser(); + private ElementStyle elementStyle; + + private ElementStyleDslContext elementStyleDslContext() { + elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag"); + ElementStyleDslContext context = new ElementStyleDslContext(elementStyle, new File(".")); + context.setWorkspace(workspace); + + return context; + } + + @Test + void test_parseElementStyle_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseElementStyle(context(), tokens("element", "tag", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: element <tag> {", e.getMessage()); + } + } + + @Test + void test_parseElementStyle_ThrowsAnException_WhenTheTagIsMissing() { + try { + parser.parseElementStyle(context(), tokens("element")); + fail(); + } catch (Exception e) { + assertEquals("Expected: element <tag> {", e.getMessage()); + } + } + + @Test + void test_parseElementStyle_ThrowsAnException_WhenTheTagIsEmpty() { + try { + parser.parseElementStyle(context(), tokens("element", "")); + fail(); + } catch (Exception e) { + assertEquals("A tag must be specified", e.getMessage()); + } + } + + @Test + void test_parseElementStyle_CreatesAnElementStyle() { + parser.parseElementStyle(context(), tokens("element", "Element")); + + ElementStyle style = workspace.getViews().getConfiguration().getStyles().getElements().stream().filter(es -> "Element".equals(es.getTag())).findFirst().get(); + assertNotNull(style); + } + + @Test + void test_parseElementStyle_FindsAnExistingElementStyle() { + ElementStyle style = workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag"); + assertSame(style, parser.parseElementStyle(context(), tokens("element", "Tag"))); + } + + @Test + void test_parseShape_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseShape(elementStyleDslContext(), tokens("shape", "shape", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: shape <Box|RoundedBox|Circle|Ellipse|Hexagon|Diamond|Cylinder|Pipe|Person|Robot|Folder|WebBrowser|Window|MobileDevicePortrait|MobileDeviceLandscape|Component>", e.getMessage()); + } + } + + @Test + void test_parseShape_ThrowsAnException_WhenTheShapeIsMissing() { + try { + parser.parseShape(elementStyleDslContext(), tokens("shape")); + fail(); + } catch (Exception e) { + assertEquals("Expected: shape <Box|RoundedBox|Circle|Ellipse|Hexagon|Diamond|Cylinder|Pipe|Person|Robot|Folder|WebBrowser|Window|MobileDevicePortrait|MobileDeviceLandscape|Component>", e.getMessage()); + } + } + + @Test + void test_parseShape_ThrowsAnException_WhenTheShapeIsNotValid() { + try { + parser.parseShape(elementStyleDslContext(), tokens("shape", "square")); + fail(); + } catch (Exception e) { + assertEquals("The shape \"square\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseShape_SetsTheShape() { + parser.parseShape(elementStyleDslContext(), tokens("shape", "roundedbox")); + assertEquals(Shape.RoundedBox, elementStyle.getShape()); + } + + @Test + void test_parseBackground_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseBackground(elementStyleDslContext(), tokens("background", "hex", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: background <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseBackground_ThrowsAnException_WhenTheBackgroundIsMissing() { + try { + parser.parseBackground(elementStyleDslContext(), tokens("background")); + fail(); + } catch (Exception e) { + assertEquals("Expected: background <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseBackground_SetsTheBackgroundWhenUsingAHexColourCode() { + parser.parseBackground(elementStyleDslContext(), tokens("background", "#ff0000")); + assertEquals("#ff0000", elementStyle.getBackground()); + } + + @Test + void test_parseBackground_SetsTheBackgroundWhenUsingAColourName() { + parser.parseBackground(elementStyleDslContext(), tokens("background", "yellow")); + assertEquals("#ffff00", elementStyle.getBackground()); + } + + @Test + void test_parseStroke_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseStroke(elementStyleDslContext(), tokens("stroke", "hex", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: stroke <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseStroke_ThrowsAnException_WhenTheStrokeIsMissing() { + try { + parser.parseStroke(elementStyleDslContext(), tokens("stroke")); + fail(); + } catch (Exception e) { + assertEquals("Expected: stroke <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseStroke_SetsTheStrokeWhenUsingAHexColourCode() { + parser.parseStroke(elementStyleDslContext(), tokens("stroke", "yellow")); + assertEquals("#ffff00", elementStyle.getStroke()); + } + + @Test + void test_parseStrokeWidth_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseStrokeWidth(elementStyleDslContext(), tokens("strokeWidth", "4", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: strokeWidth <1-10>", e.getMessage()); + } + } + + @Test + void test_parseStrokeWidth_ThrowsAnException_WhenTheStrokeWidthIsMissing() { + try { + parser.parseStrokeWidth(elementStyleDslContext(), tokens("strokeWidth")); + fail(); + } catch (Exception e) { + assertEquals("Expected: strokeWidth <1-10>", e.getMessage()); + } + } + + @Test + void test_parseStrokeWidth_ThrowsAnException_WhenTheStrokeWidthIsNotANumber() { + try { + parser.parseStrokeWidth(elementStyleDslContext(), tokens("strokeWidth", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Stroke width must be an integer between 1 and 10", e.getMessage()); + } + } + + @Test + void test_parseStrokeWidth_SetsTheStrokeWidth() { + parser.parseStrokeWidth(elementStyleDslContext(), tokens("strokeWidth", "4")); + assertEquals(4, elementStyle.getStrokeWidth()); + } + + @Test + void test_parseColour_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseColour(elementStyleDslContext(), tokens("colour", "hex", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: colour <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseColour_ThrowsAnException_WhenTheColourIsMissing() { + try { + parser.parseColour(elementStyleDslContext(), tokens("colour")); + fail(); + } catch (Exception e) { + assertEquals("Expected: colour <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseColour_SetsTheColourWhenUsingAHexColourCode() { + parser.parseColour(elementStyleDslContext(), tokens("colour", "#ff0000")); + assertEquals("#ff0000", elementStyle.getColor()); + } + + @Test + void test_parseColour_SetsTheColourWhenUsingColourName() { + parser.parseColour(elementStyleDslContext(), tokens("colour", "yellow")); + assertEquals("#ffff00", elementStyle.getColor()); + } + + @Test + void test_parseBorder_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseBorder(elementStyleDslContext(), tokens("border", "style", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: border <solid|dashed|dotted>", e.getMessage()); + } + } + + @Test + void test_parseBorder_ThrowsAnException_WhenTheBorderIsMissing() { + try { + parser.parseBorder(elementStyleDslContext(), tokens("border")); + fail(); + } catch (Exception e) { + assertEquals("Expected: border <solid|dashed|dotted>", e.getMessage()); + } + } + + @Test + void test_parseBorder_ThrowsAnException_WhenTheBorderIsNotValid() { + try { + parser.parseBorder(elementStyleDslContext(), tokens("border", "rounded")); + fail(); + } catch (Exception e) { + assertEquals("The border \"rounded\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseBorder_SetsTheBorder() { + parser.parseBorder(elementStyleDslContext(), tokens("border", "dotted")); + assertEquals(Border.Dotted, elementStyle.getBorder()); + } + + @Test + void test_parseOpacity_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseOpacity(elementStyleDslContext(), tokens("opacity", "percentage", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: opacity <0-100>", e.getMessage()); + } + } + + @Test + void test_parseOpacity_ThrowsAnException_WhenTheOpacityIsMissing() { + try { + parser.parseOpacity(elementStyleDslContext(), tokens("opacity")); + fail(); + } catch (Exception e) { + assertEquals("Expected: opacity <0-100>", e.getMessage()); + } + } + + @Test + void test_parseOpacity_ThrowsAnException_WhenTheOpacityIsNotValid() { + try { + parser.parseOpacity(elementStyleDslContext(), tokens("opacity", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Opacity must be an integer between 0 and 100", e.getMessage()); + } + } + + @Test + void test_parseOpacity_SetsTheOpacity() { + parser.parseOpacity(elementStyleDslContext(), tokens("opacity", "75")); + assertEquals(75, elementStyle.getOpacity()); + } + + @Test + void test_parseWidth_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseWidth(elementStyleDslContext(), tokens("width", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: width <number>", e.getMessage()); + } + } + + @Test + void test_parseWidth_ThrowsAnException_WhenTheWidthIsMissing() { + try { + parser.parseWidth(elementStyleDslContext(), tokens("width")); + fail(); + } catch (Exception e) { + assertEquals("Expected: width <number>", e.getMessage()); + } + } + + @Test + void test_parseWidth_ThrowsAnException_WhenTheWidthIsNotValid() { + try { + parser.parseWidth(elementStyleDslContext(), tokens("width", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Width must be a positive integer", e.getMessage()); + } + } + + @Test + void test_parseWidth_SetsTheWidth() { + parser.parseWidth(elementStyleDslContext(), tokens("width", "75")); + assertEquals(75, elementStyle.getWidth()); + } + + @Test + void test_parseHeight_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseHeight(elementStyleDslContext(), tokens("height", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: height <number>", e.getMessage()); + } + } + + @Test + void test_parseHeight_ThrowsAnException_WhenTheHeightIsMissing() { + try { + parser.parseHeight(elementStyleDslContext(), tokens("height")); + fail(); + } catch (Exception e) { + assertEquals("Expected: height <number>", e.getMessage()); + } + } + + @Test + void test_parseHeight_ThrowsAnException_WhenTheHeightIsNotValid() { + try { + parser.parseHeight(elementStyleDslContext(), tokens("height", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Height must be a positive integer", e.getMessage()); + } + } + + @Test + void test_parseHeight_SetsTheHeight() { + parser.parseHeight(elementStyleDslContext(), tokens("height", "75")); + assertEquals(75, elementStyle.getHeight()); + } + + @Test + void test_parseFontSize_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseFontSize(elementStyleDslContext(), tokens("fontSize", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: fontSize <number>", e.getMessage()); + } + } + + @Test + void test_parseFontSize_ThrowsAnException_WhenTheFontSizeIsMissing() { + try { + parser.parseFontSize(elementStyleDslContext(), tokens("fontSize")); + fail(); + } catch (Exception e) { + assertEquals("Expected: fontSize <number>", e.getMessage()); + } + } + + @Test + void test_parseFontSize_ThrowsAnException_WhenTheFontSizeIsNotValid() { + try { + parser.parseFontSize(elementStyleDslContext(), tokens("fontSize", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Font size must be a positive integer", e.getMessage()); + } + } + + @Test + void test_parseFontSize_SetsTheFontSize() { + parser.parseFontSize(elementStyleDslContext(), tokens("fontSize", "75")); + assertEquals(75, elementStyle.getFontSize()); + } + + @Test + void test_parseMetadata_ThrowsAnException_WhenTheMetadataIsMissing() { + try { + parser.parseMetadata(elementStyleDslContext(), tokens("metadata")); + fail(); + } catch (Exception e) { + assertEquals("Expected: metadata <true|false>", e.getMessage()); + } + } + + @Test + void test_parseMetadata_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseMetadata(elementStyleDslContext(), tokens("metadata", "boolean", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: metadata <true|false>", e.getMessage()); + } + } + + @Test + void test_parseMetadata_ThrowsAnException_WhenTheMetadataIsNotValid() { + try { + parser.parseMetadata(elementStyleDslContext(), tokens("metadata", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Metadata must be true or false", e.getMessage()); + } + } + + @Test + void test_parseMetadata_SetsTheMetadata() { + ElementStyleDslContext context = elementStyleDslContext(); + parser.parseMetadata(context, tokens("metadata", "false")); + assertEquals(false, elementStyle.getMetadata()); + + parser.parseMetadata(context, tokens("metadata", "true")); + assertEquals(true, elementStyle.getMetadata()); + } + + @Test + void test_parseDescription_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseDescription(elementStyleDslContext(), tokens("description", "boolean", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: description <true|false>", e.getMessage()); + } + } + + @Test + void test_parseDescription_ThrowsAnException_WhenTheDescriptionIsMissing() { + try { + parser.parseDescription(elementStyleDslContext(), tokens("description")); + fail(); + } catch (Exception e) { + assertEquals("Expected: description <true|false>", e.getMessage()); + } + } + + @Test + void test_parseDescription_ThrowsAnException_WhenTheDescriptionIsNotValid() { + try { + parser.parseDescription(elementStyleDslContext(), tokens("description", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Description must be true or false", e.getMessage()); + } + } + + @Test + void test_parseDescription_SetsTheDescription() { + ElementStyleDslContext context = elementStyleDslContext(); + parser.parseDescription(context, tokens("description", "false")); + assertEquals(false, elementStyle.getDescription()); + + parser.parseDescription(context, tokens("description", "true")); + assertEquals(true, elementStyle.getDescription()); + } + + @Test + void test_parseIcon_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseIcon(elementStyleDslContext(), tokens("icon", "file", "extra"), false); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: icon <file|url>", e.getMessage()); + } + } + + @Test + void test_parseIcon_ThrowsAnException_WhenTheIconIsMissing() { + try { + parser.parseIcon(elementStyleDslContext(), tokens("icon"), false); + fail(); + } catch (Exception e) { + assertEquals("Expected: icon <file|url>", e.getMessage()); + } + } + + @Test + void test_parseIcon_ThrowsAnException_WhenTheIconDoesNotExist() { + try { + parser.parseIcon(elementStyleDslContext(), tokens("icon", "hello.png"), false); + fail(); + } catch (Exception e) { + assertEquals("hello.png does not exist", e.getMessage()); + } + } + + @Test + void test_parseIcon_SetsTheIconFromADataUri() { + parser.parseIcon(elementStyleDslContext(), tokens("icon", "data:image/png;base64,123456789012345678901234567890"), true); + assertTrue(elementStyle.getIcon().startsWith("data:image/png;base64,123456789012345678901234567890")); + } + + @Test + void test_parseIcon_SetsTheIconFromAHttpUrl() { + parser.parseIcon(elementStyleDslContext(), tokens("icon", "http://structurizr.com/logo.png"), true); + assertEquals("http://structurizr.com/logo.png", elementStyle.getIcon()); + } + + @Test + void test_parseIcon_SetsTheIconFromAHttpsUrl() { + parser.parseIcon(elementStyleDslContext(), tokens("icon", "https://structurizr.com/logo.png"), true); + assertEquals("https://structurizr.com/logo.png", elementStyle.getIcon()); + } + + @Test + void test_parseIcon_SetsTheIconFromAFile() { + parser.parseIcon(elementStyleDslContext(), tokens("icon", "src/test/resources/dsl/logo.png"), false); + System.out.println(elementStyle.getIcon()); + assertTrue(elementStyle.getIcon().startsWith("data:image/png;base64,")); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java new file mode 100644 index 000000000..fa28b7cec --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java @@ -0,0 +1,63 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Enterprise; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EnterpriseParserTests extends AbstractTests { + + private EnterpriseParser parser = new EnterpriseParser(); + + @Test + void test_parse_SetsTheEnterpriseName_WhenItHasNotBeenSet() { + assertNull(workspace.getModel().getEnterprise()); + parser.parse(context(), tokens("enterprise", "New Name")); + assertEquals("New Name", workspace.getModel().getEnterprise().getName()); + } + + @Test + void test_parse_ThrowsAnException_WhenTheEnterpriseNameHasAlreadyBeenSet() { + workspace.getModel().setEnterprise(new Enterprise("My Enterprise")); + try { + parser.parse(context(), tokens("enterprise", "name")); + fail(); + } catch (Exception e) { + assertEquals("The name of the enterprise has already been set", e.getMessage()); + } + } + + @Test + void test_parse_DoesNothing_WhenANameIsSpecifiedButIsTheSameAsTheExistingEnterpriseName() { + workspace.getModel().setEnterprise(new Enterprise("My Enterprise")); + parser.parse(context(), tokens("My Enterprise")); + + assertEquals("My Enterprise", workspace.getModel().getEnterprise().getName()); + } + + @Test + void test_parse_DoesNothing_WhenNoNameIsSpecified() { + parser.parse(context(), tokens("enterprise")); + + assertNull(workspace.getModel().getEnterprise()); + } + + @Test + void test_parse_DoesNothing_WhenNoNameIsSpecifiedAndTheEnterpriseNameHasAlreadyBeenSet() { + workspace.getModel().setEnterprise(new Enterprise("My Enterprise")); + parser.parse(context(), tokens("enterprise")); + + assertEquals("My Enterprise", workspace.getModel().getEnterprise().getName()); + } + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("enterprise", "name", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: enterprise [name]", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java new file mode 100644 index 000000000..1c9b5c401 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java @@ -0,0 +1,232 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ExplicitRelationshipParserTests extends AbstractTests { + + private ExplicitRelationshipParser parser = new ExplicitRelationshipParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("source", "->", "destination", "description", "technology", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: <identifier> -> <identifier> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { + try { + parser.parse(context(), tokens("source", "->")); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier> -> <identifier> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheSourceElementIsNotDefined() { + try { + parser.parse(context(), tokens("source", "->", "destination")); + fail(); + } catch (Exception e) { + assertEquals("The source element \"source\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", model.addPerson("User", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("source", "->", "destination")); + fail(); + } catch (Exception e) { + assertEquals("The destination element \"destination\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_AddsTheRelationship() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DslContext context = context(); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("source", "->", "destination")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("", r.getDescription()); + assertEquals("", r.getTechnology()); + assertEquals("Relationship", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationshipWithADescription() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DslContext context = context(); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("source", "->", "destination", "Uses")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("", r.getTechnology()); + assertEquals("Relationship", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationshipWithADescriptionAndTechnology() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DslContext context = context(); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + } + + @Test + void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTags() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DslContext context = context(); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + assertEquals("Relationship,Tag 1,Tag 2", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTechnologyAndTags() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DslContext context = context(); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", container); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + assertEquals(2, model.getRelationships().size()); + + // this is the relationship that was created + Relationship r = user.getEfferentRelationshipWith(container); + assertSame(user, r.getSource()); + assertSame(container, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + assertEquals("Relationship,Tag 1,Tag 2", r.getTags()); + + // and this is an implied relationship + r = user.getEfferentRelationshipWith(softwareSystem); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + assertEquals("", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationship_WithASourceOfThis() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + GroupableElementDslContext context = new PersonDslContext(user); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("this", "->", "destination")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("", r.getDescription()); + assertEquals("", r.getTechnology()); + assertEquals("Relationship", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationship_WithADestinationOfThis() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + GroupableElementDslContext context = new SoftwareSystemDslContext(softwareSystem); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("source", "->", "this")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("", r.getDescription()); + assertEquals("", r.getTechnology()); + assertEquals("Relationship", r.getTags()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExternalScriptDslContextTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExternalScriptDslContextTests.java new file mode 100644 index 000000000..706c9cdc6 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExternalScriptDslContextTests.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class ExternalScriptDslContextTests extends AbstractTests { + + @Test + void test_parseExternal_RunsTheScript_WhenAValidScriptFilenameIsSpecified() { + ExternalScriptDslContext context = new ExternalScriptDslContext(new WorkspaceDslContext(), new File("src/test/resources/dsl/workspace.dsl"), "test.kts"); + context.setWorkspace(workspace); + context.end(); + + assertNotNull(workspace.getModel().getPersonWithName("Kotlin")); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java new file mode 100644 index 000000000..5e5c5699c --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java @@ -0,0 +1,139 @@ +package com.structurizr.dsl; + +import com.structurizr.view.FilteredView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FilteredViewParserTests extends AbstractTests { + + private FilteredViewParser parser = new FilteredViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("filtered", "baseKey", "key", "mode", "tags", "description", "extra")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Too many tokens, expected: filtered <baseKey> <include|exclude> <tags> [key] [description]", iae.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheBaseKeyIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("filtered")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: filtered <baseKey> <include|exclude> <tags> [key] [description]", iae.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheModeIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("filtered", "baseKey")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: filtered <baseKey> <include|exclude> <tags> [key] [description]", iae.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheTagsAreMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("filtered", "baseKey", "include")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: filtered <baseKey> <include|exclude> <tags> [key] [description]", iae.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheModeIsInvalid() { + DslContext context = context(); + views.createDeploymentView("deployment", "Description"); + try { + parser.parse(context, tokens("filtered", "baseKey", "mode", "Tag 1, Tag 2", "key")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Filter mode should be include or exclude", iae.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheBaseViewDoesNotExist() { + DslContext context = context(); + try { + parser.parse(context, tokens("filtered", "baseKey", "include", "Tag 1, Tag 2", "key")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The view \"baseKey\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheBaseViewIsNotAStaticView() { + DslContext context = context(); + views.createDeploymentView("baseKey", "Description"); + try { + parser.parse(context, tokens("filtered", "baseKey", "include", "Tag 1, Tag 2", "key")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The view \"baseKey\" must be a System Landscape, System Context, Container, or Component view", iae.getMessage()); + } + } + + @Test + void test_parse_CreatesAFilteredView() { + DslContext context = context(); + views.createSystemLandscapeView("SystemLandscape", "Description"); + parser.parse(context, tokens("filtered", "SystemLandscape", "include", "Tag 1, Tag 2")); + List<FilteredView> views = new ArrayList<>(context.getWorkspace().getViews().getFilteredViews()); + + assertEquals(1, views.size()); + assertEquals("Filtered-001", views.get(0).getKey()); + assertEquals(2, views.get(0).getTags().size()); + assertTrue(views.get(0).getTags().contains("Tag 1")); + assertTrue(views.get(0).getTags().contains("Tag 2")); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesAFilteredViewWithAKey() { + DslContext context = context(); + views.createSystemLandscapeView("SystemLandscape", "Description"); + parser.parse(context, tokens("filtered", "SystemLandscape", "include", "Tag 1, Tag 2", "key")); + List<FilteredView> views = new ArrayList<>(context.getWorkspace().getViews().getFilteredViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals(2, views.get(0).getTags().size()); + assertTrue(views.get(0).getTags().contains("Tag 1")); + assertTrue(views.get(0).getTags().contains("Tag 2")); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesAFilteredViewWithAKeyAndDescription() { + DslContext context = context(); + views.createSystemLandscapeView("SystemLandscape", "Description"); + parser.parse(context, tokens("filtered", "SystemLandscape", "include", "Tag 1, Tag 2", "key", "Description")); + List<FilteredView> views = new ArrayList<>(context.getWorkspace().getViews().getFilteredViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals(2, views.get(0).getTags().size()); + assertTrue(views.get(0).getTags().contains("Tag 1")); + assertTrue(views.get(0).getTags().contains("Tag 2")); + assertEquals("Description", views.get(0).getDescription()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java new file mode 100644 index 000000000..0186144de --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java @@ -0,0 +1,72 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GroupParserTests extends AbstractTests { + + private GroupParser parser = new GroupParser(); + + @Test + void parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(null, tokens("group", "name", "{", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: group <name> {", e.getMessage()); + } + } + + @Test + void parse_ThrowsAnException_WhenTheNameIsMissing() { + try { + parser.parse(null, tokens("group")); + fail(); + } catch (Exception e) { + assertEquals("Expected: group <name> {", e.getMessage()); + } + } + + @Test + void parse_ThrowsAnException_WhenTheBraceIsMissing() { + try { + parser.parse(null, tokens("group", "Name", "foo")); + fail(); + } catch (Exception e) { + assertEquals("Expected: group <name> {", e.getMessage()); + } + } + + @Test + void parse() { + ElementGroup group = parser.parse(context(), tokens("group", "Group 1", "{")); + assertEquals("Group 1", group.getName()); + assertTrue(group.getElements().isEmpty()); + } + + @Test + void parse_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { + ModelDslContext context = new ModelDslContext(new ElementGroup(workspace.getModel(), "Group 1")); + context.setWorkspace(workspace); + + try { + parser.parse(context, tokens("group", "Group 2", "{")); + fail(); + } catch (Exception e) { + assertEquals("To use nested groups, please define a model property named structurizr.groupSeparator", e.getMessage()); + } + } + + @Test + void parse_NestedGroup() { + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + ModelDslContext context = new ModelDslContext(new ElementGroup(workspace.getModel(), "Group 1")); + context.setWorkspace(workspace); + + ElementGroup group = parser.parse(context, tokens("group", "Group 2", "{")); + assertEquals("Group 1/Group 2", group.getName()); + assertTrue(group.getElements().isEmpty()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/HealthCheckParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/HealthCheckParserTests.java new file mode 100644 index 000000000..9ab9c60b4 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/HealthCheckParserTests.java @@ -0,0 +1,127 @@ +package com.structurizr.dsl; + +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.HttpHealthCheck; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.SoftwareSystemInstance; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class HealthCheckParserTests extends AbstractTests { + + private HealthCheckParser parser = new HealthCheckParser(); + + private SoftwareSystemInstance softwareSystemInstance; + + @BeforeEach + void setUp() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name"); + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + softwareSystemInstance = deploymentNode.add(softwareSystem); + } + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "name", "url", "interval", "timeout", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: healthCheck <name> <url> [interval] [timeout]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoNameIsSpecified() { + try { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck")); + fail(); + } catch (Exception e) { + assertEquals("Expected: healthCheck <name> <url> [interval] [timeout]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoUrlIsSpecified() { + try { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "Name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: healthCheck <name> <url> [interval] [timeout]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenAnInvalidIntervalIsSpecified() { + try { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "Name", "https://example.com/health", "hello")); + fail(); + } catch (Exception e) { + assertEquals("The interval of \"hello\" is not valid - it must be a positive integer (number of seconds)", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenANegativeIntervalIsSpecified() { + try { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "Name", "https://example.com/health", "-1")); + fail(); + } catch (Exception e) { + assertEquals("The interval must be a positive integer (number of seconds)", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenAnInvalidTimeoutIsSpecified() { + try { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "Name", "https://example.com/health", "60", "hello")); + fail(); + } catch (Exception e) { + assertEquals("The timeout of \"hello\" is not valid - it must be zero or a positive integer (number of milliseconds)", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenANegativeTimeoutIsSpecified() { + try { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "Name", "https://example.com/health", "60", "-1")); + fail(); + } catch (Exception e) { + assertEquals("The timeout must be zero or a positive integer (number of milliseconds)", e.getMessage()); + } + } + + @Test + void test_parse_AddsAHealthCheck_WhenTheNameAndUrlAreSpecified() { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "Name", "https://example.com/health")); + + HttpHealthCheck healthCheck = softwareSystemInstance.getHealthChecks().iterator().next(); + assertEquals("Name", healthCheck.getName()); + assertEquals("https://example.com/health", healthCheck.getUrl()); + assertEquals(60, healthCheck.getInterval()); + assertEquals(0, healthCheck.getTimeout()); + } + + @Test + void test_parse_AddsAHealthCheck_WhenAllPropertiesAreSpecified() { + SoftwareSystemInstanceDslContext context = new SoftwareSystemInstanceDslContext(softwareSystemInstance); + parser.parse(context, tokens("healthCheck", "Name", "https://example.com/health", "120", "2000")); + + HttpHealthCheck healthCheck = softwareSystemInstance.getHealthChecks().iterator().next(); + assertEquals("Name", healthCheck.getName()); + assertEquals("https://example.com/health", healthCheck.getUrl()); + assertEquals(120, healthCheck.getInterval()); + assertEquals(2000, healthCheck.getTimeout()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java new file mode 100644 index 000000000..15936e9ee --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java @@ -0,0 +1,63 @@ +package com.structurizr.dsl; + +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class IdentifierRegisterTests extends AbstractTests { + + private final IdentifiersRegister register = new IdentifiersRegister(); + + @Test + void test_validateIdentifierName() { + new IdentifiersRegister().validateIdentifierName("a"); + new IdentifiersRegister().validateIdentifierName("abc"); + new IdentifiersRegister().validateIdentifierName("ABC"); + new IdentifiersRegister().validateIdentifierName("softwaresystem"); + new IdentifiersRegister().validateIdentifierName("SoftwareSystem"); + new IdentifiersRegister().validateIdentifierName("123456"); + new IdentifiersRegister().validateIdentifierName("_softwareSystem"); + new IdentifiersRegister().validateIdentifierName("SoftwareSystem-1"); + + try { + new IdentifiersRegister().validateIdentifierName("-softwareSystem"); + fail(); + } catch (Exception e) { + assertEquals("Identifiers cannot start with a - character", e.getMessage()); + } + + try { + new IdentifiersRegister().validateIdentifierName("SoftwareSystém"); + fail(); + } catch (Exception e) { + assertEquals("Identifiers can only contain the following characters: a-zA-Z0-9_-", e.getMessage()); + } + } + + @Test + void test_register_ThrowsAnException_WhenTheElementHasAlreadyBeenRegisteredWithADifferentIdentifier() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + try { + register.register("a", softwareSystem); + register.register("x", softwareSystem); + fail(); + } catch (Exception e) { + assertEquals("The element is already registered with an identifier of \"a\"", e.getMessage()); + } + } + + @Test + void test_register_ThrowsAnException_WhenTheElementHasAlreadyBeenRegisteredWithAnInternalIdentifier() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + try { + register.register("", softwareSystem); + register.register("x", softwareSystem); + fail(); + } catch (Exception e) { + assertEquals("Please assign an identifier to \"SoftwareSystem://Software System\" before using it with !ref", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierScopeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierScopeParserTests.java new file mode 100644 index 000000000..7ce861a1a --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierScopeParserTests.java @@ -0,0 +1,46 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class IdentifierScopeParserTests extends AbstractTests { + + private IdentifierScopeParser parser = new IdentifierScopeParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!identifiers", "hierarchical", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !identifiers <flat|hierarchical>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoScopeIsSpecified() { + try { + parser.parse(context(), tokens("!identifiers")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !identifiers <flat|hierarchical>", e.getMessage()); + } + } + + @Test + void test_parse_SetsTheScope_WhenLocalIsSpecified() { + IdentifierScope scope = parser.parse(context(), tokens("!identifiers", "hierarchical")); + + assertEquals(IdentifierScope.Hierarchical, scope); + } + + @Test + void test_parse_SetsTheScope_WhenGlobalIsSpecified() { + IdentifierScope scope = parser.parse(context(), tokens("!identifiers", "flat")); + + assertEquals(IdentifierScope.Flat, scope); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewParserTests.java new file mode 100644 index 000000000..fb2260f0a --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewParserTests.java @@ -0,0 +1,62 @@ +package com.structurizr.dsl; + +import com.structurizr.model.SoftwareSystem; +import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ImageViewParserTests extends AbstractTests { + + private final ImageViewParser parser = new ImageViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("image", "*", "key", "extra")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Too many tokens, expected: image <*|element identifier> [key] {", iae.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementDoesNotExist() { + DslContext context = context(); + try { + parser.parse(context, tokens("image", "element", "key")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element \"element\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parse_CreatesAnImageView() { + DslContext context = context(); + parser.parse(context, tokens("image", "*", "key")); + + ImageView imageView = (ImageView)context.getWorkspace().getViews().getViewWithKey("key"); + assertEquals("key", imageView.getKey()); + assertNull(imageView.getElement()); + assertNull(imageView.getElementId()); + } + + + @Test + void test_parse_CreatesAnImageViewForAnElement() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + parser.parse(context, tokens("image", "softwaresystem", "key")); + + ImageView imageView = (ImageView)context.getWorkspace().getViews().getViewWithKey("key"); + assertEquals("key", imageView.getKey()); + assertSame(softwareSystem, imageView.getElement()); + assertEquals(softwareSystem.getId(), imageView.getElementId()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java new file mode 100644 index 000000000..25f7d194b --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java @@ -0,0 +1,179 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ImplicitRelationshipParserTests extends AbstractTests { + + private ImplicitRelationshipParser parser = new ImplicitRelationshipParser(); + + private ModelItemDslContext context(Person person) { + ModelItemDslContext context = new PersonDslContext(person); + context.setWorkspace(workspace); + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + return context; + } + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(null), tokens("->", "destination", "description", "technology", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: -> <identifier> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { + try { + parser.parse(context(null), tokens("->")); + fail(); + } catch (Exception e) { + assertEquals("Expected: -> <identifier> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { + Person user = model.addPerson("User", "Description"); + ModelItemDslContext context = context(user); + IdentifiersRegister elements = new IdentifiersRegister(); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("->", "destination")); + fail(); + } catch (Exception e) { + assertEquals("The destination element \"destination\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_AddsTheRelationship() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + ModelItemDslContext context = context(user); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("->", "destination")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("", r.getDescription()); + assertEquals("", r.getTechnology()); + assertEquals("Relationship", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationshipWithADescription() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + ModelItemDslContext context = context(user); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("->", "destination", "Uses")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("", r.getTechnology()); + assertEquals("Relationship", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationshipWithADescriptionAndTechnology() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + ModelItemDslContext context = context(user); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("->", "destination", "Uses", "HTTP")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + } + + @Test + void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTags() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + ModelItemDslContext context = context(user); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + assertEquals("Relationship,Tag 1,Tag 2", r.getTags()); + } + + @Test + void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTechnologyAndTags() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ModelItemDslContext context = context(user); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("destination", container); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + assertEquals(2, model.getRelationships().size()); + + // this is the relationship that was created + Relationship r = user.getEfferentRelationshipWith(container); + assertSame(user, r.getSource()); + assertSame(container, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + assertEquals("Relationship,Tag 1,Tag 2", r.getTags()); + + // and this is an implied relationship + r = user.getEfferentRelationshipWith(softwareSystem); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); + assertEquals("HTTP", r.getTechnology()); + assertEquals("", r.getTags()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java new file mode 100644 index 000000000..a8e4e8585 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java @@ -0,0 +1,46 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ImpliedRelationshipsParserTests extends AbstractTests { + + private ImpliedRelationshipsParser parser = new ImpliedRelationshipsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!impliedRelationships", "boolean", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !impliedRelationships <true|false>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoFlagIsSpecified() { + try { + parser.parse(context(), tokens("!impliedRelationships")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !impliedRelationships <true|false>", e.getMessage()); + } + } + + @Test + void test_parse_SetsTheStrategy_WhenFalseIsSpecified() { + parser.parse(context(), tokens("!impliedRelationships", "false")); + + assertEquals("com.structurizr.model.DefaultImpliedRelationshipsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); + } + + @Test + void test_parse_SetsTheStrategy_WhenTrueIsSpecified() { + parser.parse(context(), tokens("!impliedRelationships", "true")); + + assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java new file mode 100644 index 000000000..cc5cad518 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java @@ -0,0 +1,32 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class IncludeParserTests extends AbstractTests { + + private IncludeParser parser = new IncludeParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new IncludedDslContext(null), tokens("!include", "file", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !include <file|directory|url>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenAFileIsNotSpecified() { + try { + parser.parse(new IncludedDslContext(null), tokens("!include")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !include <file|directory|url>", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java new file mode 100644 index 000000000..9c0aa769c --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java @@ -0,0 +1,134 @@ +package com.structurizr.dsl; + +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.InfrastructureNode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class InfrastructureNodeParserTests extends AbstractTests { + + private InfrastructureNodeParser parser = new InfrastructureNodeParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("infrastructureNode", "name", "description", "technology", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: infrastructureNode <name> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("infrastructureNode")); + fail(); + } catch (Exception e) { + assertEquals("Expected: infrastructureNode <name> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAnInfrastructureNode() { + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + + parser.parse(context, tokens("infrastructureNode", "Name")); + + assertEquals(2, model.getElements().size()); + assertEquals(1, deploymentNode.getInfrastructureNodes().size()); + InfrastructureNode infrastructureNode = deploymentNode.getInfrastructureNodeWithName("Name"); + assertNotNull(infrastructureNode); + assertEquals("", infrastructureNode.getDescription()); + assertEquals("", infrastructureNode.getTechnology()); + assertEquals("Element,Infrastructure Node", infrastructureNode.getTags()); + assertEquals("Live", infrastructureNode.getEnvironment()); + } + + @Test + void test_parse_CreatesAnInfrastructureNodeWithADescription() { + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + + parser.parse(context, tokens("infrastructureNode", "Name", "Description")); + + assertEquals(2, model.getElements().size()); + assertEquals(1, deploymentNode.getInfrastructureNodes().size()); + InfrastructureNode infrastructureNode = deploymentNode.getInfrastructureNodeWithName("Name"); + assertNotNull(infrastructureNode); + assertEquals("Description", infrastructureNode.getDescription()); + assertEquals("", infrastructureNode.getTechnology()); + assertEquals("Element,Infrastructure Node", infrastructureNode.getTags()); + assertEquals("Live", infrastructureNode.getEnvironment()); + } + + @Test + void test_parse_CreatesAnInfrastructureNodeWithADescriptionAndTechnology() { + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + + parser.parse(context, tokens("infrastructureNode", "Name", "Description", "Technology")); + + assertEquals(2, model.getElements().size()); + assertEquals(1, deploymentNode.getInfrastructureNodes().size()); + InfrastructureNode infrastructureNode = deploymentNode.getInfrastructureNodeWithName("Name"); + assertNotNull(infrastructureNode); + assertEquals("Description", infrastructureNode.getDescription()); + assertEquals("Technology", infrastructureNode.getTechnology()); + assertEquals("Element,Infrastructure Node", infrastructureNode.getTags()); + assertEquals("Live", infrastructureNode.getEnvironment()); + } + + @Test + void test_parse_CreatesAnInfrastructureNodeWithADescriptionAndTechnologyAndTags() { + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + + parser.parse(context, tokens("infrastructureNode", "Name", "Description", "Technology", "Tag 1, Tag 2")); + + assertEquals(2, model.getElements().size()); + assertEquals(1, deploymentNode.getInfrastructureNodes().size()); + InfrastructureNode infrastructureNode = deploymentNode.getInfrastructureNodeWithName("Name"); + assertNotNull(infrastructureNode); + assertEquals("Description", infrastructureNode.getDescription()); + assertEquals("Technology", infrastructureNode.getTechnology()); + assertEquals("Element,Infrastructure Node,Tag 1,Tag 2", infrastructureNode.getTags()); + assertEquals("Live", infrastructureNode.getEnvironment()); + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + InfrastructureNode infrastructureNode = model.addDeploymentNode("Deployment Node").addInfrastructureNode("Infrastructure Node"); + InfrastructureNodeDslContext context = new InfrastructureNodeDslContext(infrastructureNode); + parser.parseTechnology(context, tokens("technology", "technology", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + InfrastructureNode infrastructureNode = model.addDeploymentNode("Deployment Node").addInfrastructureNode("Infrastructure Node"); + InfrastructureNodeDslContext context = new InfrastructureNodeDslContext(infrastructureNode); + parser.parseTechnology(context, tokens("technology")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology <technology>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_SetsTheDescription_WhenADescriptionIsSpecified() { + InfrastructureNode infrastructureNode = model.addDeploymentNode("Deployment Node").addInfrastructureNode("Infrastructure Node"); + InfrastructureNodeDslContext context = new InfrastructureNodeDslContext(infrastructureNode); + parser.parseTechnology(context, tokens("technology", "Technology")); + + assertEquals("Technology", infrastructureNode.getTechnology()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/InlineScriptDslContextTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/InlineScriptDslContextTests.java new file mode 100644 index 000000000..6b2775318 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/InlineScriptDslContextTests.java @@ -0,0 +1,23 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class InlineScriptDslContextTests extends AbstractTests { + + @Test + void test_end_ThrowsAnException_WhenAnUnsupportedLanguageIsSpecified() { + try { + InlineScriptDslContext context = new InlineScriptDslContext(new WorkspaceDslContext(), new File("workspace.dsl"), "java"); + context.end(); + fail(); + } catch (Exception e) { + assertEquals("Error running inline script, caused by java.lang.RuntimeException: Unsupported scripting language \"java\"", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java new file mode 100644 index 000000000..08e976738 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java @@ -0,0 +1,77 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Enterprise; +import com.structurizr.model.Location; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ModelDslContextTests extends AbstractTests { + + @Test + void end_DoesNothing_WhenNoPeopleAreMarkedAsInternal() { + ModelDslContext context = new ModelDslContext(); + context.setWorkspace(workspace); + + Person user1 = workspace.getModel().addPerson("Name 1"); + Person user2 = workspace.getModel().addPerson("Name 2"); + assertEquals(Location.Unspecified, user1.getLocation()); + assertEquals(Location.Unspecified, user2.getLocation()); + + context.end(); + assertEquals(Location.Unspecified, user1.getLocation()); + assertEquals(Location.Unspecified, user2.getLocation()); + } + + @Test + void end_MarksAllOtherPeopleAsExternal_WhenSomePeopleAreMarkedAsInternal() { + ModelDslContext context = new ModelDslContext(); + context.setWorkspace(workspace); + workspace.getModel().setEnterprise(new Enterprise("Name")); + + Person user1 = workspace.getModel().addPerson("Name 1"); + Person user2 = workspace.getModel().addPerson("Name 2"); + user2.setLocation(Location.Internal); + assertEquals(Location.Unspecified, user1.getLocation()); + assertEquals(Location.Internal, user2.getLocation()); + + context.end(); + assertEquals(Location.External, user1.getLocation()); + assertEquals(Location.Internal, user2.getLocation()); + } + + @Test + void end_DoesNothing_WhenNoSoftwareSystemsAreMarkedAsInternal() { + ModelDslContext context = new ModelDslContext(); + context.setWorkspace(workspace); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Name 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Name 2"); + assertEquals(Location.Unspecified, softwareSystem1.getLocation()); + assertEquals(Location.Unspecified, softwareSystem2.getLocation()); + + context.end(); + assertEquals(Location.Unspecified, softwareSystem1.getLocation()); + assertEquals(Location.Unspecified, softwareSystem2.getLocation()); + } + + @Test + void end_MarksAllOtherSoftwareSystemsAsExternal_WhenSomeSoftwareSystemsAreMarkedAsInternal() { + ModelDslContext context = new ModelDslContext(); + context.setWorkspace(workspace); + workspace.getModel().setEnterprise(new Enterprise("Name")); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Name 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Name 2"); + softwareSystem1.setLocation(Location.Internal); + assertEquals(Location.Internal, softwareSystem1.getLocation()); + assertEquals(Location.Unspecified, softwareSystem2.getLocation()); + + context.end(); + assertEquals(Location.Internal, softwareSystem1.getLocation()); + assertEquals(Location.External, softwareSystem2.getLocation()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java new file mode 100644 index 000000000..379ce8e29 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java @@ -0,0 +1,165 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Perspective; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ModelItemParserTests extends AbstractTests { + + private ModelItemParser parser = new ModelItemParser(); + + @Test + void test_parseTags_ThrowsAnException_WhenNoTagsAreSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parseTags(context, tokens("tags")); + fail(); + } catch (Exception e) { + assertEquals("Expected: tags <tags> [tags]", e.getMessage()); + } + } + + @Test + void test_parseTags_AddsTheTags_WhenTagsAreSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parseTags(context, tokens("tags", "Tag 1")); + assertEquals(3, softwareSystem.getTagsAsSet().size()); + assertTrue(softwareSystem.getTagsAsSet().contains("Tag 1")); + + parser.parseTags(context, tokens("tags", "Tag 1, Tag 2, Tag 3")); + assertEquals(5, softwareSystem.getTagsAsSet().size()); + assertTrue(softwareSystem.getTagsAsSet().contains("Tag 2")); + assertTrue(softwareSystem.getTagsAsSet().contains("Tag 3")); + + parser.parseTags(context, tokens("tags", "Tag 3", "Tag 4", "Tag 5")); + assertEquals(7, softwareSystem.getTagsAsSet().size()); + assertTrue(softwareSystem.getTagsAsSet().contains("Tag 4")); + assertTrue(softwareSystem.getTagsAsSet().contains("Tag 5")); + } + + @Test + void test_parseDescription_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + ModelItemDslContext context = new SoftwareSystemDslContext(null); + parser.parseDescription(context, tokens("description", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: description <description>", e.getMessage()); + } + } + + @Test + void test_parseDescription_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parseDescription(context, tokens("description")); + fail(); + } catch (Exception e) { + assertEquals("Expected: description <description>", e.getMessage()); + } + } + + @Test + void test_parseDescription_SetsTheDescription_WhenADescriptionIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", ""); + ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parseDescription(context, tokens("description", "Description")); + + assertEquals("Description", softwareSystem.getDescription()); + } + + @Test + void test_parseUrl_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + ModelItemDslContext context = new SoftwareSystemDslContext(null); + parser.parseUrl(context, tokens("url", "url", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: url <url>", e.getMessage()); + } + } + + @Test + void test_parseUrl_ThrowsAnException_WhenNoUrlIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parseUrl(context, tokens("url")); + fail(); + } catch (Exception e) { + assertEquals("Expected: url <url>", e.getMessage()); + } + } + + @Test + void test_parseUrl_SetsTheUrl_WhenAUrlIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parseUrl(context, tokens("url", "http://example.com")); + + assertEquals("http://example.com", softwareSystem.getUrl()); + } + + @Test + void test_parsePerspective_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(null); + parser.parsePerspective(context, tokens("name", "description", "value", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: <name> <description> [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_ThrowsAnException_WhenNoNameIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); + parser.parsePerspective(context, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: <name> <description> [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); + parser.parsePerspective(context, tokens("name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: <name> <description> [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_AddsThePerspective_WhenADescriptionIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); + parser.parsePerspective(context, tokens("Security", "Description")); + + Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); + assertEquals("Description", perspective.getDescription()); + assertEquals("", perspective.getValue()); + } + + @Test + void test_parsePerspective_AddsThePerspective_WhenADescriptionAndValueIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); + parser.parsePerspective(context, tokens("Security", "Description", "Value")); + + Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); + assertEquals("Description", perspective.getDescription()); + assertEquals("Value", perspective.getValue()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java new file mode 100644 index 000000000..f24c1c20d --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java @@ -0,0 +1,83 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Location; +import com.structurizr.model.Person; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PersonParserTests extends AbstractTests { + + private PersonParser parser = new PersonParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("person", "name", "description", "tags", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: person <name> [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { + try { + parser.parse(context(), tokens("person")); + fail(); + } catch (Exception e) { + assertEquals("Expected: person <name> [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAPerson() { + parser.parse(context(), tokens("person", "User")); + + assertEquals(1, model.getElements().size()); + Person user = model.getPersonWithName("User"); + assertNotNull(user); + assertEquals("", user.getDescription()); + assertEquals(Location.Unspecified, user.getLocation()); + assertEquals("Element,Person", user.getTags()); + } + + @Test + void test_parse_CreatesAPersonWithADescription() { + parser.parse(context(), tokens("person", "User", "Description")); + + assertEquals(1, model.getElements().size()); + Person user = model.getPersonWithName("User"); + assertNotNull(user); + assertEquals("Description", user.getDescription()); + assertEquals(Location.Unspecified, user.getLocation()); + assertEquals("Element,Person", user.getTags()); + } + + @Test + void test_parse_CreatesAPersonWithADescriptionAndTags() { + parser.parse(context(), tokens("person", "User", "Description", "Tag 1, Tag 2")); + + assertEquals(1, model.getElements().size()); + Person user = model.getPersonWithName("User"); + assertNotNull(user); + assertEquals("Description", user.getDescription()); + assertEquals(Location.Unspecified, user.getLocation()); + assertEquals("Element,Person,Tag 1,Tag 2", user.getTags()); + } + + @Test + void test_parse_CreatesAnInternalPerson() { + EnterpriseDslContext context = new EnterpriseDslContext(); + context.setWorkspace(workspace); + parser.parse(context, tokens("person", "User")); + + assertEquals(1, model.getElements().size()); + Person user = model.getPersonWithName("User"); + assertNotNull(user); + assertEquals("", user.getDescription()); + assertEquals(Location.Internal, user.getLocation()); + assertEquals("Element,Person", user.getTags()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PluginDslContextTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PluginDslContextTests.java new file mode 100644 index 000000000..90900c706 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PluginDslContextTests.java @@ -0,0 +1,23 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class PluginDslContextTests extends AbstractTests { + + @Test + void test_end_ThrowsAnException_WhenThePluginClassDoesNotExist() { + try { + PluginDslContext context = new PluginDslContext("com.structurizr.TestPlugin", new File("src/test/dsl"), null); + context.end(); + fail(); + } catch (Exception e) { + assertEquals("Error running plugin com.structurizr.TestPlugin, caused by java.lang.ClassNotFoundException: com.structurizr.TestPlugin", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PluginParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PluginParserTests.java new file mode 100644 index 000000000..c55de300a --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PluginParserTests.java @@ -0,0 +1,38 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class PluginParserTests extends AbstractTests { + + private PluginParser parser = new PluginParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!plugin", "com.example.ClassName", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !plugin <fqn>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoFullyQualifiedNameIsSpecified() { + try { + parser.parse(context(), tokens("!plugin")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !plugin <fqn>", e.getMessage()); + } + } + + @Test + void test_parse_ReturnsTheFullyQualifiedClassName_WhenAValidPluginIsSpecified() { + String fqcn = parser.parse(context(), tokens("!plugin", "com.example.ExampleStructurizrDslPlugin")); + assertEquals("com.example.ExampleStructurizrDslPlugin", fqcn); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PropertyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PropertyParserTests.java new file mode 100644 index 000000000..33101711c --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PropertyParserTests.java @@ -0,0 +1,31 @@ +package com.structurizr.dsl; + +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PropertyParserTests extends AbstractTests { + + @Test + void test_parseProperty_ThrowsAnException_WhenNoValueIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PropertiesDslContext context = new PropertiesDslContext(softwareSystem); + new PropertyParser().parse(context, tokens("name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: <name> <value>", e.getMessage()); + } + } + + @Test + void test_parseProperty_AddsTheProperty_WhenAValueIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PropertiesDslContext context = new PropertiesDslContext(softwareSystem); + new PropertyParser().parse(context, tokens("name", "value")); + + assertEquals("value", softwareSystem.getProperties().get("name")); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java new file mode 100644 index 000000000..10e0cf947 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java @@ -0,0 +1,79 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Person; +import com.structurizr.model.Relationship; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RefParserTests extends AbstractTests { + + private RefParser parser = new RefParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!ref", "name", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !ref <identifier|canonical name>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierOrCanonicalNameIsNotSpecified() { + try { + parser.parse(context(), tokens("!extend")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !extend <identifier|canonical name>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheReferencedElementCannotBeFound() { + try { + parser.parse(context(), tokens("!ref", "Person://User")); + fail(); + } catch (Exception e) { + assertEquals("An element/relationship identified by \"Person://User\" could not be found", e.getMessage()); + } + } + + @Test + void test_parse_FindsAnElementByCanonicalName() { + Person user = workspace.getModel().addPerson("User"); + ModelItem element = parser.parse(context(), tokens("!ref", "Person://User")); + + assertSame(user, element); + } + + @Test + void test_parse_FindsAnElementByIdentifier() { + Person user = workspace.getModel().addPerson("User"); + + ModelDslContext context = context(); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("user", user); + context.setIdentifierRegister(register); + + ModelItem modelItem = parser.parse(context, tokens("!ref", "user")); + assertSame(modelItem, user); + } + + @Test + void test_parse_FindsARelationshipByIdentifier() { + Person user = workspace.getModel().addPerson("User"); + Relationship relationship = user.interactsWith(user, "Description"); + + ModelDslContext context = context(); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("rel", relationship); + context.setIdentifierRegister(register); + + ModelItem modelItem = parser.parse(context, tokens("!ref", "rel")); + assertSame(modelItem, relationship); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java new file mode 100644 index 000000000..8736c5acb --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java @@ -0,0 +1,392 @@ +package com.structurizr.dsl; + +import com.structurizr.view.Border; +import com.structurizr.view.LineStyle; +import com.structurizr.view.RelationshipStyle; +import com.structurizr.view.Routing; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RelationshipStyleParserTests extends AbstractTests { + + private RelationshipStyleParser parser = new RelationshipStyleParser(); + private RelationshipStyle relationshipStyle; + + private RelationshipStyleDslContext relationshipStyleDslContext() { + relationshipStyle = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag"); + RelationshipStyleDslContext context = new RelationshipStyleDslContext(relationshipStyle); + context.setWorkspace(workspace); + + return context; + } + + @Test + void test_parseRelationshipStyle_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseRelationshipStyle(context(), tokens("relationship", "tag", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: relationship <tag> {", e.getMessage()); + } + } + + @Test + void test_parseRelationshipStyle_ThrowsAnException_WhenTheTagIsMissing() { + try { + parser.parseRelationshipStyle(context(), tokens("relationship")); + fail(); + } catch (Exception e) { + assertEquals("Expected: relationship <tag> {", e.getMessage()); + } + } + + @Test + void test_parseRelationshipStyle_ThrowsAnException_WhenTheTagIsEmpty() { + try { + parser.parseRelationshipStyle(context(), tokens("relationship", "")); + fail(); + } catch (Exception e) { + assertEquals("A tag must be specified", e.getMessage()); + } + } + + @Test + void test_parseRelationshipStyle_CreatesAnRelationshipStyle() { + parser.parseRelationshipStyle(context(), tokens("relationship", "Relationship")); + + RelationshipStyle style = workspace.getViews().getConfiguration().getStyles().getRelationships().stream().filter(es -> "Relationship".equals(es.getTag())).findFirst().get(); + assertNotNull(style); + } + + @Test + void test_parseRelationshipStyle_FindsAnExistingRelationshipStyle() { + RelationshipStyle style = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag"); + assertSame(style, parser.parseRelationshipStyle(context(), tokens("relationship", "Tag"))); + } + + @Test + void test_parseThickness_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseThickness(relationshipStyleDslContext(), tokens("thickness", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: thickness <number>", e.getMessage()); + } + } + + @Test + void test_parseThickness_ThrowsAnException_WhenTheThicknessIsMissing() { + try { + parser.parseThickness(relationshipStyleDslContext(), tokens("thickness")); + fail(); + } catch (Exception e) { + assertEquals("Expected: thickness <number>", e.getMessage()); + } + } + + @Test + void test_parseThickness_ThrowsAnException_WhenTheThicknessIsNotValid() { + try { + parser.parseThickness(relationshipStyleDslContext(), tokens("thickness", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Thickness must be a positive integer", e.getMessage()); + } + } + + @Test + void test_parseThickness_SetsTheThickness() { + parser.parseThickness(relationshipStyleDslContext(), tokens("thickness", "75")); + assertEquals(75, relationshipStyle.getThickness()); + } + + @Test + void test_parseColour_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseColour(relationshipStyleDslContext(), tokens("colour", "hex", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: colour <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseColour_ThrowsAnException_WhenTheColourIsMissing() { + try { + parser.parseColour(relationshipStyleDslContext(), tokens("colour")); + fail(); + } catch (Exception e) { + assertEquals("Expected: colour <#rrggbb|color name>", e.getMessage()); + } + } + + @Test + void test_parseColour_SetsTheColourWhenUsingAHexColourCode() { + parser.parseColour(relationshipStyleDslContext(), tokens("colour", "#ff0000")); + assertEquals("#ff0000", relationshipStyle.getColor()); + } + + @Test + void test_parseColour_SetsTheColourWhenUsingAColourName() { + parser.parseColour(relationshipStyleDslContext(), tokens("colour", "yellow")); + assertEquals("#ffff00", relationshipStyle.getColor()); + } + + @Test + void test_parseOpacity_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseOpacity(relationshipStyleDslContext(), tokens("opacity", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: opacity <0-100>", e.getMessage()); + } + } + + @Test + void test_parseOpacity_ThrowsAnException_WhenTheOpacityIsMissing() { + try { + parser.parseOpacity(relationshipStyleDslContext(), tokens("opacity")); + fail(); + } catch (Exception e) { + assertEquals("Expected: opacity <0-100>", e.getMessage()); + } + } + + @Test + void test_parseOpacity_ThrowsAnException_WhenTheOpacityIsNotValid() { + try { + parser.parseOpacity(relationshipStyleDslContext(), tokens("opacity", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Opacity must be an integer between 0 and 100", e.getMessage()); + } + } + + @Test + void test_parseOpacity_SetsTheOpacity() { + parser.parseOpacity(relationshipStyleDslContext(), tokens("opacity", "75")); + assertEquals(75, relationshipStyle.getOpacity()); + } + + @Test + void test_parseWidth_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseWidth(relationshipStyleDslContext(), tokens("width", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: width <number>", e.getMessage()); + } + } + + @Test + void test_parseWidth_ThrowsAnException_WhenTheWidthIsMissing() { + try { + parser.parseWidth(relationshipStyleDslContext(), tokens("width")); + fail(); + } catch (Exception e) { + assertEquals("Expected: width <number>", e.getMessage()); + } + } + + @Test + void test_parseWidth_ThrowsAnException_WhenTheWidthIsNotValid() { + try { + parser.parseWidth(relationshipStyleDslContext(), tokens("width", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Width must be a positive integer", e.getMessage()); + } + } + + @Test + void test_parseWidth_SetsTheWidth() { + parser.parseWidth(relationshipStyleDslContext(), tokens("width", "75")); + assertEquals(75, relationshipStyle.getWidth()); + } + + @Test + void test_parseFontSize_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseFontSize(relationshipStyleDslContext(), tokens("fontSize", "number", "extrta")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: fontSize <number>", e.getMessage()); + } + } + + @Test + void test_parseFontSize_ThrowsAnException_WhenTheFontSizeIsMissing() { + try { + parser.parseFontSize(relationshipStyleDslContext(), tokens("fontSize")); + fail(); + } catch (Exception e) { + assertEquals("Expected: fontSize <number>", e.getMessage()); + } + } + + @Test + void test_parseFontSize_ThrowsAnException_WhenTheFontSizeIsNotValid() { + try { + parser.parseFontSize(relationshipStyleDslContext(), tokens("fontSize", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Font size must be a positive integer", e.getMessage()); + } + } + + @Test + void test_parseFontSize_SetsTheFontSize() { + parser.parseFontSize(relationshipStyleDslContext(), tokens("fontSize", "75")); + assertEquals(75, relationshipStyle.getFontSize()); + } + + @Test + void test_parseDashed_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseDashed(relationshipStyleDslContext(), tokens("dashed", "boolean", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: dashed <true|false>", e.getMessage()); + } + } + + @Test + void test_parseDashed_ThrowsAnException_WhenTheDashedIsMissing() { + try { + parser.parseDashed(relationshipStyleDslContext(), tokens("dashed")); + fail(); + } catch (Exception e) { + assertEquals("Expected: dashed <true|false>", e.getMessage()); + } + } + + @Test + void test_parseDashed_ThrowsAnException_WhenTheDashedIsNotValid() { + try { + parser.parseDashed(relationshipStyleDslContext(), tokens("dashed", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Dashed must be true or false", e.getMessage()); + } + } + + @Test + void test_parseDashed_SetsTheDashed() { + RelationshipStyleDslContext context = relationshipStyleDslContext(); + parser.parseDashed(context, tokens("dashed", "false")); + assertEquals(false, relationshipStyle.getDashed()); + + parser.parseDashed(context, tokens("dashed", "true")); + assertEquals(true, relationshipStyle.getDashed()); + } + + @Test + void test_parseLineStyle_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseLineStyle(relationshipStyleDslContext(), tokens("style", "solid", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: style <solid|dashed|dotted>", e.getMessage()); + } + } + + @Test + void test_parseLineStyle_ThrowsAnException_WhenTheStyleIsMissing() { + try { + parser.parseLineStyle(relationshipStyleDslContext(), tokens("style")); + fail(); + } catch (Exception e) { + assertEquals("Expected: style <solid|dashed|dotted>", e.getMessage()); + } + } + + @Test + void test_parseLineStyle_ThrowsAnException_WhenTheStyleIsNotValid() { + try { + parser.parseLineStyle(relationshipStyleDslContext(), tokens("style", "none")); + fail(); + } catch (Exception e) { + assertEquals("The line style \"none\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseLineStyle_SetsTheStyle() { + parser.parseLineStyle(relationshipStyleDslContext(), tokens("style", "dotted")); + assertEquals(LineStyle.Dotted, relationshipStyle.getStyle()); + } + + @Test + void test_parsePosition_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parsePosition(relationshipStyleDslContext(), tokens("position", "number", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: position <0-100>", e.getMessage()); + } + } + + @Test + void test_parsePosition_ThrowsAnException_WhenThePositionIsMissing() { + try { + parser.parsePosition(relationshipStyleDslContext(), tokens("position")); + fail(); + } catch (Exception e) { + assertEquals("Expected: position <0-100>", e.getMessage()); + } + } + + @Test + void test_parsePosition_ThrowsAnException_WhenThePositionIsNotValid() { + try { + parser.parsePosition(relationshipStyleDslContext(), tokens("position", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Position must be an integer between 0 and 100", e.getMessage()); + } + } + + @Test + void test_parsePosition_SetsThePosition() { + parser.parsePosition(relationshipStyleDslContext(), tokens("position", "75")); + assertEquals(75, relationshipStyle.getPosition()); + } + + @Test + void test_parseRouting_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseRouting(relationshipStyleDslContext(), tokens("routing", "enum", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: routing <direct|orthogonal|curved>", e.getMessage()); + } + } + + @Test + void test_parseRouting_ThrowsAnException_WhenTheRoutingIsMissing() { + try { + parser.parseRouting(relationshipStyleDslContext(), tokens("routing")); + fail(); + } catch (Exception e) { + assertEquals("Expected: routing <direct|orthogonal|curved>", e.getMessage()); + } + } + + @Test + void test_parseRouting_ThrowsAnException_WhenTheRoutingIsNotValid() { + try { + parser.parseRouting(relationshipStyleDslContext(), tokens("routing", "rounded")); + fail(); + } catch (Exception e) { + assertEquals("The routing \"rounded\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseRouting_SetsTheRouting() { + parser.parseRouting(relationshipStyleDslContext(), tokens("routing", "curved")); + assertEquals(Routing.Curved, relationshipStyle.getRouting()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ScriptParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ScriptParserTests.java new file mode 100644 index 000000000..a7627f7af --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ScriptParserTests.java @@ -0,0 +1,34 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class ScriptParserTests extends AbstractTests { + + private ScriptParser parser = new ScriptParser(); + + @Test + void test_parseExternal_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseExternal(tokens("!script", "test.kts", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !script <filename>", e.getMessage()); + } + } + + @Test + void test_parseExternal_ThrowsAnException_WhenNoFilenameIsSpecified() { + try { + parser.parseExternal(tokens("!script")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !script <filename>", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java new file mode 100644 index 000000000..1d0b7e278 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java @@ -0,0 +1,143 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SoftwareSystemInstanceParserTests extends AbstractTests { + + private SoftwareSystemInstanceParser parser = new SoftwareSystemInstanceParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("softwareSystemInstance", "identifier", "deploymentGroups", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: softwareSystemInstance <identifier> [deploymentGroups] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierIsNotSpecified() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("softwareSystemInstance")); + fail(); + } catch (Exception e) { + assertEquals("Expected: softwareSystemInstance <identifier> [deploymentGroups] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementDoesNotExist() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("softwareSystemInstance", "softwareSystem")); + fail(); + } catch (Exception e) { + assertEquals("The software system \"softwareSystem\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotASoftwareSystem() { + DeploymentNodeDslContext context = new DeploymentNodeDslContext(null); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("softwareSystemInstance", "softwareSystem")); + fail(); + } catch (Exception e) { + assertEquals("The software system \"softwareSystem\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_CreatesASoftwareSystemInstanceInTheDefaultDeploymentGroup() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("softwareSystemInstance", "softwareSystem")); + + assertEquals(3, model.getElements().size()); + assertEquals(1, deploymentNode.getSoftwareSystemInstances().size()); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.getSoftwareSystemInstances().iterator().next(); + assertSame(softwareSystem, softwareSystemInstance.getSoftwareSystem()); + assertEquals("Software System Instance", softwareSystemInstance.getTags()); + assertEquals("Live", softwareSystemInstance.getEnvironment()); + assertEquals(1, softwareSystemInstance.getDeploymentGroups().size()); + assertEquals("Default", softwareSystemInstance.getDeploymentGroups().iterator().next()); + } + + @Test + void test_parse_CreatesASoftwareSystemInstanceInTheDefaultDeploymentGroupWithTags() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("softwareSystemInstance", "softwareSystem", "", "Tag 1, Tag 2")); + + assertEquals(3, model.getElements().size()); + assertEquals(1, deploymentNode.getSoftwareSystemInstances().size()); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.getSoftwareSystemInstances().iterator().next(); + assertSame(softwareSystem, softwareSystemInstance.getSoftwareSystem()); + assertEquals("Software System Instance,Tag 1,Tag 2", softwareSystemInstance.getTags()); + assertEquals("Live", softwareSystemInstance.getEnvironment()); + assertEquals(1, softwareSystemInstance.getDeploymentGroups().size()); + assertEquals("Default", softwareSystemInstance.getDeploymentGroups().iterator().next()); + } + + @Test + void test_parse_CreatesASoftwareSystemInstanceInTheSpecifiedDeploymentGroup() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", softwareSystem); + elements.register("group", new DeploymentGroup("Group")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("softwareSystemInstance", "softwareSystem", "group")); + + assertEquals(3, model.getElements().size()); + assertEquals(1, deploymentNode.getSoftwareSystemInstances().size()); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.getSoftwareSystemInstances().iterator().next(); + assertSame(softwareSystem, softwareSystemInstance.getSoftwareSystem()); + assertEquals("Software System Instance", softwareSystemInstance.getTags()); + assertEquals("Live", softwareSystemInstance.getEnvironment()); + assertEquals(1, softwareSystemInstance.getDeploymentGroups().size()); + assertEquals("Group", softwareSystemInstance.getDeploymentGroups().iterator().next()); + } + + @Test + void test_parse_CreatesASoftwareSystemInstanceInTheSpecifiedDeploymentGroupWithTags() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", softwareSystem); + elements.register("group", new DeploymentGroup("Group")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("softwareSystemInstance", "softwareSystem", "group", "Tag 1, Tag 2")); + + assertEquals(3, model.getElements().size()); + assertEquals(1, deploymentNode.getSoftwareSystemInstances().size()); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.getSoftwareSystemInstances().iterator().next(); + assertSame(softwareSystem, softwareSystemInstance.getSoftwareSystem()); + assertEquals("Software System Instance,Tag 1,Tag 2", softwareSystemInstance.getTags()); + assertEquals("Live", softwareSystemInstance.getEnvironment()); + assertEquals(1, softwareSystemInstance.getDeploymentGroups().size()); + assertEquals("Group", softwareSystemInstance.getDeploymentGroups().iterator().next()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java new file mode 100644 index 000000000..adbc2dfa8 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java @@ -0,0 +1,83 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Location; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SoftwareSystemParserTests extends AbstractTests { + + private SoftwareSystemParser parser = new SoftwareSystemParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("softwareSystem", "name", "description", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: softwareSystem <name> [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { + try { + parser.parse(context(), tokens("softwareSystem")); + fail(); + } catch (Exception e) { + assertEquals("Expected: softwareSystem <name> [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesASoftwareSystem() { + parser.parse(context(), tokens("softwareSystem", "Name")); + + assertEquals(1, model.getElements().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("", softwareSystem.getDescription()); + assertEquals(Location.Unspecified, softwareSystem.getLocation()); + assertEquals("Element,Software System", softwareSystem.getTags()); + } + + @Test + void test_parse_CreatesASoftwareSystemWithADescription() { + parser.parse(context(), tokens("softwareSystem", "Name", "Description")); + + assertEquals(1, model.getElements().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("Description", softwareSystem.getDescription()); + assertEquals(Location.Unspecified, softwareSystem.getLocation()); + assertEquals("Element,Software System", softwareSystem.getTags()); + } + + @Test + void test_parse_CreatesASoftwareSystemWithADescriptionAndTags() { + parser.parse(context(), tokens("softwareSystem", "Name", "Description", "Tag 1, Tag 2")); + + assertEquals(1, model.getElements().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("Description", softwareSystem.getDescription()); + assertEquals(Location.Unspecified, softwareSystem.getLocation()); + assertEquals("Element,Software System,Tag 1,Tag 2", softwareSystem.getTags()); + } + + @Test + void test_parse_CreatesAnInternalSoftwareSystem() { + EnterpriseDslContext context = new EnterpriseDslContext(); + context.setWorkspace(workspace); + parser.parse(context, tokens("softwareSystem", "Name")); + + assertEquals(1, model.getElements().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("", softwareSystem.getDescription()); + assertEquals(Location.Internal, softwareSystem.getLocation()); + assertEquals("Element,Software System", softwareSystem.getTags()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java new file mode 100644 index 000000000..557f1e378 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java @@ -0,0 +1,32 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class StaticViewAnimationStepParserTests extends AbstractTests { + + private StaticViewAnimationStepParser parser = new StaticViewAnimationStepParser(); + + @Test + void test_parseExplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((StaticViewDslContext)null, tokens("animationStep")); + fail(); + } catch (Exception e) { + assertEquals("Expected: animationStep <identifier> [identifier...]", e.getMessage()); + } + } + + @Test + void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((StaticViewAnimationDslContext) null, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier> [identifier...]", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java new file mode 100644 index 000000000..82d572de7 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java @@ -0,0 +1,790 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import com.structurizr.view.ComponentView; +import com.structurizr.view.SystemContextView; +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class StaticViewContentParserTests extends AbstractTests { + + private StaticViewContentParser parser = new StaticViewContentParser(); + + @Test + void test_parseInclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { + try { + parser.parseInclude(new SystemLandscapeViewDslContext(null), tokens("include")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: include <*|identifier|expression> [*|identifier|expression...]", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTheSpecifiedElementDoesNotExist() { + try { + parser.parseInclude(new SystemLandscapeViewDslContext(null), tokens("include", "user")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element/relationship \"user\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTryingToAddAContainerToASystemLandscapeView() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + context.setIdentifierRegister(elements); + + try { + parser.parseInclude(context, tokens("include", "container")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element \"container\" can not be added to this type of view", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTryingToAddAComponentToASystemLandscapeView() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + Component component = container.addComponent("Component", "Description", "Technology"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("component", component); + context.setIdentifierRegister(elements); + + try { + parser.parseInclude(context, tokens("include", "component")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element \"component\" can not be added to this type of view", iae.getMessage()); + } + } + + @Test + void test_parseInclude_AddsAllPeopleAndSoftwareSystemsToASystemLandscapeView_WhenTheWildcardIsSpecified() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + softwareSystem1.addContainer("Container 1", "Description", "Technology"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + user.uses(softwareSystem1, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "*")); + + assertEquals(3, view.getElements().size()); + assertEquals(2, view.getRelationships().size()); + } + + @Test + void test_parseInclude_AddsTheSpecifiedElementsToASystemLandscapeView() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + user.uses(softwareSystem1, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + CustomElement box1 = model.addCustomElement("Box 1"); + box1.uses(softwareSystem1, ""); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + elements.register("softwaresystem1", softwareSystem1); + elements.register("softwaresystem2", softwareSystem2); + elements.register("box1", box1); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "user", "softwareSystem1", "box1")); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(user))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(softwareSystem1))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(box1))); + assertTrue(view.getElements().stream().noneMatch(ev -> ev.getElement().equals(softwareSystem2))); + assertEquals(2, view.getRelationships().size()); + } + + @Test + void test_parseInclude_AddsNearestNeighboursToASystemContextView_WhenTheWildcardIsSpecified() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + softwareSystem1.addContainer("Container 1", "Description", "Technology"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + user.uses(softwareSystem1, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + + SystemContextView view = views.createSystemContextView(softwareSystem1, "key", "Description"); + SystemContextViewDslContext context = new SystemContextViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "*")); + + assertEquals(3, view.getElements().size()); + assertEquals(2, view.getRelationships().size()); + } + + @Test + void test_parseInclude_AddsTheSpecifiedPeopleAndSoftwareSystemsToASystemContextView() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + user.uses(softwareSystem1, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + + SystemContextView view = views.createSystemContextView(softwareSystem1, "key", "Description"); + SystemContextViewDslContext context = new SystemContextViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + elements.register("softwaresystem1", softwareSystem1); + elements.register("softwaresystem2", softwareSystem2); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "user")); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(user))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(softwareSystem1))); + assertTrue(view.getElements().stream().noneMatch(ev -> ev.getElement().equals(softwareSystem2))); + assertEquals(1, view.getRelationships().size()); + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTryingToAddAContainerToASystemContextView() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + + SystemContextView view = views.createSystemContextView(softwareSystem, "key", "Description"); + SystemContextViewDslContext context = new SystemContextViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + context.setIdentifierRegister(elements); + + try { + parser.parseInclude(context, tokens("include", "container")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element \"container\" can not be added to this type of view", iae.getMessage()); + } + } + + @Test + void test_parseInclude_ThrowsAnException_WhenTryingToAddAComponentToASystemContextView() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + Component component = container.addComponent("Component", "Description", "Technology"); + + SystemContextView view = views.createSystemContextView(softwareSystem, "key", "Description"); + SystemContextViewDslContext context = new SystemContextViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("component", component); + context.setIdentifierRegister(elements); + + try { + parser.parseInclude(context, tokens("include", "component")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element \"component\" can not be added to this type of view", iae.getMessage()); + } + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parseExclude(context, tokens("include")); + fail(); + } catch (RuntimeException iae) { + assertEquals("Expected: exclude <identifier|expression> [identifier|expression...]", iae.getMessage()); + } + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheSpecifiedElementDoesNotExist() { + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parseExclude(context, tokens("exclude", "user")); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element/relationship \"user\" does not exist", iae.getMessage()); + } + } + + @Test + void test_parseExclude_RemovesTheSpecifiedPeopleAndSoftwareSystemsFromAView() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + user.uses(softwareSystem1, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + elements.register("softwaresystem1", softwareSystem1); + elements.register("softwaresystem2", softwareSystem2); + context.setIdentifierRegister(elements); + + assertEquals(3, view.getElements().size()); + assertEquals(2, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "user", "softwaresystem1")); + + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().stream().noneMatch(ev -> ev.getElement().equals(user))); + assertTrue(view.getElements().stream().noneMatch(ev -> ev.getElement().equals(softwareSystem1))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(softwareSystem2))); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExplicitIdentifierIsSpecified() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Relationship rel = user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister identifersRegister = new IdentifiersRegister(); + identifersRegister.register("rel", rel); + context.setIdentifierRegister(identifersRegister); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "rel")); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(user))); + assertTrue(view.getElements().stream().anyMatch(ev -> ev.getElement().equals(softwareSystem))); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheRelationshipSourceElementDoesNotExistInTheModel() { + try { + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "relationship.source==user && relationship.destination==softwareSystem")); + + fail(); + } catch (RuntimeException re) { + assertEquals("The element \"user\" does not exist", re.getMessage()); + } + } + + @Test + void test_parseExclude_ThrowsAnException_WhenTheRelationshipDestinationElementDoesNotExistInTheModel() { + try { + Person user = model.addPerson("User", "Description"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.add(user); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + context.setIdentifierRegister(elements); + + parser.parseExclude(context, tokens("exclude", "relationship.source==user && relationship.destination==softwareSystem")); + + fail(); + } catch (RuntimeException re) { + assertEquals("The element \"softwareSystem\" does not exist", re.getMessage()); + } + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithSourceAndDestination() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship.source==user && relationship.destination==softwareSystem")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithSourceAndWildcard() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship.source==user")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithWildcardAndDestination() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship.destination==softwareSystem")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseExclude_RemovesTheRelationshipFromAView_WhenAnExpressionIsSpecifiedWithWildcardAndWildcard() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("user", user); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + parser.parseExclude(context, tokens("exclude", "relationship==*")); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void test_parseInclude_AddsAllElementsWithTheSpecifiedTag() { + Person user = model.addPerson("User"); + user.addTags("Tag 1"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container 1"); + Component component = container.addComponent("Component"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(1, view.getElements().size()); + assertNotNull(view.getElementView(user)); + } + + @Test + void test_parseInclude_AddsAllElementsWithTheSpecifiedTags() { + Person user = model.addPerson("User"); + user.addTags("Tag 1", "Tag 2"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + softwareSystem.addTags("Tag 1"); + Container container = softwareSystem.addContainer("Container 1"); + Component component = container.addComponent("Component"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1,Tag 2")); + + assertEquals(1, view.getElements().size()); + assertNotNull(view.getElementView(user)); + } + + @Test + void test_parseInclude_AddsAllElementsWithTheSpecifiedTagIgnoringElementsThatAreNotPermitted() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container 1"); + Component component = container.addComponent("Component"); + + model.getElements().forEach(e -> e.addTags("Tag 1")); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + assertNull(view.getElementView(container)); // containers are not permitted on system landscape views + assertNull(view.getElementView(component)); // components are not permitted on system landscape views + } + + @Test + void test_parseInclude_AddsAllElementsWithTheSpecifiedTagsIgnoringElementsThatAreNotPermitted() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container 1"); + Component component = container.addComponent("Component"); + + model.getElements().forEach(e -> e.addTags("Tag 1", "Tag 2")); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag==Tag 1,Tag 2")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + assertNull(view.getElementView(container)); // containers are not permitted on system landscape views + assertNull(view.getElementView(component)); // components are not permitted on system landscape views + } + + @Test + void test_parseInclude_AddsAllElementsWithoutTheSpecifiedTag() { + Person user = model.addPerson("User"); + user.addTags("Tag 1"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container 1"); + Component component = container.addComponent("Component"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag!=Tag 1")); + + assertEquals(1, view.getElements().size()); + assertNotNull(view.getElementView(softwareSystem)); + } + + @Test + void test_parseInclude_AddsAllElementsWithoutTheSpecifiedTags() { + Person user = model.addPerson("User"); + user.addTags("Tag 1", "Tag 2"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + softwareSystem.addTags("Tag 1"); + Container container = softwareSystem.addContainer("Container 1"); + Component component = container.addComponent("Component"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag!=Tag 1,Tag 2")); + + assertEquals(1, view.getElements().size()); + assertNotNull(view.getElementView(softwareSystem)); + } + + @Test + void test_parseInclude_AddsAllElementsWithoutTheSpecifiedTagIgnoringElementsThatAreNotPermitted() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container 1"); + Component component = container.addComponent("Component"); + + model.getElements().forEach(e -> e.addTags("Tag 1")); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "element.tag!=Tag 2")); + + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + assertNull(view.getElementView(container)); // containers are not permitted on system landscape views + assertNull(view.getElementView(component)); // components are not permitted on system landscape views + } + + @Test + void test_parseExclude_RemovesAllElementsWithTheSpecifiedTag() { + Person user = model.addPerson("User"); + user.addTags("Tag 1"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "element.tag==Tag 1")); + + assertEquals(1, view.getElements().size()); + assertNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + } + + @Test + void test_parseExclude_RemovesAllElementsWithTheSpecifiedTags() { + Person user = model.addPerson("User"); + user.addTags("Tag 1", "Tag 2"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + softwareSystem.addTags("Tag 1"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "element.tag==Tag 1,Tag 2")); + + assertEquals(1, view.getElements().size()); + assertNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + } + + @Test + void test_parseExclude_RemovesAllElementsWithoutTheSpecifiedTag() { + Person user = model.addPerson("User"); + user.addTags("Tag 1"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "element.tag!=Tag 1")); + + assertEquals(1, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNull(view.getElementView(softwareSystem)); + } + + @Test + void test_parseExclude_RemovesAllElementsWithoutTheSpecifiedTags() { + Person user = model.addPerson("User"); + user.addTags("Tag 1", "Tag 2"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + softwareSystem.addTags("Tag 1"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + assertEquals(2, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNotNull(view.getElementView(softwareSystem)); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "element.tag!=Tag 1,Tag 2")); + + assertEquals(1, view.getElements().size()); + assertNotNull(view.getElementView(user)); + assertNull(view.getElementView(softwareSystem)); + } + + @Test + void test_parseInclude_AddsAllRelationshipsWithTheSpecifiedTag() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + Relationship relationship1 = user.uses(softwareSystem, "1"); + relationship1.addTags("Tag 1"); + Relationship relationship2 = user.uses(softwareSystem, "2"); + relationship2.addTags("Tag 2"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + view.remove(relationship1); + view.remove(relationship2); + assertEquals(0, view.getRelationships().size()); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "relationship.tag==Tag 1")); + + assertEquals(1, view.getRelationships().size()); + assertNotNull(view.getRelationshipView(relationship1)); + } + + @Test + void test_parseInclude_AddsAllRelationshipsWithTheSpecifiedTags() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + Relationship relationship1 = user.uses(softwareSystem, "1"); + relationship1.addTags("Tag 1", "Tag 2"); + Relationship relationship2 = user.uses(softwareSystem, "2"); + relationship2.addTags("Tag 2"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + view.remove(relationship1); + view.remove(relationship2); + assertEquals(0, view.getRelationships().size()); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "relationship.tag==Tag 1,Tag 2")); + + assertEquals(1, view.getRelationships().size()); + assertNotNull(view.getRelationshipView(relationship1)); + } + + @Test + void test_parseExclude_RemovesAllRelationshipsWithTheSpecifiedTag() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + Relationship relationship1 = user.uses(softwareSystem, "1"); + relationship1.addTags("Tag 1"); + Relationship relationship2 = user.uses(softwareSystem, "2"); + relationship2.addTags("Tag 2"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + assertEquals(2, view.getRelationships().size()); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "relationship.tag==Tag 1")); + + assertEquals(1, view.getRelationships().size()); + assertNull(view.getRelationshipView(relationship1)); + assertNotNull(view.getRelationshipView(relationship2)); + } + + @Test + void test_parseExclude_RemovesAllRelationshipsWithTheSpecifiedTags() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + Relationship relationship1 = user.uses(softwareSystem, "1"); + relationship1.addTags("Tag 1", "Tag 2"); + Relationship relationship2 = user.uses(softwareSystem, "2"); + relationship2.addTags("Tag 2"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + view.addAllElements(); + assertEquals(2, view.getRelationships().size()); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseExclude(context, tokens("exclude", "relationship.tag==Tag 1,Tag 2")); + + assertEquals(1, view.getRelationships().size()); + assertNull(view.getRelationshipView(relationship1)); + assertNotNull(view.getRelationshipView(relationship2)); + } + + @Test + void test_parseInclude_IncludesTheMostAbstractElementWhenEfferentCouplingExpressionUsed() { + SoftwareSystem ss1 = model.addSoftwareSystem("Software System 1"); + Container c1 = ss1.addContainer("Container 1"); + Component cc1 = c1.addComponent("Component 1"); + + SoftwareSystem ss2 = model.addSoftwareSystem("Software System 2"); + Container c2 = ss2.addContainer("Container 2"); + Component cc2 = c2.addComponent("Component 2"); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + cc1.uses(cc2, "Uses"); + + ComponentView view = views.createComponentView(c1, "key", "Description"); + ComponentViewDslContext context = new ComponentViewDslContext(view); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("cc1", cc1); + context.setIdentifierRegister(elements); + + parser.parseInclude(context, tokens("include", "cc1->")); + + assertEquals(2, view.getElements().size()); + assertTrue(view.isElementInView(cc1)); + assertTrue(view.isElementInView(ss2)); // this is the software system, not the component + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewExpressionParserTests.java new file mode 100644 index 000000000..5c4a45e38 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewExpressionParserTests.java @@ -0,0 +1,199 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class StaticViewExpressionParserTests extends AbstractTests { + + private StaticViewExpressionParser parser = new StaticViewExpressionParser(); + + @Test + void test_parseExpression_ThrowsAnException_WhenElementTypeIsNotSupported() { + try { + parser.parseExpression("element.type==DeploymentNode", null); + fail(); + } catch (RuntimeException iae) { + assertEquals("The element type of \"DeploymentNode\" is not valid for this view", iae.getMessage()); + } + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsPerson() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==Person", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(user)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsSoftwareSystem() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==SoftwareSystem", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(softwareSystem)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsContainer() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==Container", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(container)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementTypeEqualsComponent() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.type==Component", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(component)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementHasTag() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Container", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(container)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenElementDoesNotHaveTag() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag!=Container", context); + assertEquals(3, elements.size()); + assertFalse(elements.contains(container)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenBooleanAndUsed() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Element && element.type==Container", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(container)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenBooleanOrUsed() { + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + Set<ModelItem> elements = parser.parseExpression("element.tag==Container || element.type==Component", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(container)); + assertTrue(elements.contains(component)); + } + + @Test + void test_parseExpression_ReturnsElements_ForAfferentCouplings() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component2 = container2.addComponent("Component 2"); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + component1.uses(component2, "Uses"); + + ComponentViewDslContext context = new ComponentViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("container1", container1); + context.setIdentifierRegister(map); + + Set<ModelItem> elements = parser.parseExpression("container1->", context); + assertEquals(4, elements.size()); + assertTrue(elements.contains(container1)); + assertTrue(elements.contains(softwareSystem2)); + assertTrue(elements.contains(container2)); + assertTrue(elements.contains(component2)); + } + + @Test + void test_parseExpression_ReturnsElements_ForAfferentCouplingsOfType() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component2 = container2.addComponent("Component 2"); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + component1.uses(component2, "Uses"); + + ComponentViewDslContext context = new ComponentViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("container1", container1); + context.setIdentifierRegister(map); + + Set<ModelItem> elements = parser.parseExpression("container1-> && element.type==Container", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(container1)); + assertTrue(elements.contains(container2)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SystemContextViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SystemContextViewParserTests.java new file mode 100644 index 000000000..0b04ce40f --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SystemContextViewParserTests.java @@ -0,0 +1,109 @@ +package com.structurizr.dsl; + +import com.structurizr.view.SystemContextView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class SystemContextViewParserTests extends AbstractTests { + + private SystemContextViewParser parser = new SystemContextViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("systemContext", "identifier", "key", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: systemContext <software system identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheSoftwareSystemIdentifierIsMissing() { + DslContext context = context(); + try { + parser.parse(context, tokens("systemContext")); + fail(); + } catch (Exception e) { + assertEquals("Expected: systemContext <software system identifier> [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheSoftwareSystemIsNotDefined() { + DslContext context = context(); + try { + parser.parse(context, tokens("systemContext", "softwareSystem", "key")); + fail(); + } catch (Exception e) { + assertEquals("The software system \"softwareSystem\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotASoftwareSystem() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("systemContext", "softwareSystem", "key")); + fail(); + } catch (Exception e) { + assertEquals("The element \"softwareSystem\" is not a software system", e.getMessage()); + } + } + + @Test + void test_parse_CreatesASystemContextView() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addSoftwareSystem("Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("systemContext", "softwareSystem")); + List<SystemContextView> views = new ArrayList<>(context.getWorkspace().getViews().getSystemContextViews()); + + assertEquals(1, views.size()); + assertEquals("SystemContext-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesASystemContextViewWithAKey() { + DslContext context = context(); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addSoftwareSystem("Name", "Description")); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("systemContext", "softwareSystem", "key")); + List<SystemContextView> views = new ArrayList<>(context.getWorkspace().getViews().getSystemContextViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesASystemContextViewWithAKeyAndDescription() { + DslContext context = context(); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("softwaresystem", model.addSoftwareSystem("Name", "Description")); + context.setIdentifierRegister(register); + + parser.parse(context, tokens("systemContext", "softwareSystem", "key", "Description")); + List<SystemContextView> views = new ArrayList<>(context.getWorkspace().getViews().getSystemContextViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("Description", views.get(0).getDescription()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SystemLandscapeViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SystemLandscapeViewParserTests.java new file mode 100644 index 000000000..5b00923cf --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SystemLandscapeViewParserTests.java @@ -0,0 +1,60 @@ +package com.structurizr.dsl; + +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class SystemLandscapeViewParserTests extends AbstractTests { + + private SystemLandscapeViewParser parser = new SystemLandscapeViewParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + DslContext context = context(); + try { + parser.parse(context, tokens("systemLandscape", "key", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: systemLandscape [key] [description] {", e.getMessage()); + } + } + + @Test + void test_parse_CreatesASystemLandscapeView() { + DslContext context = context(); + parser.parse(context, tokens("systemLandscape")); + List<SystemLandscapeView> views = new ArrayList<>(context.getWorkspace().getViews().getSystemLandscapeViews()); + + assertEquals(1, views.size()); + assertEquals("SystemLandscape-001", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesASystemLandscapeViewWithAKey() { + DslContext context = context(); + parser.parse(context, tokens("systemLandscape", "key")); + List<SystemLandscapeView> views = new ArrayList<>(context.getWorkspace().getViews().getSystemLandscapeViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("", views.get(0).getDescription()); + } + + @Test + void test_parse_CreatesASystemLandscapeViewWithAKeyAndDescription() { + DslContext context = context(); + parser.parse(context, tokens("systemLandscape", "key", "description")); + List<SystemLandscapeView> views = new ArrayList<>(context.getWorkspace().getViews().getSystemLandscapeViews()); + + assertEquals(1, views.size()); + assertEquals("key", views.get(0).getKey()); + assertEquals("description", views.get(0).getDescription()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java new file mode 100644 index 000000000..dbff12115 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java @@ -0,0 +1,148 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class TerminologyParserTests extends AbstractTests { + + private TerminologyParser parser = new TerminologyParser(); + + @Test + void test_parseEnterprise_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parseEnterprise(context(), tokens("enterprise")); + fail(); + } catch (Exception e) { + assertEquals("Expected: enterprise <term>", e.getMessage()); + } + } + + @Test + void test_parseEnterprise_SetsTheTerm_WhenOneIsSpecified() { + parser.parseEnterprise(context(), tokens("enterprise", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getEnterprise()); + } + + @Test + void test_parsePerson_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parsePerson(context(), tokens("person")); + fail(); + } catch (Exception e) { + assertEquals("Expected: person <term>", e.getMessage()); + } + } + + @Test + void test_parsePerson_SetsTheTerm_WhenOneIsSpecified() { + parser.parsePerson(context(), tokens("person", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getPerson()); + } + + @Test + void test_parseSoftwareSystem_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parseSoftwareSystem(context(), tokens("softwareSystem")); + fail(); + } catch (Exception e) { + assertEquals("Expected: softwareSystem <term>", e.getMessage()); + } + } + + @Test + void test_parseSoftwareSystem_SetsTheTerm_WhenOneIsSpecified() { + parser.parseSoftwareSystem(context(), tokens("softwareSystem", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getSoftwareSystem()); + } + + @Test + void test_parseContainer_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parseContainer(context(), tokens("container")); + fail(); + } catch (Exception e) { + assertEquals("Expected: container <term>", e.getMessage()); + } + } + + @Test + void test_parseContainer_SetsTheTerm_WhenOneIsSpecified() { + parser.parseContainer(context(), tokens("container", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getContainer()); + } + + @Test + void test_parseComponent_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parseComponent(context(), tokens("component")); + fail(); + } catch (Exception e) { + assertEquals("Expected: component <term>", e.getMessage()); + } + } + + @Test + void test_parseComponent_SetsTheTerm_WhenOneIsSpecified() { + parser.parseComponent(context(), tokens("component", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getComponent()); + } + + @Test + void test_parsedeploymentNode_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parseDeploymentNode(context(), tokens("deploymentNode")); + fail(); + } catch (Exception e) { + assertEquals("Expected: deploymentNode <term>", e.getMessage()); + } + } + + @Test + void test_parsedeploymentNode_SetsTheTerm_WhenOneIsSpecified() { + parser.parseDeploymentNode(context(), tokens("deploymentNode", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getDeploymentNode()); + } + + @Test + void test_parseInfrastructureNode_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parseInfrastructureNode(context(), tokens("infrastructureNode")); + fail(); + } catch (Exception e) { + assertEquals("Expected: infrastructureNode <term>", e.getMessage()); + } + } + + @Test + void test_parseInfrastructureNode_SetsTheTerm_WhenOneIsSpecified() { + parser.parseInfrastructureNode(context(), tokens("infrastructureNode", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getInfrastructureNode()); + } + + @Test + void test_parseRelationship_ThrowsAnException_WhenNoTermIsSpecified() { + try { + parser.parseRelationship(context(), tokens("relationship")); + fail(); + } catch (Exception e) { + assertEquals("Expected: relationship <term>", e.getMessage()); + } + } + + @Test + void test_parseRelationship_SetsTheTerm_WhenOneIsSpecified() { + parser.parseRelationship(context(), tokens("relationship", "TERM")); + + assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getRelationship()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java new file mode 100644 index 000000000..46463d603 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java @@ -0,0 +1,84 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ThemeParserTests extends AbstractTests { + + private ThemeParser parser = new ThemeParser(); + + @Test + void test_parseTheme_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseTheme(context(), tokens("theme", "url", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: theme <url>", e.getMessage()); + } + } + + @Test + void test_parseTheme_ThrowsAnException_WhenNoThemeIsSpecified() { + try { + parser.parseTheme(context(), tokens("theme")); + fail(); + } catch (Exception e) { + assertEquals("Expected: theme <url>", e.getMessage()); + } + } + + @Test + void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { + parser.parseTheme(context(), tokens("theme", "http://example.com/1")); + + assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); + assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); + } + + @Test + void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { + parser.parseTheme(context(), tokens("theme", "default")); + + assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); + assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); + } + + @Test + void test_parseThemes_ThrowsAnException_WhenNoThemesAreSpecified() { + try { + parser.parseThemes(context(), tokens("themes")); + fail(); + } catch (Exception e) { + assertEquals("Expected: themes <url> [url] ... [url]", e.getMessage()); + } + } + + @Test + void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { + parser.parseThemes(context(), tokens("themes", "http://example.com/1")); + + assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); + assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); + } + + @Test + void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { + parser.parseThemes(context(), tokens("themes", "http://example.com/1", "http://example.com/2", "http://example.com/3")); + + assertEquals(3, workspace.getViews().getConfiguration().getThemes().length); + assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); + assertEquals("http://example.com/2", workspace.getViews().getConfiguration().getThemes()[1]); + assertEquals("http://example.com/3", workspace.getViews().getConfiguration().getThemes()[2]); + } + + @Test + void test_parseThemes_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { + parser.parseThemes(context(), tokens("themes", "default")); + + assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); + assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/TokenizerTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/TokenizerTests.java new file mode 100644 index 000000000..71b316836 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/TokenizerTests.java @@ -0,0 +1,71 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Location; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TokenizerTests extends AbstractTests { + + @Test + void tokenize_ReturnsASingleToken_WhenTheLineIsOneUnquotedToken() { + List<String> tokens = new Tokenizer().tokenize("Hello"); + assertEquals(1, tokens.size()); + assertEquals("Hello", tokens.get(0)); + } + + @Test + void tokenize_ReturnsASingleToken_WhenTheLineIsOneUnquotedTokenWithEscapedQuoteCharacters() { + List<String> tokens = new Tokenizer().tokenize("\"Hello \\\"World\\\"\""); + assertEquals(1, tokens.size()); + assertEquals("Hello \\\"World\\\"", tokens.get(0)); + } + + @Test + void tokenize_ReturnsASingleToken_WhenTheLineIsTwoUnquotedTokens() { + List<String> tokens = new Tokenizer().tokenize("Hello World"); + assertEquals(2, tokens.size()); + assertEquals("Hello", tokens.get(0)); + assertEquals("World", tokens.get(1)); + } + + @Test + void tokenize_ReturnsTwoTokens_WhenTheLineIsOneQuotedToken() { + List<String> tokens = new Tokenizer().tokenize("\"Hello World\""); + assertEquals(1, tokens.size()); + assertEquals("Hello World", tokens.get(0)); + } + + @Test + void tokenize_ReturnsTwoTokens_WhenTheLineIsTwoQuotedTokens() { + List<String> tokens = new Tokenizer().tokenize("\"Hello\" \"World\""); + assertEquals(2, tokens.size()); + assertEquals("Hello", tokens.get(0)); + assertEquals("World", tokens.get(1)); + } + + @Test + void tokenize_ReturnsTokens_WhenTheLineIsSeveralQuotedTokens() { + List<String> tokens = new Tokenizer().tokenize("user = person \"User\""); + assertEquals(4, tokens.size()); + assertEquals("user", tokens.get(0)); + assertEquals("=", tokens.get(1)); + assertEquals("person", tokens.get(2)); + assertEquals("User", tokens.get(3)); + } + + @Test + void tokenize_ReturnsASingleToken_WhenTheLineIncludesTabCharacters() { + List<String> tokens = new Tokenizer().tokenize("\t\tuser\t=\tperson\t\"User\""); + assertEquals(4, tokens.size()); + assertEquals("user", tokens.get(0)); + assertEquals("=", tokens.get(1)); + assertEquals("person", tokens.get(2)); + assertEquals("User", tokens.get(3)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/TokensTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/TokensTests.java new file mode 100644 index 000000000..0b03ba3c1 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/TokensTests.java @@ -0,0 +1,23 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TokensTests { + + @Test + void test_get_HandlesEscapedDoubleQuotes() { + Tokens tokens = new Tokens(Collections.singletonList("Hello \\\"World\\\"")); + assertEquals("Hello \"World\"", tokens.get(0)); + } + + @Test + void test_get_HandlesEscapedNewlines() { + Tokens tokens = new Tokens(Collections.singletonList("Hello\\nWorld")); + assertEquals("Hello\nWorld", tokens.get(0)); + } + +} diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/UserRoleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/UserRoleParserTests.java new file mode 100644 index 000000000..df80a4b25 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/UserRoleParserTests.java @@ -0,0 +1,80 @@ +package com.structurizr.dsl; + +import com.structurizr.configuration.Role; +import com.structurizr.configuration.User; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class UserRoleParserTests extends AbstractTests { + + private UserRoleParser parser = new UserRoleParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("username", "role", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: <username> <read|write>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoUsernameIsSpecified() { + try { + parser.parse(context(), tokens("")); + fail(); + } catch (Exception e) { + assertEquals("Expected: <username> <read|write>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoRoleIsSpecified() { + try { + parser.parse(context(), tokens("user@example.com")); + fail(); + } catch (Exception e) { + assertEquals("Expected: <username> <read|write>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenAnInvalidRoleIsSpecified() { + try { + parser.parse(context(), tokens("user@example.com", "role")); + fail(); + } catch (Exception e) { + assertEquals("The role should be \"read\" or \"write\"", e.getMessage()); + } + } + + @Test + void test_parse_AddsAReadOnlyUser() { + parser.parse(context(), tokens("user@example.com", "read")); + + Set<User> users = context().getWorkspace().getConfiguration().getUsers(); + assertEquals(1, users.size()); + + User user = users.stream().findFirst().get(); + assertEquals("user@example.com", user.getUsername()); + assertEquals(Role.ReadOnly, user.getRole()); + } + + @Test + void test_parse_AddsAReadWriteUser() { + parser.parse(context(), tokens("user@example.com", "write")); + + Set<User> users = context().getWorkspace().getConfiguration().getUsers(); + assertEquals(1, users.size()); + + User user = users.stream().findFirst().get(); + assertEquals("user@example.com", user.getUsername()); + assertEquals(Role.ReadWrite, user.getRole()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ViewParserTests.java new file mode 100644 index 000000000..bad186b03 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ViewParserTests.java @@ -0,0 +1,156 @@ +package com.structurizr.dsl; + +import com.structurizr.view.CustomView; +import com.structurizr.view.DeploymentView; +import com.structurizr.view.DynamicView; +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ViewParserTests extends AbstractTests { + + private ViewParser parser = new ViewParser(); + + @Test + void test_parseTitle_ThrowsAnException_WhenThereAreTooManyTokens() { + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + try { + parser.parseTitle(context, tokens("title", "title", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: title <title>", e.getMessage()); + } + } + + @Test + void test_parseTitle_ThrowsAnException_WhenNoTitleIsSpecified() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parseTitle(context, tokens("title")); + fail(); + } catch (Exception e) { + assertEquals("Expected: title <title>", e.getMessage()); + } + } + + @Test + void test_parseTitle_SetsTheTitleOfACustomView() { + CustomView view = workspace.getViews().createCustomView("key", "title", "description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + assertEquals("title", view.getTitle()); + parser.parseTitle(context, tokens("title", "A new title")); + assertEquals("A new title", view.getTitle()); + } + + @Test + void test_parseTitle_SetsTheTitleOfAStaticView() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(view.getTitle()); + parser.parseTitle(context, tokens("title", "A new title")); + assertEquals("A new title", view.getTitle()); + } + + @Test + void test_parseTitle_SetsTheTitleOfADynamicView() { + DynamicView view = workspace.getViews().createDynamicView("key", "description"); + DynamicViewDslContext context = new DynamicViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(view.getTitle()); + parser.parseTitle(context, tokens("title", "A new title")); + assertEquals("A new title", view.getTitle()); + } + + @Test + void test_parseTitle_SetsTheTitleOfADeploymentView() { + DeploymentView view = workspace.getViews().createDeploymentView("key", "description"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + assertNull(view.getTitle()); + parser.parseTitle(context, tokens("title", "A new title")); + assertEquals("A new title", view.getTitle()); + } + + @Test + void test_parseDescription_ThrowsAnException_WhenThereAreTooManyTokens() { + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + try { + parser.parseDescription(context, tokens("description", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: description <description>", e.getMessage()); + } + } + + @Test + void test_parseDescription_ThrowsAnException_WhenNoTitleIsSpecified() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + try { + parser.parseDescription(context, tokens("description")); + fail(); + } catch (Exception e) { + assertEquals("Expected: description <description>", e.getMessage()); + } + } + + @Test + void test_parseDescription_SetsTheTitleOfACustomView() { + CustomView view = workspace.getViews().createCustomView("key", "title", "description"); + CustomViewDslContext context = new CustomViewDslContext(view); + context.setWorkspace(workspace); + + assertEquals("description", view.getDescription()); + parser.parseDescription(context, tokens("description", "A new description")); + assertEquals("A new description", view.getDescription()); + } + + @Test + void test_parseDescription_SetsTheTitleOfAStaticView() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + assertEquals("description", view.getDescription()); + parser.parseDescription(context, tokens("description", "A new description")); + assertEquals("A new description", view.getDescription()); + } + + @Test + void test_parseDescription_SetsTheTitleOfADynamicView() { + DynamicView view = workspace.getViews().createDynamicView("key", "description"); + DynamicViewDslContext context = new DynamicViewDslContext(view); + context.setWorkspace(workspace); + + assertEquals("description", view.getDescription()); + parser.parseDescription(context, tokens("description", "A new description")); + assertEquals("A new description", view.getDescription()); + } + + @Test + void test_parseDescription_SetsTheTitleOfADeploymentView() { + DeploymentView view = workspace.getViews().createDeploymentView("key", "description"); + DeploymentViewDslContext context = new DeploymentViewDslContext(view); + context.setWorkspace(workspace); + + assertEquals("description", view.getDescription()); + parser.parseDescription(context, tokens("description", "A new description")); + assertEquals("A new description", view.getDescription()); + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java new file mode 100644 index 000000000..009498f92 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java @@ -0,0 +1,105 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class WorkspaceParserTests extends AbstractTests { + + private WorkspaceParser parser = new WorkspaceParser(); + + @Test + void test_parseTitle_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(null, tokens("workspace", "name", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: workspace [name] [description] or workspace extends <file|url>", e.getMessage()); + } + } + + @Test + void test_parse_DoesNotThrowAnException_WhenNoPropertiesAreSpecified() { + assertEquals("Name", workspace.getName()); + assertEquals("Description", workspace.getDescription()); + workspace = parser.parse(null, tokens("workspace")); + assertEquals("Name", workspace.getName()); + assertEquals("Description", workspace.getDescription()); + } + + @Test + void test_parse_SetsTheWorkspaceName_WhenANameIsSpecified() { + assertEquals("Name", workspace.getName()); + assertEquals("Description", workspace.getDescription()); + workspace = parser.parse(null, tokens("workspace", "New Name")); + assertEquals("New Name", workspace.getName()); + assertEquals("Description", workspace.getDescription()); + } + + @Test + void test_parse_SetsTheWorkspaceNameAndDescription_WhenANameAndDescriptionAreSpecified() { + assertEquals("Name", workspace.getName()); + assertEquals("Description", workspace.getDescription()); + workspace = parser.parse(null, tokens("workspace", "New Name", "New Description")); + assertEquals("New Name", workspace.getName()); + assertEquals("New Description", workspace.getDescription()); + } + + @Test + void test_parseName_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + ModelItemDslContext context = new SoftwareSystemDslContext(null); + parser.parseName(context, tokens("name", "name", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: name <name>", e.getMessage()); + } + } + + @Test + void test_parseName_ThrowsAnException_WhenNoUrlIsSpecified() { + try { + parser.parseName(context(), tokens("name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: name <name>", e.getMessage()); + } + } + + @Test + void test_parseName_SetsTheName_WhenANameIsSpecified() { + parser.parseName(context(), tokens("name", "A new name")); + + assertEquals("A new name", context().getWorkspace().getName()); + } + + @Test + void test_parseDescription_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + ModelItemDslContext context = new SoftwareSystemDslContext(null); + parser.parseDescription(context, tokens("description", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: description <description>", e.getMessage()); + } + } + + @Test + void test_parseDescription_ThrowsAnException_WhenNoUrlIsSpecified() { + try { + parser.parseDescription(context(), tokens("description")); + fail(); + } catch (Exception e) { + assertEquals("Expected: description <description>", e.getMessage()); + } + } + + @Test + void test_parseDescription_SetsTheDescription_WhenADescriptionIsSpecified() { + parser.parseDescription(context(), tokens("description", "A new description")); + + assertEquals("A new description", context().getWorkspace().getDescription()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/example/ExampleDecisionImporter.java b/structurizr-dsl/src/test/java/com/structurizr/example/ExampleDecisionImporter.java new file mode 100644 index 000000000..e17d2370e --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/example/ExampleDecisionImporter.java @@ -0,0 +1,6 @@ +package com.structurizr.example; + +import com.structurizr.importer.documentation.AdrToolsDecisionImporter; + +public class ExampleDecisionImporter extends AdrToolsDecisionImporter { +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/example/ExampleDocumentationImporter.java b/structurizr-dsl/src/test/java/com/structurizr/example/ExampleDocumentationImporter.java new file mode 100644 index 000000000..59e58a1c4 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/example/ExampleDocumentationImporter.java @@ -0,0 +1,6 @@ +package com.structurizr.example; + +import com.structurizr.importer.documentation.DefaultDocumentationImporter; + +public class ExampleDocumentationImporter extends DefaultDocumentationImporter { +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0001-record-architecture-decisions.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0001-record-architecture-decisions.md new file mode 100644 index 000000000..f30860000 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0001-record-architecture-decisions.md @@ -0,0 +1,19 @@ +# 1. Record architecture decisions + +Date: 2016-02-12 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as described by Michael Nygard in this article: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions + +## Consequences + +See Michael Nygard's article, linked above. diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0002-implement-as-shell-scripts.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0002-implement-as-shell-scripts.md new file mode 100644 index 000000000..8e6ea15e6 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0002-implement-as-shell-scripts.md @@ -0,0 +1,28 @@ +# 2. Implement as shell scripts + +Date: 2016-02-12 + +## Status + +Accepted + +## Context + +ADRs are plain text files stored in a subdirectory of the project. + +The tool needs to create new files and apply small edits to +the Status section of existing files. + +## Decision + +The tool is implemented as shell scripts that use standard Unix +tools -- grep, sed, awk, etc. + +## Consequences + +The tool won't support Windows. Being plain text files, ADRs can +be created by hand and edited in any text editor. This tool just +makes the process more convenient. + +Development will have to cope with differences between Unix +variants, particularly Linux and MacOS X. diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0003-single-command-with-subcommands.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0003-single-command-with-subcommands.md new file mode 100644 index 000000000..f64db8da1 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0003-single-command-with-subcommands.md @@ -0,0 +1,45 @@ +# 3. Single command with subcommands + +Date: 2016-02-12 + +## Status + +Accepted + +## Context + +The tool provides a number of related commands to create +and manipulate architecture decision records. + +How can the user find out about the commands that are available? + +## Decision + +The tool defines a single command, called `adr`. + +The first argument to `adr` (the subcommand) specifies the +action to perform. Further arguments are interpreted by the +subcommand. + +Running `adr` without any arguments lists the available +subcommands. + +Subcommands are implemented as scripts in the same +directory as the `adr` script. E.g. the subcommand `new` is +implemented as the script `adr-new`, the subcommand `help` +as the script `adr-help` and so on. + +Helper scripts that are part of the implementation but not +subcommands follow a different naming convention, so that +subcommands can be listed by filtering and transforming script +file names. + +## Consequences + +Users can more easily explore the capabilities of the tool. + +Users are already used to this style of command-line tool. For +example, Git works this way. + +Each subcommand can be implemented in the most appropriate +language. diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0004-markdown-format.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0004-markdown-format.md new file mode 100644 index 000000000..86a21bf7e --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0004-markdown-format.md @@ -0,0 +1,40 @@ +# 4. Markdown format + +Date: 2016-02-12 + +## Status + +Superceded by [10. AsciiDoc format](0010-asciidoc-format.md) + +## Context + +The decision records must be stored in a plain text format: + +* This works well with version control systems. + +* It allows the tool to modify the status of records and insert + hyperlinks when one decision supercedes another. + +* Decisions can be read in the terminal, IDE, version control + browser, etc. + +People will want to use some formatting: lists, code examples, +and so on. + +People will want to view the decision records in a more readable +format than plain text, and maybe print them out. + + +## Decision + +Record architecture decisions in [Markdown format](https://daringfireball.net/projects/markdown/). + +## Consequences + +Decisions can be read in the terminal. + +Decisions will be formatted nicely and hyperlinked by the +browsers of project hosting sites like GitHub and Bitbucket. + +Tools like [Pandoc](http://pandoc.org/) can be used to convert +the decision records into HTML or PDF. diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0005-help-comments.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0005-help-comments.md new file mode 100644 index 000000000..b19bf0fb1 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0005-help-comments.md @@ -0,0 +1,42 @@ +# 5. Help comments + +Date: 2016-02-13 + +## Status + +Accepted + +Amended by [9. Help scripts](0009-help-scripts.md) + +## Context + +The tool will have a `help` subcommand to provide documentation +for users. + +It's nice to have usage documentation in the script files +themselves, in comments. When reading the code, that's the first +place to look for information about how to run a script. + +## Decision + +Write usage documentation in comments in the source file. + +Distinguish between documentation comments and normal comments. +Documentation comments have two hash characters at the start of +the line. + +The `adr help` command can parse comments out from the script +using the standard Unix tools `grep` and `cut`. + +## Consequences + +No need to maintain help text in a separate file. + +Help text can easily be kept up to date as the script is edited. + +There's no automated check that the help text is up to date. The +tests do not work well as documentation for users, and the help +text is not easily cross-checked against the code. + +This won't work if any subcommands are not implemented as scripts +that use '#' as a comment character. diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md new file mode 100644 index 000000000..4a3485f79 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md @@ -0,0 +1,41 @@ +# 6. Packaging and distribution in other version control repositories + +Date: 2016-02-16 + +## Status + +Accepted + +## Context + +Users want to install adr-tools with their preferred package +manager. For example, Ubuntu users use `apt`, RedHat users use +`yum` and Mac OS X users use [Homebrew](http://brew.sh). + +The developers of `adr-tools` don't know how, nor have permissions, +to use all these packaging and distribution systems. Therefore packaging +and distribution must be done by "downstream" parties. + +The developers of the tool should not favour any one particular +packaging and distribution solution. + +## Decision + +The `adr-tools` project will not contain any packaging or +distribution scripts and config. + +Packaging and distribution will be managed by other projects in +separate version control repositories. + +## Consequences + +The git repo of this project will be simpler. + +Eventually, users will not have to use Git to get the software. + +We will have to tag releases in the `adr-tools` repository so that +packaging projects know what can be published and how it should be +identified. + +We will document how users can install the software in this +project's README file. diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0007-invoke-adr-config-executable-to-get-configuration.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0007-invoke-adr-config-executable-to-get-configuration.md new file mode 100644 index 000000000..a649b2356 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0007-invoke-adr-config-executable-to-get-configuration.md @@ -0,0 +1,31 @@ +# 7. Invoke adr-config executable to get configuration + +Date: 2016-12-17 + +## Status + +Accepted + +## Context + +Packagers (e.g. Homebrew developers) want to configure adr-tools to match the conventions of their installation. + +Currently, this is done by sourcing a file `config.sh`, which should sit beside the `adr` executable. + +This name is too common. + +The `config.sh` file is not executable, and so doesn't belong in a bin directory. + +## Decision + +Replace `config.sh` with an executable, named `adr-config` that outputs configuration. + +Each script in ADR Tools will eval the output of `adr-config` to configure itself. + +## Consequences + +Configuration within ADR Tools is a little more complicated. + +Packagers can write their own implementation of `adr-config` that outputs configuration that matches the platform's installation conventions, and deploy it next to the `adr` script. + +To make development easier, the implementation of `adr-config` in the project's src/ directory will output configuration that lets the tool to run from the src/ directory without any installation step. (Packagers should not include this script in a deployable package.) diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0008-use-iso-8601-format-for-dates.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0008-use-iso-8601-format-for-dates.md new file mode 100644 index 000000000..4146f11df --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0008-use-iso-8601-format-for-dates.md @@ -0,0 +1,43 @@ +# 8. Use ISO 8601 Format for Dates + +Date: 2017-02-21 + +## Status + +Accepted + +## Context + +`adr-tools` seeks to communicate the history of architectural decisions of a +project. An important component of the history is the time at which a decision +was made. + +To communicate effectively, `adr-tools` should present information as +unambiguously as possible. That means that culture-neutral data formats should +be preferred over culture-specific formats. + +Existing `adr-tools` deployments format dates as `dd/mm/yyyy` by default. That +formatting is common formatting in the United Kingdom (where the `adr-tools` +project was originally written), but is easily confused with the `mm/dd/yyyy` +format preferred in the United States. + +The default date format may be overridden by setting `ADR_DATE` in `config.sh`. + +## Decision + +`adr-tools` will use the ISO 8601 format for dates: `yyyy-mm-dd` + +## Consequences + +Dates are displayed in a standard, culture-neutral format. + +The UK-style and ISO 8601 formats can be distinguished by their separator +character. The UK-style dates used a slash (`/`), while the ISO dates use a +hyphen (`-`). + +Prior to this decision, `adr-tools` was deployed using the UK format for dates. +After adopting the ISO 8601 format, existing deployments of `adr-tools` must do +one of the following: + + * Accept mixed formatting of dates within their documentation library. + * Update existing documents to use ISO 8601 dates by running `adr upgrade-repository` diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0009-help-scripts.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0009-help-scripts.md new file mode 100644 index 000000000..0146d127c --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0009-help-scripts.md @@ -0,0 +1,28 @@ +# 9. Help scripts + +Date: 2018-06-26 + +## Status + +Accepted + +Amends [5. Help comments](0005-help-comments.md) + +## Context + +Currently help text is generated by extracting specially formatted comments from the top of the command script. + +This makes it easy for developers of the tool: documentation and code is all in one place. + +But, it means that help text cannot include calculated values, such as the location of files. + +## Decision + +Where necessary, help text can be generated by a script. + +The script will be called _adr_help_<command>_<subcommand> + +## Consequences + +Help scripts can include helper scripts to locate files, giving more accurate instructions to the user that reflect how the tool is deployed in their environment. + diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0010-asciidoc-format.md b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0010-asciidoc-format.md new file mode 100644 index 000000000..edb613d1a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0010-asciidoc-format.md @@ -0,0 +1,21 @@ +# 10. AsciiDoc format + +Date: 2018-08-11 + +## Status + +Accepted + +Supercedes [4. Markdown format](0004-markdown-format.md) + +## Context + +The issue motivating this decision, and any context that influences or constrains the decision. + +## Decision + +The change that we're proposing or have agreed to implement. + +## Consequences + +What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated. diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/adrs/workspace.dsl new file mode 100644 index 000000000..a27b08488 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/adrs/workspace.dsl @@ -0,0 +1,19 @@ +workspace { + + !adrs adrs com.structurizr.example.ExampleDecisionImporter + + model { + softwareSystem = softwareSystem "Software System" { + !adrs adrs + + container "Container" { + !adrs adrs + + component "Component" { + !adrs adrs + } + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/amazon-web-services-local.dsl b/structurizr-dsl/src/test/resources/dsl/amazon-web-services-local.dsl new file mode 100644 index 000000000..6cb363198 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/amazon-web-services-local.dsl @@ -0,0 +1,65 @@ +workspace "Amazon Web Services Example" "An example AWS deployment architecture." { + + !identifiers hierarchical + + model { + springPetClinic = softwaresystem "Spring PetClinic" "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." "Spring Boot Application" { + webApplication = container "Web Application" "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." "Java and Spring Boot" + database = container "Database" "Stores information regarding the veterinarians, the clients, and their pets." "Relational database schema" "Database" + webApplication -> database "Reads from and writes to" "JDBC/SSL" + } + + live = deploymentEnvironment "Live" { + aws = deploymentNode "Amazon Web Services" "" "" "Amazon Web Services - Cloud" { + region = deploymentNode "US-East-1" "" "" "Amazon Web Services - Region" { + route53 = infrastructureNode "Route 53" "" "" "Amazon Web Services - Route 53" + elb = infrastructureNode "Elastic Load Balancer" "" "" "Amazon Web Services - Elastic Load Balancing" + + autoscalingGroup = deploymentNode "Autoscaling group" "" "" "Amazon Web Services - Auto Scaling" { + ec2 = deploymentNode "Amazon EC2" "" "" "Amazon Web Services - EC2" { + webApplicationInstance = containerInstance springPetClinic.webApplication + elb -> webApplicationInstance "Forwards requests to" "HTTPS" + } + } + + rds = deploymentNode "Amazon RDS" "" "" "Amazon Web Services - RDS" { + mysql = deploymentNode "MySQL" "" "" "Amazon Web Services - RDS MySQL instance" { + databaseInstance = containerInstance springPetClinic.database + } + } + + route53 -> elb "Forwards requests to" "HTTPS" + } + } + } + } + + views { + deployment springPetClinic "Live" "AmazonWebServicesDeployment" { + include * + autolayout lr + + animation { + live.aws.region.route53 + live.aws.region.elb + live.aws.region.autoscalingGroup.ec2.webApplicationInstance + live.aws.region.rds.mysql.databaseInstance + } + } + + styles { + element "Element" { + shape roundedbox + background #ffffff + } + element "Database" { + shape cylinder + } + element "Infrastructure Node" { + shape roundedbox + } + } + + themes https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/amazon-web-services.dsl b/structurizr-dsl/src/test/resources/dsl/amazon-web-services.dsl new file mode 100644 index 000000000..78948465d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/amazon-web-services.dsl @@ -0,0 +1,84 @@ +workspace "Amazon Web Services Example" "An example AWS deployment architecture." { + + model { + springPetClinic = softwaresystem "Spring PetClinic" "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." "Spring Boot Application" { + webApplication = container "Web Application" "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." "Java and Spring Boot" + database = container "Database" "Stores information regarding the veterinarians, the clients, and their pets." "Relational database schema" "Database" + } + + webApplication -> database "Reads from and writes to" "MySQL Protocol/SSL" + + live = deploymentEnvironment "Live" { + + deploymentNode "Amazon Web Services" { + tags "Amazon Web Services - Cloud" + + region = deploymentNode "US-East-1" { + tags "Amazon Web Services - Region" + + route53 = infrastructureNode "Route 53" { + tags "Amazon Web Services - Route 53" + } + + elb = infrastructureNode "Elastic Load Balancer" { + tags "Amazon Web Services - Elastic Load Balancing" + } + + deploymentNode "Autoscaling group" { + tags "Amazon Web Services - Auto Scaling" + + deploymentNode "Amazon EC2" { + tags "Amazon Web Services - EC2" + + webApplicationInstance = containerInstance webApplication + } + } + + deploymentNode "Amazon RDS" { + tags "Amazon Web Services - RDS" + + deploymentNode "MySQL" { + tags "Amazon Web Services - RDS MySQL instance" + + databaseInstance = containerInstance database + } + } + + } + } + + route53 -> elb "Forwards requests to" "HTTPS" + elb -> webApplicationInstance "Forwards requests to" "HTTPS" + } + } + + views { + deployment springPetClinic "Live" "AmazonWebServicesDeployment" { + include * + autolayout lr + + animation { + route53 + elb + webApplicationInstance + databaseInstance + } + } + + styles { + element "Element" { + shape roundedbox + background #ffffff + } + element "Database" { + shape cylinder + } + element "Infrastructure Node" { + shape roundedbox + } + } + + themes https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl new file mode 100644 index 000000000..2e6783160 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl @@ -0,0 +1,249 @@ +/* + * This is a combined version of the following workspaces: + * + * - "Big Bank plc - System Landscape" (https://structurizr.com/share/28201/) + * - "Big Bank plc - Internet Banking System" (https://structurizr.com/share/36141/) +*/ +workspace "Big Bank plc" "This is an example workspace to illustrate the key features of Structurizr, via the DSL, based around a fictional online banking system." { + + model { + customer = person "Personal Banking Customer" "A customer of the bank, with personal bank accounts." "Customer" + + enterprise "Big Bank plc" { + supportStaff = person "Customer Service Staff" "Customer service staff within the bank." "Bank Staff" + backoffice = person "Back Office Staff" "Administration and support staff within the bank." "Bank Staff" + + mainframe = softwaresystem "Mainframe Banking System" "Stores all of the core banking information about customers, accounts, transactions, etc." "Existing System" + email = softwaresystem "E-mail System" "The internal Microsoft Exchange e-mail system." "Existing System" + atm = softwaresystem "ATM" "Allows customers to withdraw cash." "Existing System" + + internetBankingSystem = softwaresystem "Internet Banking System" "Allows customers to view information about their bank accounts, and make payments." { + singlePageApplication = container "Single-Page Application" "Provides all of the Internet banking functionality to customers via their web browser." "JavaScript and Angular" "Web Browser" + mobileApp = container "Mobile App" "Provides a limited subset of the Internet banking functionality to customers via their mobile device." "Xamarin" "Mobile App" + webApplication = container "Web Application" "Delivers the static content and the Internet banking single page application." "Java and Spring MVC" + apiApplication = container "API Application" "Provides Internet banking functionality via a JSON/HTTPS API." "Java and Spring MVC" { + signinController = component "Sign In Controller" "Allows users to sign in to the Internet Banking System." "Spring MVC Rest Controller" + accountsSummaryController = component "Accounts Summary Controller" "Provides customers with a summary of their bank accounts." "Spring MVC Rest Controller" + resetPasswordController = component "Reset Password Controller" "Allows users to reset their passwords with a single use URL." "Spring MVC Rest Controller" + securityComponent = component "Security Component" "Provides functionality related to signing in, changing passwords, etc." "Spring Bean" + mainframeBankingSystemFacade = component "Mainframe Banking System Facade" "A facade onto the mainframe banking system." "Spring Bean" + emailComponent = component "E-mail Component" "Sends e-mails to users." "Spring Bean" + } + database = container "Database" "Stores user registration information, hashed authentication credentials, access logs, etc." "Oracle Database Schema" "Database" + } + } + + # relationships between people and software systems + customer -> internetBankingSystem "Views account balances, and makes payments using" + internetBankingSystem -> mainframe "Gets account information from, and makes payments using" + internetBankingSystem -> email "Sends e-mail using" + email -> customer "Sends e-mails to" + customer -> supportStaff "Asks questions to" "Telephone" + supportStaff -> mainframe "Uses" + customer -> atm "Withdraws cash using" + atm -> mainframe "Uses" + backoffice -> mainframe "Uses" + + # relationships to/from containers + customer -> webApplication "Visits bigbank.com/ib using" "HTTPS" + customer -> singlePageApplication "Views account balances, and makes payments using" + customer -> mobileApp "Views account balances, and makes payments using" + webApplication -> singlePageApplication "Delivers to the customer's web browser" + + # relationships to/from components + singlePageApplication -> signinController "Makes API calls to" "JSON/HTTPS" + singlePageApplication -> accountsSummaryController "Makes API calls to" "JSON/HTTPS" + singlePageApplication -> resetPasswordController "Makes API calls to" "JSON/HTTPS" + mobileApp -> signinController "Makes API calls to" "JSON/HTTPS" + mobileApp -> accountsSummaryController "Makes API calls to" "JSON/HTTPS" + mobileApp -> resetPasswordController "Makes API calls to" "JSON/HTTPS" + signinController -> securityComponent "Uses" + accountsSummaryController -> mainframeBankingSystemFacade "Uses" + resetPasswordController -> securityComponent "Uses" + resetPasswordController -> emailComponent "Uses" + securityComponent -> database "Reads from and writes to" "JDBC" + mainframeBankingSystemFacade -> mainframe "Makes API calls to" "XML/HTTPS" + emailComponent -> email "Sends e-mail using" + + deploymentEnvironment "Development" { + deploymentNode "Developer Laptop" "" "Microsoft Windows 10 or Apple macOS" { + deploymentNode "Web Browser" "" "Chrome, Firefox, Safari, or Edge" { + developerSinglePageApplicationInstance = containerInstance singlePageApplication + } + deploymentNode "Docker Container - Web Server" "" "Docker" { + deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" { + developerWebApplicationInstance = containerInstance webApplication + developerApiApplicationInstance = containerInstance apiApplication + } + } + deploymentNode "Docker Container - Database Server" "" "Docker" { + deploymentNode "Database Server" "" "Oracle 12c" { + developerDatabaseInstance = containerInstance database + } + } + } + deploymentNode "Big Bank plc" "" "Big Bank plc data center" "" { + deploymentNode "bigbank-dev001" "" "" "" { + softwareSystemInstance mainframe + } + } + + } + + deploymentEnvironment "Live" { + deploymentNode "Customer's mobile device" "" "Apple iOS or Android" { + liveMobileAppInstance = containerInstance mobileApp + } + deploymentNode "Customer's computer" "" "Microsoft Windows or Apple macOS" { + deploymentNode "Web Browser" "" "Chrome, Firefox, Safari, or Edge" { + liveSinglePageApplicationInstance = containerInstance singlePageApplication + } + } + + deploymentNode "Big Bank plc" "" "Big Bank plc data center" { + deploymentNode "bigbank-web***" "" "Ubuntu 16.04 LTS" "" 4 { + deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" { + liveWebApplicationInstance = containerInstance webApplication + } + } + deploymentNode "bigbank-api***" "" "Ubuntu 16.04 LTS" "" 8 { + deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" { + liveApiApplicationInstance = containerInstance apiApplication + } + } + + deploymentNode "bigbank-db01" "" "Ubuntu 16.04 LTS" { + primaryDatabaseServer = deploymentNode "Oracle - Primary" "" "Oracle 12c" { + livePrimaryDatabaseInstance = containerInstance database + } + } + deploymentNode "bigbank-db02" "" "Ubuntu 16.04 LTS" "Failover" { + secondaryDatabaseServer = deploymentNode "Oracle - Secondary" "" "Oracle 12c" "Failover" { + liveSecondaryDatabaseInstance = containerInstance database "Failover" + } + } + deploymentNode "bigbank-prod001" "" "" "" { + softwareSystemInstance mainframe + } + } + + primaryDatabaseServer -> secondaryDatabaseServer "Replicates data to" + } + } + + views { + systemlandscape "SystemLandscape" { + include * + autoLayout + } + + systemcontext internetBankingSystem "SystemContext" { + include * + animation { + internetBankingSystem + customer + mainframe + email + } + autoLayout + } + + container internetBankingSystem "Containers" { + include * + animation { + customer mainframe email + webApplication + singlePageApplication + mobileApp + apiApplication + database + } + autoLayout + } + + component apiApplication "Components" { + include * + animation { + singlePageApplication mobileApp database email mainframe + signinController securityComponent + accountsSummaryController mainframeBankingSystemFacade + resetPasswordController emailComponent + } + autoLayout + } + + dynamic apiApplication "SignIn" "Summarises how the sign in feature works in the single-page application." { + singlePageApplication -> signinController "Submits credentials to" + signinController -> securityComponent "Validates credentials using" + securityComponent -> database "select * from users where username = ?" + database -> securityComponent "Returns user data to" + securityComponent -> signinController "Returns true if the hashed password matches" + signinController -> singlePageApplication "Sends back an authentication token to" + autoLayout + } + + deployment internetBankingSystem "Development" "DevelopmentDeployment" { + include * + animation { + developerSinglePageApplicationInstance + developerWebApplicationInstance developerApiApplicationInstance + developerDatabaseInstance + } + autoLayout + } + + deployment internetBankingSystem "Live" "LiveDeployment" { + include * + animation { + liveSinglePageApplicationInstance + liveMobileAppInstance + liveWebApplicationInstance liveApiApplicationInstance + livePrimaryDatabaseInstance + liveSecondaryDatabaseInstance + } + autoLayout + } + + styles { + element "Person" { + color #ffffff + fontSize 22 + shape Person + } + element "Customer" { + background #08427b + } + element "Bank Staff" { + background #999999 + } + element "Software System" { + background #1168bd + color #ffffff + } + element "Existing System" { + background #999999 + color #ffffff + } + element "Container" { + background #438dd5 + color #ffffff + } + element "Web Browser" { + shape WebBrowser + } + element "Mobile App" { + shape MobileDeviceLandscape + } + element "Database" { + shape Cylinder + } + element "Component" { + background #85bbf0 + color #000000 + } + element "Failover" { + opacity 25 + } + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl new file mode 100644 index 000000000..140b822ae --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl @@ -0,0 +1,189 @@ +!constant INTERNET_BANKING_SYSTEM_INCLUDE "details.dsl" + +workspace "Big Bank plc - Internet Banking System" "The software architecture of the Big Bank plc Internet Banking System." { + + model { + !include model/people-and-software-systems.dsl + + # relationships to/from containers + customer -> webApplication "Visits bigbank.com/ib using" "HTTPS" + customer -> singlePageApplication "Views account balances, and makes payments using" + customer -> mobileApp "Views account balances, and makes payments using" + webApplication -> singlePageApplication "Delivers to the customer's web browser" + + # relationships to/from components + singlePageApplication -> signinController "Makes API calls to" "JSON/HTTPS" + singlePageApplication -> accountsSummaryController "Makes API calls to" "JSON/HTTPS" + singlePageApplication -> resetPasswordController "Makes API calls to" "JSON/HTTPS" + mobileApp -> signinController "Makes API calls to" "JSON/HTTPS" + mobileApp -> accountsSummaryController "Makes API calls to" "JSON/HTTPS" + mobileApp -> resetPasswordController "Makes API calls to" "JSON/HTTPS" + signinController -> securityComponent "Uses" + accountsSummaryController -> mainframeBankingSystemFacade "Uses" + resetPasswordController -> securityComponent "Uses" + resetPasswordController -> emailComponent "Uses" + securityComponent -> database "Reads from and writes to" "JDBC" + mainframeBankingSystemFacade -> mainframe "Makes API calls to" "XML/HTTPS" + emailComponent -> email "Sends e-mail using" + + deploymentEnvironment "Development" { + deploymentNode "Developer Laptop" "" "Microsoft Windows 10 or Apple macOS" { + deploymentNode "Web Browser" "" "Chrome, Firefox, Safari, or Edge" { + developerSinglePageApplicationInstance = containerInstance singlePageApplication + } + deploymentNode "Docker Container - Web Server" "" "Docker" { + deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" { + developerWebApplicationInstance = containerInstance webApplication + developerApiApplicationInstance = containerInstance apiApplication + } + } + deploymentNode "Docker Container - Database Server" "" "Docker" { + deploymentNode "Database Server" "" "Oracle 12c" { + developerDatabaseInstance = containerInstance database + } + } + } + deploymentNode "Big Bank plc" "" "Big Bank plc data center" "" { + deploymentNode "bigbank-dev001" "" "" "" { + softwareSystemInstance mainframe + } + } + } + + deploymentEnvironment "Live" { + deploymentNode "Customer's mobile device" "" "Apple iOS or Android" { + liveMobileAppInstance = containerInstance mobileApp + } + deploymentNode "Customer's computer" "" "Microsoft Windows or Apple macOS" { + deploymentNode "Web Browser" "" "Chrome, Firefox, Safari, or Edge" { + liveSinglePageApplicationInstance = containerInstance singlePageApplication + } + } + + deploymentNode "Big Bank plc" "" "Big Bank plc data center" { + deploymentNode "bigbank-web***" "" "Ubuntu 16.04 LTS" "" 4 { + deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" { + liveWebApplicationInstance = containerInstance webApplication + } + } + deploymentNode "bigbank-api***" "" "Ubuntu 16.04 LTS" "" 8 { + deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" { + liveApiApplicationInstance = containerInstance apiApplication + } + } + + deploymentNode "bigbank-db01" "" "Ubuntu 16.04 LTS" { + primaryDatabaseServer = deploymentNode "Oracle - Primary" "" "Oracle 12c" { + livePrimaryDatabaseInstance = containerInstance database + } + } + deploymentNode "bigbank-db02" "" "Ubuntu 16.04 LTS" "Failover" { + secondaryDatabaseServer = deploymentNode "Oracle - Secondary" "" "Oracle 12c" "Failover" { + liveSecondaryDatabaseInstance = containerInstance database "Failover" + } + } + deploymentNode "bigbank-prod001" "" "" "" { + softwareSystemInstance mainframe + } + } + + primaryDatabaseServer -> secondaryDatabaseServer "Replicates data to" + } + } + + views { + systemcontext internetBankingSystem "SystemContext" { + include * + animation { + internetBankingSystem + customer + mainframe + email + } + } + + container internetBankingSystem "Containers" { + include * + animation { + customer mainframe email + webApplication + singlePageApplication + mobileApp + apiApplication + database + } + } + + component apiApplication "Components" { + include * + animation { + singlePageApplication mobileApp database email mainframe + signinController securityComponent + accountsSummaryController mainframeBankingSystemFacade + resetPasswordController emailComponent + } + } + + dynamic apiApplication "SignIn" "Summarises how the sign in feature works in the single-page application." { + singlePageApplication -> signinController "Submits credentials to" + signinController -> securityComponent "Validates credentials using" + securityComponent -> database "select * from users where username = ?" + database -> securityComponent "Returns user data to" + securityComponent -> signinController "Returns true if the hashed password matches" + signinController -> singlePageApplication "Sends back an authentication token to" + } + + deployment internetBankingSystem "Development" "DevelopmentDeployment" { + include * + animation { + developerSinglePageApplicationInstance + developerWebApplicationInstance developerApiApplicationInstance + developerDatabaseInstance + } + } + + deployment internetBankingSystem "Live" "LiveDeployment" { + include * + animation { + liveSinglePageApplicationInstance + liveMobileAppInstance + liveWebApplicationInstance liveApiApplicationInstance + livePrimaryDatabaseInstance + liveSecondaryDatabaseInstance + } + } + + styles { + !include views/styles-people.dsl + + element "Software System" { + background #1168bd + color #ffffff + } + element "Existing System" { + background #999999 + color #ffffff + } + element "Container" { + background #438dd5 + color #ffffff + } + element "Web Browser" { + shape WebBrowser + } + element "Mobile App" { + shape MobileDeviceLandscape + } + element "Database" { + shape Cylinder + } + element "Component" { + background #85bbf0 + color #000000 + } + element "Failover" { + opacity 25 + } + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/adrs/0001-record-architecture-decisions.md b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/adrs/0001-record-architecture-decisions.md new file mode 100644 index 000000000..7461d99d3 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/adrs/0001-record-architecture-decisions.md @@ -0,0 +1,19 @@ +# 1. Record architecture decisions + +Date: 2020-06-05 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as described by Michael Nygard in this article: [http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) + +## Consequences + +See Michael Nygard's article, linked above. \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/details.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/details.dsl new file mode 100644 index 000000000..c3b491544 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/details.dsl @@ -0,0 +1,15 @@ +singlePageApplication = container "Single-Page Application" "Provides all of the Internet banking functionality to customers via their web browser." "JavaScript and Angular" "Web Browser" +mobileApp = container "Mobile App" "Provides a limited subset of the Internet banking functionality to customers via their mobile device." "Xamarin" "Mobile App" +webApplication = container "Web Application" "Delivers the static content and the Internet banking single page application." "Java and Spring MVC" +apiApplication = container "API Application" "Provides Internet banking functionality via a JSON/HTTPS API." "Java and Spring MVC" { + signinController = component "Sign In Controller" "Allows users to sign in to the Internet Banking System." "Spring MVC Rest Controller" + accountsSummaryController = component "Accounts Summary Controller" "Provides customers with a summary of their bank accounts." "Spring MVC Rest Controller" + resetPasswordController = component "Reset Password Controller" "Allows users to reset their passwords with a single use URL." "Spring MVC Rest Controller" + securityComponent = component "Security Component" "Provides functionality related to signing in, changing passwords, etc." "Spring Bean" + mainframeBankingSystemFacade = component "Mainframe Banking System Facade" "A facade onto the mainframe banking system." "Spring Bean" + emailComponent = component "E-mail Component" "Sends e-mails to users." "Spring Bean" +} +database = container "Database" "Stores user registration information, hashed authentication credentials, access logs, etc." "Oracle Database Schema" "Database" + +!docs docs +!adrs adrs \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/01-context.md b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/01-context.md new file mode 100644 index 000000000..5440e9435 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/01-context.md @@ -0,0 +1,11 @@ +## Context + +Here is some context about the Internet Banking System... + +![](embed:SystemContext) + +### Internet Banking System +... + +### Mainframe Banking System +... diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/02-containers.md b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/02-containers.md new file mode 100644 index 000000000..d4d8d9aab --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/02-containers.md @@ -0,0 +1,21 @@ +## Software Architecture + +Here is some information about the software architecture of the Internet Banking System... + +![](embed:Containers) + +### Web Application +... + +### Database +... + +Here is some information about the API Application... + +![](embed:Components) + +### Sign in process + +Here is some information about the Sign In Controller, including how the sign in process works... + +![](embed:SignIn) \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/03-development-environment.adoc b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/03-development-environment.adoc new file mode 100644 index 000000000..9f9b6d664 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/03-development-environment.adoc @@ -0,0 +1,5 @@ +== Development Environment + +Here is some information about how to set up a development environment for the Internet Banking System... + +image::embed:DevelopmentDeployment[] \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/04-deployment.adoc b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/04-deployment.adoc new file mode 100644 index 000000000..be82ea565 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/docs/04-deployment.adoc @@ -0,0 +1,5 @@ +== Deployment + +Here is some information about the live deployment environment for the Internet Banking System... + +image::embed:LiveDeployment[] \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/summary.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/summary.dsl new file mode 100644 index 000000000..71adf255f --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/internet-banking-system/summary.dsl @@ -0,0 +1 @@ +url https://structurizr.com/share/36141/diagrams#SystemContext \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl new file mode 100644 index 000000000..b4ff925b0 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl @@ -0,0 +1,25 @@ +customer = person "Personal Banking Customer" "A customer of the bank, with personal bank accounts." "Customer" + +enterprise "Big Bank plc" { + supportStaff = person "Customer Service Staff" "Customer service staff within the bank." "Bank Staff" + backoffice = person "Back Office Staff" "Administration and support staff within the bank." "Bank Staff" + + mainframe = softwaresystem "Mainframe Banking System" "Stores all of the core banking information about customers, accounts, transactions, etc." "Existing System" + email = softwaresystem "E-mail System" "The internal Microsoft Exchange e-mail system." "Existing System" + atm = softwaresystem "ATM" "Allows customers to withdraw cash." "Existing System" + + internetBankingSystem = softwaresystem "Internet Banking System" "Allows customers to view information about their bank accounts, and make payments." { + !include "internet-banking-system/${INTERNET_BANKING_SYSTEM_INCLUDE}" + } +} + +# relationships between people and software systems +customer -> internetBankingSystem "Views account balances, and makes payments using" +internetBankingSystem -> mainframe "Gets account information from, and makes payments using" +internetBankingSystem -> email "Sends e-mail using" +email -> customer "Sends e-mails to" +customer -> supportStaff "Asks questions to" "Telephone" +supportStaff -> mainframe "Uses" +customer -> atm "Withdraws cash using" +atm -> mainframe "Uses" +backoffice -> mainframe "Uses" \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl new file mode 100644 index 000000000..f60329605 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl @@ -0,0 +1,23 @@ +!constant INTERNET_BANKING_SYSTEM_INCLUDE "summary.dsl" + +workspace "Big Bank plc - System Landscape" "The system landscape for Big Bank plc." { + + model { + !include model/people-and-software-systems.dsl + } + + views { + systemlandscape "SystemLandscape" { + include * + } + + styles { + !include views/styles-people.dsl + + element "Software System" { + background #999999 + color #ffffff + } + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/views/styles-people.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/views/styles-people.dsl new file mode 100644 index 000000000..f8052dca0 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/views/styles-people.dsl @@ -0,0 +1,11 @@ +element "Person" { + color #ffffff + fontSize 22 + shape Person +} +element "Customer" { + background #08427b +} +element "Bank Staff" { + background #999999 +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl new file mode 100644 index 000000000..484323ede --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl @@ -0,0 +1,9 @@ +workspace { + model { + de = deploymentEnvironment "DeploymentEnvironment" + + !ref de { + dn = deploymentNode "DeploymentNode" + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-groups.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-groups.dsl new file mode 100644 index 000000000..e58f506ee --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/deployment-groups.dsl @@ -0,0 +1,48 @@ +workspace { + + model { + softwareSystem = softwareSystem "Software System" { + database = container "Database" + api = container "Service API" { + -> database "Uses" + } + } + + deploymentEnvironment "Example 1" { + deploymentNode "Server 1" { + containerInstance api + containerInstance database + } + deploymentNode "Server 2" { + containerInstance api + containerInstance database + } + } + + deploymentEnvironment "Example 2" { + serviceInstance1 = deploymentGroup "Service Instance 1" + serviceInstance2 = deploymentGroup "Service Instance 2" + deploymentNode "Server 1" { + containerInstance api serviceInstance1 + containerInstance database serviceInstance1 + } + deploymentNode "Server 2" { + containerInstance api serviceInstance2 + containerInstance database serviceInstance2 + } + } + } + + views { + deployment * "Example 1" { + include * + autolayout + } + + deployment * "Example 2" { + include * + autolayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md b/structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md new file mode 100644 index 000000000..ddb90b8f7 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md @@ -0,0 +1,5 @@ +## Context + +Here is a description of my software system... + +![](embed:Diagram1) \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl new file mode 100644 index 000000000..937977cc4 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl @@ -0,0 +1,31 @@ +workspace { + + !docs docs com.structurizr.example.ExampleDocumentationImporter + + model { + user = person "User" + softwareSystem = softwareSystem "Software System" { + !docs docs + + container "Container" { + !docs docs + + component "Component" { + !docs docs + } + } + } + + user -> softwareSystem "Uses" + } + + views { + systemContext softwareSystem "Diagram1" { + include * + autoLayout + } + + theme default + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-custom-elements.dsl b/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-custom-elements.dsl new file mode 100644 index 000000000..4dbbb263b --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-custom-elements.dsl @@ -0,0 +1,19 @@ +workspace { + + model { + a = element "A" + b = softwareSystem "B" + c = element "C" + + a -> b + b -> c + } + + views { + dynamic * { + a -> b + b -> c + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-relationships.dsl b/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-relationships.dsl new file mode 100644 index 000000000..33103f4b4 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-relationships.dsl @@ -0,0 +1,37 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + + r1 = a -> b "Sync" { + tags "Sync" + } + + r2 = a -> b "Async" { + tags "Async" + } + } + + views { + systemLandscape { + include * + autoLayout + } + + dynamic * { + r2 "Async" + autoLayout + } + + styles { + relationship "Sync" { + style solid + } + relationship "Async" { + style dashed + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/dynamic.dsl b/structurizr-dsl/src/test/resources/dsl/dynamic.dsl new file mode 100644 index 000000000..774afab65 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/dynamic.dsl @@ -0,0 +1,24 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + + a -> b "Sends data to" + } + + views { + dynamic * { + // with this example, the relationship uses the same description as defined in the static model + a -> b + autoLayout + } + + dynamic * { + // with this example, the relationship description is overriden to describe a particular feature/use case/etc + a -> b "Sends customer data to" + autoLayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/exclude-implied-relationship.dsl b/structurizr-dsl/src/test/resources/dsl/exclude-implied-relationship.dsl new file mode 100644 index 000000000..ec8370df9 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/exclude-implied-relationship.dsl @@ -0,0 +1,22 @@ +workspace { + + model { + softwareSystem "A" { + a = container "A" + } + + softwareSystem "B" { + b = container "B" + } + + r = a -> b + } + + views { + systemLandscape { + include * + exclude r + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/exclude-relationships.dsl b/structurizr-dsl/src/test/resources/dsl/exclude-relationships.dsl new file mode 100644 index 000000000..463d8d45c --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/exclude-relationships.dsl @@ -0,0 +1,20 @@ +workspace { + + model { + user = person "User" + softwareSystem = softwareSystem "Software System" + + user -> softwareSystem "Uses" + } + + views { + systemContext softwareSystem { + include * + exclude "* -> element.tag==Software System" + autolayout + } + + theme default + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/1.dsl b/structurizr-dsl/src/test/resources/dsl/extend/1.dsl new file mode 100644 index 000000000..b3c1cbc0b --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/1.dsl @@ -0,0 +1,7 @@ +workspace { + + model { + user1 = person "User 1" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/2.dsl b/structurizr-dsl/src/test/resources/dsl/extend/2.dsl new file mode 100644 index 000000000..f873c6d4e --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/2.dsl @@ -0,0 +1,7 @@ +workspace extends 1.dsl { + + model { + user2 = person "User 2" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/3.dsl b/structurizr-dsl/src/test/resources/dsl/extend/3.dsl new file mode 100644 index 000000000..d23c96655 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/3.dsl @@ -0,0 +1,7 @@ +workspace extends 2.dsl { + + model { + user3 = person "User 3" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/4.dsl b/structurizr-dsl/src/test/resources/dsl/extend/4.dsl new file mode 100644 index 000000000..a978e2db0 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/4.dsl @@ -0,0 +1,9 @@ +workspace extends 3.dsl { + + views { + systemLandscape { + include user1 user2 user3 + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl new file mode 100644 index 000000000..4260ec1e2 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl @@ -0,0 +1,12 @@ +workspace extends workspace.dsl { + + model { + !ref softwareSystem1 { + webapp = container "Web Application" + } + + user -> softwareSystem1 "Uses" + softwareSystem3.webapp -> softwareSystem3.db + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl new file mode 100644 index 000000000..d5a24910a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl @@ -0,0 +1,12 @@ +workspace extends https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/extend/workspace.dsl { + + model { + !ref softwareSystem1 { + webapp = container "Web Application" + } + + user -> softwareSystem1 "Uses" + softwareSystem3.webapp -> softwareSystem3.db + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl new file mode 100644 index 000000000..06e3ffc8b --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl @@ -0,0 +1,18 @@ +workspace extends workspace.json { + + model { + // !extend with DSL identifier + !extend softwareSystem1 { + webapp1 = container "Web Application 1" + } + + // !extend with canonical name + !extend "SoftwareSystem://Software System 1" { + webapp2 = container "Web Application 2" + } + + user -> softwareSystem1 "Uses" + softwareSystem3.webapp -> softwareSystem3.db + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl new file mode 100644 index 000000000..e84912308 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl @@ -0,0 +1,18 @@ +workspace extends https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/extend/workspace.json { + + model { + // !extend with DSL identifier + !extend softwareSystem1 { + webapp1 = container "Web Application 1" + } + + // !extend with canonical name + !extend "SoftwareSystem://Software System 1" { + webapp2 = container "Web Application 2" + } + + user -> softwareSystem1 "Uses" + softwareSystem3.webapp -> softwareSystem3.db + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/extend/workspace.dsl new file mode 100644 index 000000000..29861779d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/workspace.dsl @@ -0,0 +1,18 @@ +workspace { + + !identifiers hierarchical + + model { + user = person "User" + + softwareSystem1 = softwareSystem "Software System 1" + + softwareSystem "Software System 2" + + softwareSystem3 = softwareSystem "Software System 3" { + webapp = container "Web Application" + db = container "Database" + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend/workspace.json b/structurizr-dsl/src/test/resources/dsl/extend/workspace.json new file mode 100644 index 000000000..09a9f179a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend/workspace.json @@ -0,0 +1,73 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "properties" : { + "structurizr.dsl" : "d29ya3NwYWNlIHsKCiAgICAhaWRlbnRpZmllcnMgaGllcmFyY2hpY2FsCgogICAgbW9kZWwgewogICAgICAgIHVzZXIgPSBwZXJzb24gIlVzZXIiCgogICAgICAgIHNvZnR3YXJlU3lzdGVtMSA9IHNvZnR3YXJlU3lzdGVtICJTb2Z0d2FyZSBTeXN0ZW0gMSIKCiAgICAgICAgc29mdHdhcmVTeXN0ZW0gIlNvZnR3YXJlIFN5c3RlbSAyIgoKICAgICAgICBzb2Z0d2FyZVN5c3RlbTMgPSBzb2Z0d2FyZVN5c3RlbSAiU29mdHdhcmUgU3lzdGVtIDMiIHsKICAgICAgICAgICAgd2ViYXBwID0gY29udGFpbmVyICJXZWIgQXBwbGljYXRpb24iCiAgICAgICAgICAgIGRiID0gY29udGFpbmVyICJEYXRhYmFzZSIKICAgICAgICB9CiAgICB9Cgp9Cg==" + }, + "configuration" : { }, + "model" : { + "people" : [ { + "id" : "1", + "tags" : "Element,Person", + "properties" : { + "structurizr.dsl.identifier" : "user" + }, + "name" : "User", + "location" : "Unspecified" + } ], + "softwareSystems" : [ { + "id" : "2", + "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "softwaresystem1" + }, + "name" : "Software System 1", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "3", + "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "93ea2110-adec-4936-97aa-b55397325115" + }, + "name" : "Software System 2", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "4", + "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "softwaresystem3" + }, + "name" : "Software System 3", + "location" : "Unspecified", + "containers" : [ { + "id" : "5", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "softwaresystem3.webapp" + }, + "name" : "Web Application", + "documentation" : { } + }, { + "id" : "6", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "softwaresystem3.db" + }, + "name" : "Database", + "documentation" : { } + } ], + "documentation" : { } + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/filteredviews.dsl b/structurizr-dsl/src/test/resources/dsl/filteredviews.dsl new file mode 100644 index 000000000..236be9509 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/filteredviews.dsl @@ -0,0 +1,41 @@ +workspace "FilteredDemo" "This is an example of Filtered views" { + + # This will render the two diagrams at https://structurizr.com/help/filtered-views + + model { + + user = person "Customer" "A description of the user." + sysa = softwareSystem "Software System A" "A description of software system A." + + user -> sysa "Uses for tasks 1 and 2" "" Current + + sysb = softwareSystem "Software System B" "A description of software system B." Future + + user -> sysa "Uses for task 1" "" Future + user -> sysb "Uses for task 2" "" Future + + } + + views { + + systemLandscape FullLandscape "System Landscape, current and future" { + include * + } + + filtered FullLandscape exclude Future CurrentLandscape "The current system landscape." + filtered FullLandscape exclude Current FutureLandscape "The future state system landscape after Software System B is live." + + styles { + element "Software System" { + background #91a437 + shape RoundedBox + } + + element "Person" { + background #6a7b15 + shape Person + } + } + + } +} diff --git a/structurizr-dsl/src/test/resources/dsl/financial-risk-system.dsl b/structurizr-dsl/src/test/resources/dsl/financial-risk-system.dsl new file mode 100644 index 000000000..a5539386f --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/financial-risk-system.dsl @@ -0,0 +1,67 @@ +workspace "Financial Risk System" "This is a simple (incomplete) example C4 model based upon the financial risk system architecture kata, which can be found at http://bit.ly/sa4d-risksystem" { + + model { + businessUser = person "Business User" "A regular business user." + configurationUser = person "Configuration User" "A regular business user who can also configure the parameters used in the risk calculations." + + financialRiskSystem = softwareSystem "Financial Risk System" "Calculates the bank's exposure to risk for product X." "Financial Risk System" + tradeDataSystem = softwareSystem "Trade Data System" "The system of record for trades of type X." + referenceDataSystem = softwareSystem "Reference Data System" "Manages reference data for all counterparties the bank interacts with." + referenceDataSystemV2 = softwareSystem "Reference Data System v2.0" "Manages reference data for all counterparties the bank interacts with." "Future State" + emailSystem = softwareSystem "E-mail system" "The bank's Microsoft Exchange system." + centralMonitoringService = softwareSystem "Central Monitoring Service" "The bank's central monitoring and alerting dashboard." + activeDirectory = softwareSystem "Active Directory" "The bank's authentication and authorisation system." + + businessUser -> financialRiskSystem "Views reports using" + financialRiskSystem -> tradeDataSystem "Gets trade data from" + financialRiskSystem -> referenceDataSystem "Gets counterparty data from" + financialRiskSystem -> referenceDataSystemV2 "Gets counterparty data from" "" "Future State" + configurationUser -> financialRiskSystem "Configures parameters using" + financialRiskSystem -> emailSystem "Sends a notification that a report is ready to" + emailSystem -> businessUser "Sends a notification that a report is ready to" "E-mail message" "Asynchronous" + financialRiskSystem -> centralMonitoringService "Sends critical failure alerts to" "SNMP" "Asynchronous, Alert" + financialRiskSystem -> activeDirectory "Uses for user authentication and authorisation" + } + + views { + + systemContext financialRiskSystem "Context" "An example System Context diagram for the Financial Risk System architecture kata." { + include * + autoLayout + } + + styles { + element "Element" { + color #ffffff + } + element "Software System" { + background #801515 + shape RoundedBox + } + element "Financial Risk System" { + background #550000 + color #ffffff + } + element "Future State" { + opacity 30 + } + element "Person" { + background #d46a6a + shape Person + } + relationship "Relationship" { + dashed false + } + relationship "Asynchronous" { + dashed true + } + relationship "Alert" { + color #ff0000 + } + relationship "Future State" { + opacity 30 + } + } + + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/getting-started-short.dsl b/structurizr-dsl/src/test/resources/dsl/getting-started-short.dsl new file mode 100644 index 000000000..ddfcf56e1 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/getting-started-short.dsl @@ -0,0 +1,17 @@ +workspace { + + model { + user = person "User" "A user of my software system." + softwareSystem = softwareSystem "Software System" "My software system." + + user -> softwareSystem "Uses" + } + + views { + systemContext softwareSystem { + include * + autoLayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/getting-started.dsl b/structurizr-dsl/src/test/resources/dsl/getting-started.dsl new file mode 100644 index 000000000..3706c0796 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/getting-started.dsl @@ -0,0 +1,19 @@ +workspace { + + model { + user = person "User" + softwareSystem = softwareSystem "Software System" + + user -> softwareSystem "Uses" + } + + views { + systemContext softwareSystem { + include * + autolayout + } + + theme default + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/group-url.dsl b/structurizr-dsl/src/test/resources/dsl/group-url.dsl new file mode 100644 index 000000000..64d36d9c7 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/group-url.dsl @@ -0,0 +1,11 @@ +workspace { + + model { + softwareSystem "Software System" { + group "Name" { + url "https://example.com" + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/group-without-brace.dsl b/structurizr-dsl/src/test/resources/dsl/group-without-brace.dsl new file mode 100644 index 000000000..71647fe6a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/group-without-brace.dsl @@ -0,0 +1,7 @@ +workspace { + + model { + group "Name" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl b/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl new file mode 100644 index 000000000..4e98e49a3 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl @@ -0,0 +1,47 @@ +workspace { + + model { + properties { + structurizr.groupSeparator / + } + + group "Organisation" { + group "Department A" { + a = softwareSystem "A" { + group "Capability 1" { + group "Service A" { + container "A API" + container "A Database" + } + group "Service B" { + container "B API" + container "B Database" + } + } + } + } + + group "Department B" { + b = softwareSystem "B" + } + + c = softwareSystem "C" + } + + enterprise "Enterprise" { + group "Department A" { + group "Team 1" { + d = softwareSystem "D" + } + } + } + } + + views { + systemLandscape { + include * + autolayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/groups.dsl b/structurizr-dsl/src/test/resources/dsl/groups.dsl new file mode 100644 index 000000000..d38ce2d46 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/groups.dsl @@ -0,0 +1,51 @@ +workspace { + + model { + softwareSystem = softwareSystem "Software System" { + service1 = group "Service 1" { + service1Api = container "Service 1 API" + service1Database = container "Service 1 Database" + + service1Api -> service1Database "Reads from and writes to" + } + service2 = group "Service 2" { + service2Api = container "Service 2 API" + service2Database = container "Service 2 Database" + + service2Api -> service2Database "Reads from and writes to" + } + } + + live = deploymentEnvironment "Live" { + group "Servers" { + deploymentNode "Server 1" { + group "Service 1" { + containerInstance service1Api + containerInstance service1Database + } + } + deploymentNode "Server 2" { + group "Service 2" { + containerInstance service2Api + containerInstance service2Database + } + } + } + } + + service1Api -> service2Api "Uses" + } + + views { + container softwareSystem { + include service1 service2 + autolayout + } + + deployment softwareSystem live { + include service1 service2 + autolayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-1.dsl b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-1.dsl new file mode 100644 index 000000000..54af32462 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-1.dsl @@ -0,0 +1,40 @@ + workspace { + + !identifiers hierarchical + + model { + ss = softwareSystem "Software System" + + deploymentEnvironment "Live" { + + dn = deploymentNode "DN" { + dn1 = deploymentNode "DN1" { + softwareSystemInstance ss + } + + dn2 = deploymentNode "DN2" { + softwareSystemInstance ss + } + + dn1 -> dn2 + } + + dn1 = deploymentNode "DN1" { + softwareSystemInstance ss + } + + dn2 = deploymentNode "DN2" { + softwareSystemInstance ss + } + + dn1 -> dn2 + } + } + + views { + deployment * "Live" { + include * + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-2.dsl b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-2.dsl new file mode 100644 index 000000000..ae26c3d9c --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-2.dsl @@ -0,0 +1,18 @@ +workspace { + + !identifiers hierarchical + + model { + ss = softwareSystem "SS" + + live = deploymentEnvironment "Environment" { + dn = deploymentNode "DN1" { + ss = deploymentNode "DN2" { + softwareSystemInstance ss + } + } + } + + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-3.dsl b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-3.dsl new file mode 100644 index 000000000..97d3b7d37 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-3.dsl @@ -0,0 +1,22 @@ +workspace { + + !identifiers hierarchical + + model { + ss = softwareSystem "SS" { + c = container "C" + } + + live = deploymentEnvironment "Environment" { + dn = deploymentNode "DN1" { + ss = deploymentNode "DN2" { + c = deploymentNode "DN3" { + containerInstance ss.c + } + } + } + } + + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-when-unassigned.dsl b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-when-unassigned.dsl new file mode 100644 index 000000000..067a22cd8 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers-when-unassigned.dsl @@ -0,0 +1,19 @@ +workspace { + + !identifiers hierarchical + + model { + softwareSystem "A" { + container "B" { + component "C" + } + } + + deploymentEnvironment "Environment" { + deploymentNode "Deployment Node" { + infrastructureNode "Infrastructure Node" + } + } + + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers.dsl b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers.dsl new file mode 100644 index 000000000..503f6b91d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/hierarchical-identifiers.dsl @@ -0,0 +1,16 @@ +workspace { + + !identifiers hierarchical + + model { + a = person "A" + b = softwareSystem "B"{ + c = container "C" { + d = component "D" + a = component "A" + + d -> a + } + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/identifiers.dsl b/structurizr-dsl/src/test/resources/dsl/identifiers.dsl new file mode 100644 index 000000000..d4a32f218 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/identifiers.dsl @@ -0,0 +1,14 @@ +workspace { + + !identifiers hierarchical + + model { + user = person "User" + softwareSystem = softwareSystem "Software System" { + container = container "Container" { + rel = user -> this "Uses" + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/diagram.dot b/structurizr-dsl/src/test/resources/dsl/image-views/diagram.dot new file mode 100644 index 000000000..3f2b18926 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/diagram.dot @@ -0,0 +1 @@ +digraph G {Hello->World} diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/diagram.mmd b/structurizr-dsl/src/test/resources/dsl/image-views/diagram.mmd new file mode 100644 index 000000000..498140e3e --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/diagram.mmd @@ -0,0 +1,2 @@ +flowchart TD + Start --> Stop \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/diagram.puml b/structurizr-dsl/src/test/resources/dsl/image-views/diagram.puml new file mode 100644 index 000000000..1da6ac585 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/diagram.puml @@ -0,0 +1,3 @@ +@startuml +Bob -> Alice : hello +@enduml \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/logo.png b/structurizr-dsl/src/test/resources/dsl/image-views/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..763d19bf5c7d827bec4f6c31ff8e8660f09aab54 GIT binary patch literal 9262 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 literal 0 HcmV?d00001 diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl new file mode 100644 index 000000000..22d141f68 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl @@ -0,0 +1,27 @@ +workspace { + + views { + properties { + "plantuml.url" "http://localhost:7777" + "mermaid.url" "http://localhost:8888" + "kroki.url" "http://localhost:9999" + } + + image * "plantuml" { + plantuml diagram.puml + } + + image * "mermaid" { + mermaid diagram.mmd + } + + image * "kroki" { + kroki graphviz diagram.dot + } + + image * "image" { + image logo.png + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl new file mode 100644 index 000000000..91ff21755 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl @@ -0,0 +1,30 @@ +workspace { + + views { + properties { + "plantuml.url" "http://localhost:7777" + "plantuml.format" "svg" + "mermaid.url" "http://localhost:8888" + "mermaid.format" "svg" + "kroki.url" "http://localhost:9999" + "kroki.format" "svg" + } + + image * "plantuml" { + plantuml https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/image-views/diagram.puml + } + + image * "mermaid" { + mermaid https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/image-views/diagram.mmd + } + + image * "kroki" { + kroki graphviz https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/image-views/diagram.dot + } + + image * "image" { + image https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/image-views/logo.png + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/include-directory.dsl b/structurizr-dsl/src/test/resources/dsl/include-directory.dsl new file mode 100644 index 000000000..88e4a04bd --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include-directory.dsl @@ -0,0 +1,14 @@ +workspace { + + model { + !constant SOFTWARE_SYSTEM_NAME "Software System 1" + !include include/model/software-system/model.dsl + + !constant SOFTWARE_SYSTEM_NAME "Software System 2" + !include include/model/software-system + + !constant SOFTWARE_SYSTEM_NAME "Software System 3" + !include include/model + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/include-file.dsl b/structurizr-dsl/src/test/resources/dsl/include-file.dsl new file mode 100644 index 000000000..83d8d93db --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include-file.dsl @@ -0,0 +1,7 @@ +workspace { + + model { + !include include/model.dsl + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/include-implied-relationship.dsl b/structurizr-dsl/src/test/resources/dsl/include-implied-relationship.dsl new file mode 100644 index 000000000..86757f95c --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include-implied-relationship.dsl @@ -0,0 +1,23 @@ +workspace { + + model { + softwareSystem "A" { + a = container "A" + } + + softwareSystem "B" { + b = container "B" + } + + r = a -> b + } + + views { + systemLandscape { + include * + exclude *->* + include r + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/include-url.dsl b/structurizr-dsl/src/test/resources/dsl/include-url.dsl new file mode 100644 index 000000000..9739a90f7 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include-url.dsl @@ -0,0 +1,7 @@ +workspace { + + model { + !include https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/include/model.dsl + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/include/docs/section.md b/structurizr-dsl/src/test/resources/dsl/include/docs/section.md new file mode 100644 index 000000000..fa8d5e722 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include/docs/section.md @@ -0,0 +1,3 @@ +## Heading + +Text... \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/include/model.dsl b/structurizr-dsl/src/test/resources/dsl/include/model.dsl new file mode 100644 index 000000000..6bdd6ec8b --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include/model.dsl @@ -0,0 +1,3 @@ +softwareSystem = softwareSystem "Software System" { + !docs docs +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/include/model/software-system/model.dsl b/structurizr-dsl/src/test/resources/dsl/include/model/software-system/model.dsl new file mode 100644 index 000000000..a040bae35 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include/model/software-system/model.dsl @@ -0,0 +1,3 @@ +softwareSystem "${SOFTWARE_SYSTEM_NAME}" { + !docs ../../docs +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/iso-8859.dsl b/structurizr-dsl/src/test/resources/dsl/iso-8859.dsl new file mode 100644 index 000000000..caad37c01 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/iso-8859.dsl @@ -0,0 +1,5 @@ +workspace { + model { + softwareSystem "Namé" + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/logo.png b/structurizr-dsl/src/test/resources/dsl/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..763d19bf5c7d827bec4f6c31ff8e8660f09aab54 GIT binary patch literal 9262 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 literal 0 HcmV?d00001 diff --git a/structurizr-dsl/src/test/resources/dsl/multi-line-with-error.dsl b/structurizr-dsl/src/test/resources/dsl/multi-line-with-error.dsl new file mode 100644 index 000000000..8c3290044 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/multi-line-with-error.dsl @@ -0,0 +1,12 @@ +workspace { + + model { + softwareSystem = \ + softwareSystem \ + "Software \ + System" { + component "Component" // components not permitted inside software systems + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/multi-line.dsl b/structurizr-dsl/src/test/resources/dsl/multi-line.dsl new file mode 100644 index 000000000..2fc5b87c6 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/multi-line.dsl @@ -0,0 +1,10 @@ +workspace { + + model { + softwareSystem = \ + softwareSystem \ + "Software \ + System" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/multiple-model-tokens.dsl b/structurizr-dsl/src/test/resources/dsl/multiple-model-tokens.dsl new file mode 100644 index 000000000..d75ee05f3 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/multiple-model-tokens.dsl @@ -0,0 +1,11 @@ +workspace { + + model { + person "User 1" + } + + model { + person "User 2" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/multiple-view-tokens.dsl b/structurizr-dsl/src/test/resources/dsl/multiple-view-tokens.dsl new file mode 100644 index 000000000..23c1a878f --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/multiple-view-tokens.dsl @@ -0,0 +1,19 @@ +workspace { + + model { + person "User 1" + } + + views { + systemLandscape { + include * + } + } + + views { + systemLandscape { + include * + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/multiple-workspace-tokens.dsl b/structurizr-dsl/src/test/resources/dsl/multiple-workspace-tokens.dsl new file mode 100644 index 000000000..391d88d81 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/multiple-workspace-tokens.dsl @@ -0,0 +1,15 @@ +workspace { + + model { + person "User 1" + } + +} + +workspace { + + model { + person "User 2" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/parallel1.dsl b/structurizr-dsl/src/test/resources/dsl/parallel1.dsl new file mode 100644 index 000000000..70fc06138 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/parallel1.dsl @@ -0,0 +1,33 @@ +workspace { + + model { + user = person "User" + softwareSystem = softwareSystem "Software System" { + webapp = container "Web Application" + bus = container "Message Bus" + app1 = container "App 1" + app2 = container "App 2" + } + + user -> webapp "Updates details" + webapp -> bus "Sends update event" + bus -> app1 "Broadcasts update event" + bus -> app2 "Broadcasts update event" + } + + views { + dynamic softwareSystem { + user -> webapp + webapp -> bus + { + bus -> app1 + } + { + bus -> app2 + } + + autoLayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/parallel2.dsl b/structurizr-dsl/src/test/resources/dsl/parallel2.dsl new file mode 100644 index 000000000..8d7d39e94 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/parallel2.dsl @@ -0,0 +1,34 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + c = softwareSystem "C" + d = softwareSystem "D" + e = softwareSystem "E" + + a -> b + b -> c + b -> d + b -> e + } + + views { + + dynamic * { + a -> b "Makes a request to" + { + { + b -> c "Gets data from" + } + { + b -> d "Gets data from" + } + } + b -> e "Sends data to" + + autoLayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/plugin-with-parameters.dsl b/structurizr-dsl/src/test/resources/dsl/plugin-with-parameters.dsl new file mode 100644 index 000000000..4314c7539 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/plugin-with-parameters.dsl @@ -0,0 +1,7 @@ +workspace { + + !plugin com.example.ExampleStructurizrDslPlugin { + name Java + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/plugin-without-parameters.dsl b/structurizr-dsl/src/test/resources/dsl/plugin-without-parameters.dsl new file mode 100644 index 000000000..2831b60dc --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/plugin-without-parameters.dsl @@ -0,0 +1,5 @@ +workspace { + + !plugin com.example.ExampleStructurizrDslPlugin + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/plugins/structurizr-dsl-plugin-1.0.0.jar b/structurizr-dsl/src/test/resources/dsl/plugins/structurizr-dsl-plugin-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..42234ce4b597ea68492ff64a8d4095fecdd73902 GIT binary patch literal 1214 zcmWIWW@h1HVBp|j5VBMXW&i>v5CH_7K<w)p;;8HC=cdoh5P+g^Q$5o=X`lj0AQnMZ z=<Dd`>E;?7qUY=O+4sz8A8%c~i@e^tTIbH3-yCFc#rVO~M^Bm13<K(i+sXoDC+Fv4 zSct5K2dXBuA~ClhCly2OG0A=G;XrR(1{$V|rq&grC%B}jG`Xa-D6^`_r8p-br!+k? zPcJzqvAB3@$ju_5KmptEX^JTvf;_V(NIaUfp{1v@Q1z*|ki3pU$igM3wx2HGR+mg$ z5oz*^zpi2aL+&WMAB_JdPqQ`FNOkDZ-~Rktkze`!cR#;9J{``m`$qR;At#|jMoJw< zXXbyn9&>T$#n8T6>Q1E+i>hZWwDgy7`st+ICy}OdB=w9_T0qF=<v04CuIkIqe6O=K z`L$PZiT#GJZSTrVlkc0z6uM;;g<Bk7R+{yFr<O$6r`A`MJ@=n^@f7nUHh*t9Zms{= zD2UOeIeJNH%h3(<*7r?P%RBobjO+0#i**T(PHx9SW0FtIO)r)8mH)MGf&Xm9g*QvX zulKJx9CyF+^ag{@7pXknQyv=|9og6;%6UC?jneTAKd$eRND6T}`-Wp~VyP2*)jZFS zvKsHDcW6GG)^j@|{e<7g)EN)w`Z-5z%QSf7^XBxE4+_ha6whyXxU<9f-s{&+mOM|E zs80<@IT2sNu)_Vf%h3sIvf5Vc-n(tXywBVGW`$f0*?I5Lv}bqQPi^?@wb-0PXrF_b z=q&%!s<ZC?iRk?=ams7qQH7QJ)k=Qvf4uU2zFYq6xkeY_HYWEwO}<vLqbllg?fweR z{(lii4)iMCGl}&U^*3E_zf*JXjZaA%+jwu~W-03KQqSD~Qs<(t;z<v;uK5S-em(QG zTD;Gw&1i1Xg9hCqw%Yp#zZv|nt!(#v=Edx>HAu_QVN=|{o-gb!!QEfaGH!axc&*R& z&V>HmE+^!YSU9boPjC~GT$15^$u8XRfOJu1y!DRP>T{ObU$K2OIkWtE!Tb+$6KciY zSQ`mVQ?*gqe!Pxx>iht2MkWzv+&K-Dz8M$<7~VR9Xn4Lu*M^?&K#D<NNuwr^glj{} zh3F=LRKv13D6>HT$l@{}6O{Gv=>TOt2mtAr1!RJBz%nAbPV|fj)4Qbc7@AIk*)qVJ Sl?|kw6$l*|85lB{K|BDw)SE;A literal 0 HcmV?d00001 diff --git a/structurizr-dsl/src/test/resources/dsl/ref.dsl b/structurizr-dsl/src/test/resources/dsl/ref.dsl new file mode 100644 index 000000000..cf6ce5a1f --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/ref.dsl @@ -0,0 +1,45 @@ +workspace extends amazon-web-services.dsl { + + model { + + !ref "DeploymentNode://Live/Amazon Web Services" { + deploymentNode "New deployment node" { + infrastructureNode "New infrastructure node" { + -> route53 + } + } + } + + !ref "DeploymentNode://Live/Amazon Web Services/US-East-1" { + deploymentNode "New deployment node 1" { + infrastructureNode "New infrastructure node 1" { + -> route53 + } + } + } + + !ref region { + deploymentNode "New deployment node 2" { + infrastructureNode "New infrastructure node 2" { + -> route53 + } + } + } + + !ref live { + deploymentNode "New deployment node 3" { + infrastructureNode "New infrastructure node 3" { + -> route53 + } + } + } + } + + views { + deployment * "Live" { + include * + autolayout lr + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/relationship-already-exists.dsl b/structurizr-dsl/src/test/resources/dsl/relationship-already-exists.dsl new file mode 100644 index 000000000..59030d423 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/relationship-already-exists.dsl @@ -0,0 +1,13 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" { + c = container "C" + } + + c -> a + b -> a + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/script-external-with-parameters.dsl b/structurizr-dsl/src/test/resources/dsl/script-external-with-parameters.dsl new file mode 100644 index 000000000..47002cedf --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/script-external-with-parameters.dsl @@ -0,0 +1,7 @@ +workspace { + + !script test.groovy { + "name" "Groovy" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/script-external.dsl b/structurizr-dsl/src/test/resources/dsl/script-external.dsl new file mode 100644 index 000000000..4e2d8988a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/script-external.dsl @@ -0,0 +1,7 @@ +workspace { + + !script test.groovy + !script test.kts + !script test.rb + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/script-in-dynamic-view.dsl b/structurizr-dsl/src/test/resources/dsl/script-in-dynamic-view.dsl new file mode 100644 index 000000000..f29f4e950 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/script-in-dynamic-view.dsl @@ -0,0 +1,16 @@ +workspace { + + model { + user = person "User" + softwareSystem = softwareSystem "Software System" + } + + views { + dynamic * "key" { + !script groovy { + view.description = "Groovy" + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/script-inline.dsl b/structurizr-dsl/src/test/resources/dsl/script-inline.dsl new file mode 100644 index 000000000..3325479af --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/script-inline.dsl @@ -0,0 +1,42 @@ +workspace { + + !script groovy { + println("Hello from Groovy"); + workspace.model.addPerson("Groovy"); + } + + !script kotlin { + println("Hello from Kotlin"); + workspace.model.addPerson("Kotlin"); + } + + !script ruby { + puts "Hello from Ruby" + workspace.model.addPerson("Ruby"); + } + + model { + user = person "User" { + !script groovy { + element.addTags("Groovy") + } + } + + softwareSystem "Software System" { + user -> this { + !script groovy { + relationship.addTags("Groovy") + } + } + } + } + + views { + systemLandscape { + !script groovy { + view.description = "Groovy" + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/shapes.dsl b/structurizr-dsl/src/test/resources/dsl/shapes.dsl new file mode 100644 index 000000000..4f408498d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/shapes.dsl @@ -0,0 +1,89 @@ +workspace "Shapes" "An example of all shapes available in Structurizr." { + + model { + softwareSystem "Box" "" "Box" + softwareSystem "RoundedBox" "" "RoundedBox" + softwareSystem "Diamond" "" "Diamond" + softwareSystem "Circle" "" "Circle" + softwareSystem "Ellipse" "" "Ellipse" + softwareSystem "Hexagon" "" "Hexagon" + softwareSystem "Folder" "" "Folder" + softwareSystem "Cylinder" "" "Cylinder" + softwareSystem "Pipe" "" "Pipe" + softwareSystem "WebBrowser" "" "Web Browser" + softwareSystem "Mobile Device Portrait" "" "Mobile Device Portrait" + softwareSystem "Mobile Device Landscape" "" "Mobile Device Landscape" + softwareSystem "Component" "" "Component" + person "Person" + softwareSystem "Robot" "" "Robot" + } + + views { + systemLandscape "shapes" "An example of all shapes available in Structurizr." { + include * + } + + styles { + element "Element" { + width "650" + height "400" + background "#438dd5" + color "#ffffff" + fontSize "34" + metadata "false" + description "false" + } + element "Box" { + shape "Box" + } + element "RoundedBox" { + shape "RoundedBox" + } + element "Diamond" { + shape "Diamond" + } + element "Circle" { + shape "Circle" + } + element "Ellipse" { + shape "Ellipse" + } + element "Hexagon" { + shape "Hexagon" + } + element "Folder" { + shape "Folder" + } + element "Cylinder" { + shape "Cylinder" + } + element "Pipe" { + shape "Pipe" + } + element "Web Browser" { + shape "WebBrowser" + } + element "Mobile Device Portrait" { + shape "MobileDevicePortrait" + width "400" + height "650" + } + element "Mobile Device Landscape" { + shape "MobileDeviceLandscape" + } + element "Component" { + shape "Component" + } + element "Person" { + shape "Person" + width "550" + } + element "Robot" { + shape "Robot" + width "550" + } + } + + } + +} diff --git a/structurizr-dsl/src/test/resources/dsl/test-with-parameters.groovy b/structurizr-dsl/src/test/resources/dsl/test-with-parameters.groovy new file mode 100644 index 000000000..fa7d24643 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/test-with-parameters.groovy @@ -0,0 +1,4 @@ +package dsl + +println("Hello from " + name); +workspace.model.addPerson(name); \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl new file mode 100644 index 000000000..cab02b522 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -0,0 +1,345 @@ +!constant ORGANISATION_NAME "Organisation" +!constant GROUP_NAME "Group" + +workspace "Name" "Description" { + + /* + multi-line comment + */ + + /** + multi-line comment + */ + + /* multi-line comment on single line */ + + /* multi-line comment + on two lines */ + + # single line comment + // single line comment + + model { + properties { + "Name" "Value" + } + + box1 = element "Box 1" "Metadata" "Description" "Tag" + box2 = element "Box 2" "Metadata" "Description" "Tag" + box1 -> box2 + + user = person "User" "Description" "Tag" { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + "Technical Debt" "Tech debt is high due to delivering feature X rapidly." "High" + } + } + + enterprise "${ORGANISATION_NAME} - ${GROUP_NAME}" { + softwareSystem = softwareSystem "Software System" "Description" "Tag" { + webApplication = container "Web Application" "Description" "Technology" "Tag" { + homePageController = component "HomePageController" "Description" "Spring MVC Controller" "Tag" { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + } + + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + } + + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + } + + softwareSystem "E-mail System" "Description" "Tag" + } + + user -> HomePageController "Visits" "HTTPS" "Tag" + + developmentEnvironment = deploymentEnvironment "Development" { + deploymentNode "Amazon Web Services" "Description" "Technology" "Tag" { + softwareSystemInstance softwareSystem { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + healthCheck "Check 1" "https://example.com/health" + healthCheck "Check 2" "https://example.com/health" 60 + healthCheck "Check 2" "https://example.com/health" 120 1000 + } + } + } + + deploymentEnvironment "Live" { + deploymentNode "Amazon Web Services" "Description" "Technology" "Tag" { + + infrastructureNode "Elastic Load Balancer" "Description" "Technology" "Tag" { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + } + + deploymentNode "Amazon Web Services - EC2" "Description" "Technology" "Tag" { + containerInstance webApplication { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + healthCheck "Check 1" "https://example.com/health" + healthCheck "Check 2" "https://example.com/health" 60 + healthCheck "Check 2" "https://example.com/health" 120 1000 + } + } + + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + + } + } + } + + views { + + custom "CustomDiagram" "Title" "Description" { + title "Title" + description "Description" + + include box1 box2 + + animation { + box1 + box2 + } + + autolayout + + properties { + "Name" "Value" + } + + default + } + + systemLandscape "SystemLandscape" "Description" { + title "Title" + description "Description" + + include * + autoLayout + + properties { + "Name" "Value" + } + + default + } + + systemContext softwareSystem "SystemContext" "Description" { + title "Title" + description "Description" + + include * + autoLayout + + properties { + "Name" "Value" + } + + default + } + + container softwareSystem "Containers" "Description" { + title "Title" + description "Description" + + include * + autoLayout + + properties { + "Name" "Value" + } + + default + } + + component webApplication "Components" "Description" { + title "Title" + description "Description" + + include * + autoLayout + + properties { + "Name" "Value" + } + + default + } + + filtered "SystemLandscape" include "Element,Relationship" "Filtered1" + + filtered "SystemLandscape" include "Element,Relationship" "Filtered2" { + title "Filtered view" + description "Description" + + properties { + "Name" "Value" + } + + default + } + + dynamic webApplication "Dynamic" "Description" { + title "Title" + description "Description" + + user -> homePageController "Requests via web browser" + homePageController -> user { + url "https://structurizr.com" + } + + autoLayout + + properties { + "Name" "Value" + } + + default + } + + deployment * developmentEnvironment "Deployment-Development" "Description" { + title "Title" + description "Description" + + include * + autoLayout + + properties { + "Name" "Value" + } + + default + } + + deployment * "Live" "Deployment-Live" "Description" { + title "Title" + description "Description" + + include * + autoLayout + + properties { + "Name" "Value" + } + + default + } + + styles { + element "Element" { + shape roundedbox + icon logo.png + width 450 + height 300 + background #ffffff + color #000000 + colour #000000 + stroke #777777 + fontSize 24 + border solid + opacity 50 + metadata false + description false + properties { + "Name" "Value" + } + } + + relationship "Relationship" { + thickness 2 + color #777777 + colour #777777 + dashed true + routing curved + fontSize 24 + width 400 + position 50 + opacity 50 + properties { + "Name" "Value" + } + } + + theme https://example.com/theme1 + themes https://example.com/theme2 https://example.com/theme3 + } + + theme https://example.com/theme1 + themes https://example.com/theme2 https://example.com/theme3 + + branding { + logo logo.png + font "Example" https://example/com/font + } + + terminology { + enterprise "Enterprise" + person "Person" + softwareSystem "Software System" + container "Container" + component "Component" + deploymentNode "Deployment Node" + infrastructureNode "Infrastructure Node" + relationship "Relationship" + } + + properties { + "Name" "Value" + } + } + + configuration { + users { + user1@example.com read + user2@example.com write + } + + visibility public + scope softwaresystem + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.groovy b/structurizr-dsl/src/test/resources/dsl/test.groovy new file mode 100644 index 000000000..eefdea320 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/test.groovy @@ -0,0 +1,4 @@ +package dsl + +println("Hello from Groovy"); +workspace.model.addPerson("Groovy"); \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.js b/structurizr-dsl/src/test/resources/dsl/test.js new file mode 100644 index 000000000..7b6a7297d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/test.js @@ -0,0 +1,2 @@ +print("Hello from JavaScript"); +workspace.model.addPerson("JavaScript"); \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.kts b/structurizr-dsl/src/test/resources/dsl/test.kts new file mode 100644 index 000000000..ba1ad34fe --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/test.kts @@ -0,0 +1,2 @@ +println("Hello from Kotlin"); +workspace.model.addPerson("Kotlin"); \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.rb b/structurizr-dsl/src/test/resources/dsl/test.rb new file mode 100644 index 000000000..8b17cc3d4 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/test.rb @@ -0,0 +1,2 @@ +puts "Hello from JRuby" +workspace.model.addPerson("Ruby") \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/this.dsl b/structurizr-dsl/src/test/resources/dsl/this.dsl new file mode 100644 index 000000000..acc5f9af5 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/this.dsl @@ -0,0 +1,71 @@ +workspace { + + model { + custom = element "Element" + + s = softwareSystem "Software System" { + custom -> this + + c = container "Container" { + custom -> this + + component "Component" { + custom -> this + } + } + } + + live = deploymentEnvironment "live" { + deploymentNode "Live" { + in = infrastructureNode "Infrastructure Node" { + custom -> this + } + + dn = deploymentNode "Deployment Node" { + in -> this + + softwareSystemInstance s { + in -> this + } + + containerInstance c { + in -> this + } + } + } + } + } + + views { + systemLandscape { + include * + include custom + autolayout + } + + systemContext s { + include * + include custom + autolayout + } + + container s { + include * + include custom + autolayout + } + + component c { + include * + include custom + autolayout + } + + deployment * live { + include * + include custom + autolayout + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-after-workspace.dsl b/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-after-workspace.dsl new file mode 100644 index 000000000..b63613317 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-after-workspace.dsl @@ -0,0 +1,4 @@ +workspace { +} + +hello world \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-before-workspace.dsl b/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-before-workspace.dsl new file mode 100644 index 000000000..1144a9886 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-before-workspace.dsl @@ -0,0 +1,4 @@ +hello world + +workspace { +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-in-workspace.dsl b/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-in-workspace.dsl new file mode 100644 index 000000000..2ab0050d4 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/unexpected-tokens-in-workspace.dsl @@ -0,0 +1,5 @@ +workspace { + + softwareSystem "Name" + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/utf8.dsl b/structurizr-dsl/src/test/resources/dsl/utf8.dsl new file mode 100644 index 000000000..9a00ab44d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/utf8.dsl @@ -0,0 +1,7 @@ +workspace { + + model { + user = person "你好 Usér 🙂" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/views-without-keys.dsl b/structurizr-dsl/src/test/resources/dsl/views-without-keys.dsl new file mode 100644 index 000000000..ec9074d36 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/views-without-keys.dsl @@ -0,0 +1,18 @@ +workspace { + + model { + person "User" + } + + views { + systemLandscape { + } + + systemLandscape { + } + + systemLandscape { + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/workspace-properties.dsl b/structurizr-dsl/src/test/resources/dsl/workspace-properties.dsl new file mode 100644 index 000000000..15b7ff9c7 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/workspace-properties.dsl @@ -0,0 +1,7 @@ +workspace { + + properties { + "structurizr.dslEditor" "false" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl b/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl new file mode 100644 index 000000000..c5556fd63 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl @@ -0,0 +1,29 @@ +workspace "Getting Started" "This is a model of my software system." { + + model { + user = person "User" "A user of my software system." + softwareSystem = softwareSystem "Software System" "My software system, code-named \"X\"." + + user -> softwareSystem "Uses" + } + + views { + systemContext softwareSystem "SystemContext" "An example of a System Context diagram." { + include * + autoLayout + } + + styles { + element "Software System" { + background #1168bd + color #ffffff + } + element "Person" { + shape person + background #08427b + color #ffffff + } + } + } + +} \ No newline at end of file diff --git a/structurizr-export/README.md b/structurizr-export/README.md new file mode 100644 index 000000000..7ad1a2da5 --- /dev/null +++ b/structurizr-export/README.md @@ -0,0 +1,9 @@ +# structurizr-export + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-export.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-export) + +This library provides the ability to export the model and views defined in a Structurizr workspace to a number of formats, +including PlantUML and C4-PlantUML, Mermaid, DOT, WebSequenceDiagrams, and Ilograph. + +- [Structurizr DSL demo page](https://structurizr.com/dsl) (demo of export formats) +- [Documentation](https://docs.structurizr.com/export) diff --git a/structurizr-export/build.gradle b/structurizr-export/build.gradle new file mode 100644 index 000000000..0729fcb26 --- /dev/null +++ b/structurizr-export/build.gradle @@ -0,0 +1,10 @@ +dependencies { + + api project(':structurizr-core') + + testImplementation project(':structurizr-client') + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + +} + +description = 'Export Structurizr models and views to external formats' diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java new file mode 100644 index 000000000..9c2202bd8 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -0,0 +1,748 @@ +package com.structurizr.export; + +import com.structurizr.Workspace; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public abstract class AbstractDiagramExporter extends AbstractExporter implements DiagramExporter { + + protected static final String GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator"; + + private Object frame = null; + + /** + * Exports all views in the workspace. + * + * @param workspace the workspace containing the views to be written + * @return a collection of diagram definitions, one per view + */ + public final Collection<Diagram> export(Workspace workspace) { + if (workspace == null) { + throw new IllegalArgumentException("A workspace must be provided."); + } + + Collection<Diagram> diagrams = new ArrayList<>(); + + for (CustomView view : workspace.getViews().getCustomViews()) { + Diagram diagram = export(view); + if (diagram != null) { + diagrams.add(diagram); + } + } + + for (SystemLandscapeView view : workspace.getViews().getSystemLandscapeViews()) { + Diagram diagram = export(view); + if (diagram != null) { + diagrams.add(diagram); + } + } + + for (SystemContextView view : workspace.getViews().getSystemContextViews()) { + Diagram diagram = export(view); + if (diagram != null) { + diagrams.add(diagram); + } + } + + for (ContainerView view : workspace.getViews().getContainerViews()) { + Diagram diagram = export(view); + if (diagram != null) { + diagrams.add(diagram); + } + } + + for (ComponentView view : workspace.getViews().getComponentViews()) { + Diagram diagram = export(view); + if (diagram != null) { + diagrams.add(diagram); + } + } + + for (DynamicView view : workspace.getViews().getDynamicViews()) { + Diagram diagram = export(view); + if (diagram != null) { + diagrams.add(diagram); + } + } + + for (DeploymentView view : workspace.getViews().getDeploymentViews()) { + Diagram diagram = export(view); + if (diagram != null) { + diagrams.add(diagram); + } + } + + return diagrams; + } + + public Diagram export(CustomView view) { + Diagram diagram = export(view, null); + + if (isAnimationSupported(view) && !view.getAnimations().isEmpty()) { + for (Animation animation : view.getAnimations()) { + Diagram frame = export(view, animation.getOrder()); + diagram.addFrame(frame); + } + } + + diagram.setLegend(createLegend(view)); + return diagram; + } + + private Diagram export(CustomView view, Integer animationStep) { + this.frame = animationStep; + + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + List<GroupableElement> elements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + elements.add((CustomElement)elementView.getElement()); + } + + writeElements(view, elements, writer); + + writer.writeLine(); + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + public Diagram export(SystemLandscapeView view) { + Diagram diagram = export(view, null); + + if (isAnimationSupported(view) && !view.getAnimations().isEmpty()) { + for (Animation animation : view.getAnimations()) { + Diagram frame = export(view, animation.getOrder()); + diagram.addFrame(frame); + } + } + + diagram.setLegend(createLegend(view)); + return diagram; + } + + private Diagram export(SystemLandscapeView view, Integer animationStep) { + this.frame = animationStep; + return export(view, view.isEnterpriseBoundaryVisible()); + } + + public Diagram export(SystemContextView view) { + Diagram diagram = export(view, null); + + if (isAnimationSupported(view) && !view.getAnimations().isEmpty()) { + for (Animation animation : view.getAnimations()) { + Diagram frame = export(view, animation.getOrder()); + diagram.addFrame(frame); + } + } + + diagram.setLegend(createLegend(view)); + return diagram; + } + + private Diagram export(SystemContextView view, Integer animationStep) { + this.frame = animationStep; + return export(view, view.isEnterpriseBoundaryVisible()); + } + + private Diagram export(ModelView view, boolean enterpriseBoundaryIsVisible) { + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + boolean showEnterpriseBoundary = + enterpriseBoundaryIsVisible && + (view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof Person && ((Person)e).getLocation() == Location.Internal) || + view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof SoftwareSystem && ((SoftwareSystem)e).getLocation() == Location.Internal)); + + if (showEnterpriseBoundary) { + String enterpriseName = "Enterprise"; + if (view.getModel().getEnterprise() != null) { + enterpriseName = view.getModel().getEnterprise().getName(); + } + + startEnterpriseBoundary(view, enterpriseName, writer); + + List<GroupableElement> elementsInsideEnterpriseBoundary = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() == Location.Internal) { + elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); + } + if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() == Location.Internal) { + elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); + } + } + writeElements(view, elementsInsideEnterpriseBoundary, writer); + + endEnterpriseBoundary(view, writer); + + List<GroupableElement> elementsOutsideEnterpriseBoundary = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() != Location.Internal) { + elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); + } + if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() != Location.Internal) { + elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); + } + if (elementView.getElement() instanceof CustomElement) { + elementsOutsideEnterpriseBoundary.add((CustomElement)elementView.getElement()); + } + } + writeElements(view, elementsOutsideEnterpriseBoundary, writer); + } else { + List<GroupableElement> elements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + elements.add((GroupableElement)elementView.getElement()); + } + writeElements(view, elements, writer); + } + + writer.writeLine(); + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + public Diagram export(ContainerView view) { + Diagram diagram = export(view, null); + + if (isAnimationSupported(view) && !view.getAnimations().isEmpty()) { + for (Animation animation : view.getAnimations()) { + Diagram frame = export(view, animation.getOrder()); + diagram.addFrame(frame); + } + } + + diagram.setLegend(createLegend(view)); + return diagram; + } + + public Diagram export(ContainerView view, Integer animationStep) { + this.frame = animationStep; + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + boolean elementsWritten = false; + for (ElementView elementView : view.getElements()) { + if (!(elementView.getElement() instanceof Container)) { + writeElement(view, elementView.getElement(), writer); + elementsWritten = true; + } + } + + if (elementsWritten) { + writer.writeLine(); + } + + List<SoftwareSystem> softwareSystems = getBoundarySoftwareSystems(view); + for (SoftwareSystem softwareSystem : softwareSystems) { + startSoftwareSystemBoundary(view, softwareSystem, writer); + + List<GroupableElement> scopedElements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + if (elementView.getElement().getParent() == softwareSystem) { + scopedElements.add((StaticStructureElement) elementView.getElement()); + } + } + + writeElements(view, scopedElements, writer); + + endSoftwareSystemBoundary(view, writer); + } + + writeRelationships(view, writer); + + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + protected List<SoftwareSystem> getBoundarySoftwareSystems(ModelView view) { + List<SoftwareSystem> softwareSystems = new ArrayList<>(view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Container).map(c -> ((Container)c).getSoftwareSystem()).collect(Collectors.toSet())); + softwareSystems.sort(Comparator.comparing(Element::getId)); + + return softwareSystems; + } + + public Diagram export(ComponentView view) { + Diagram diagram = export(view, null); + + if (isAnimationSupported(view) && !view.getAnimations().isEmpty()) { + for (Animation animation : view.getAnimations()) { + Diagram frame = export(view, animation.getOrder()); + diagram.addFrame(frame); + } + } + + diagram.setLegend(createLegend(view)); + return diagram; + } + + public Diagram export(ComponentView view, Integer animationStep) { + this.frame = animationStep; + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + boolean elementsWritten = false; + for (ElementView elementView : view.getElements()) { + if (!(elementView.getElement() instanceof Component)) { + writeElement(view, elementView.getElement(), writer); + elementsWritten = true; + } + } + + if (elementsWritten) { + writer.writeLine(); + } + + boolean includeSoftwareSystemBoundaries = "true".equals(view.getProperties().getOrDefault("structurizr.softwareSystemBoundaries", "false")); + + List<Container> containers = getBoundaryContainers(view); + Set<SoftwareSystem> softwareSystems = containers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + for (SoftwareSystem softwareSystem : softwareSystems) { + + if (includeSoftwareSystemBoundaries) { + startSoftwareSystemBoundary(view, softwareSystem, writer); + writer.indent(); + } + + for (Container container : containers) { + if (container.getSoftwareSystem() == softwareSystem) { + startContainerBoundary(view, container, writer); + + List<GroupableElement> scopedElements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + if (elementView.getElement().getParent() == container) { + scopedElements.add((StaticStructureElement) elementView.getElement()); + } + } + writeElements(view, scopedElements, writer); + + endContainerBoundary(view, writer); + } + } + + if (includeSoftwareSystemBoundaries) { + endSoftwareSystemBoundary(view, writer); + writer.outdent(); + } + } + + writeRelationships(view, writer); + + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + protected List<Container> getBoundaryContainers(ModelView view) { + List<Container> containers = new ArrayList<>(view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Component).map(c -> ((Component)c).getContainer()).collect(Collectors.toSet())); + containers.sort(Comparator.comparing(Element::getId)); + + return containers; + } + + public Diagram export(DynamicView view) { + Diagram diagram = export(view, null); + + if (isAnimationSupported(view)) { + LinkedHashSet<String> orders = new LinkedHashSet<>(); + for (RelationshipView relationshipView : view.getRelationships()) { + orders.add(relationshipView.getOrder()); + } + + for (String order : orders) { + Diagram frame = export(view, order); + diagram.addFrame(frame); + } + } + + diagram.setLegend(createLegend(view)); + return diagram; + } + + public Diagram export(DynamicView view, String order) { + this.frame = order; + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + boolean elementsWritten = false; + + Element element = view.getElement(); + + if (element == null) { + // dynamic view with no scope + List<GroupableElement> elements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + elements.add((StaticStructureElement) elementView.getElement()); + } + writeElements(view, elements, writer); + } else { + if (element instanceof SoftwareSystem) { + // dynamic view with software system scope + List<SoftwareSystem> softwareSystems = getBoundarySoftwareSystems(view); + for (SoftwareSystem softwareSystem : softwareSystems) { + startSoftwareSystemBoundary(view, softwareSystem, writer); + + List<GroupableElement> scopedElements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + if (elementView.getElement().getParent() == softwareSystem) { + scopedElements.add((StaticStructureElement) elementView.getElement()); + } + } + + writeElements(view, scopedElements, writer); + + endSoftwareSystemBoundary(view, writer); + } + + for (ElementView elementView : view.getElements()) { + if (elementView.getElement().getParent() == null) { + writeElement(view, elementView.getElement(), writer); + elementsWritten = true; + } + } + } else if (element instanceof Container) { + // dynamic view with container scope + boolean includeSoftwareSystemBoundaries = "true".equals(view.getProperties().getOrDefault("structurizr.softwareSystemBoundaries", "false")); + + List<Container> containers = getBoundaryContainers(view); + Set<SoftwareSystem> softwareSystems = containers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + for (SoftwareSystem softwareSystem : softwareSystems) { + + if (includeSoftwareSystemBoundaries) { + startSoftwareSystemBoundary(view, softwareSystem, writer); + writer.indent(); + } + + for (Container container : containers) { + if (container.getSoftwareSystem() == softwareSystem) { + startContainerBoundary(view, container, writer); + + List<GroupableElement> scopedElements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + if (elementView.getElement().getParent() == container) { + scopedElements.add((StaticStructureElement) elementView.getElement()); + } + } + writeElements(view, scopedElements, writer); + + endContainerBoundary(view, writer); + } + } + + if (includeSoftwareSystemBoundaries) { + endSoftwareSystemBoundary(view, writer); + writer.outdent(); + } + } + + for (ElementView elementView : view.getElements()) { + if (!(elementView.getElement().getParent() instanceof Container)) { + writeElement(view, elementView.getElement(), writer); + elementsWritten = true; + } + } + } + } + + if (elementsWritten) { + writer.writeLine(); + } + + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + public Diagram export(DeploymentView view) { + Diagram diagram = export(view, null); + + if (isAnimationSupported(view) && !view.getAnimations().isEmpty()) { + for (Animation animation : view.getAnimations()) { + Diagram frame = export(view, animation.getOrder()); + diagram.addFrame(frame); + } + } + + diagram.setLegend(createLegend(view)); + return diagram; + } + + public Diagram export(DeploymentView view, Integer animationStep) { + this.frame = animationStep; + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + List<GroupableElement> elements = new ArrayList<>(); + + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof DeploymentNode && elementView.getElement().getParent() == null) { + elements.add((DeploymentNode)elementView.getElement()); + } + } + + writeElements(view, elements, writer); + + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + protected void writeElements(ModelView view, List<GroupableElement> elements, IndentingWriter writer) { + String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); + boolean nested = !StringUtils.isNullOrEmpty(groupSeparator); + + elements.sort(Comparator.comparing(Element::getId)); + + Set<String> groupsAsSet = new HashSet<>(); + for (GroupableElement element : elements) { + String group = element.getGroup(); + + if (!StringUtils.isNullOrEmpty(group)) { + groupsAsSet.add(group); + + if (nested) { + while (group.contains(groupSeparator)) { + group = group.substring(0, group.lastIndexOf(groupSeparator)); + groupsAsSet.add(group); + } + } + } + } + + List<String> groupsAsList = new ArrayList<>(groupsAsSet); + Collections.sort(groupsAsList); + + // first render grouped elements + if (groupsAsList.size() > 0) { + if (nested) { + String context = ""; + + for (String group : groupsAsList) { + int groupCount = group.split(Pattern.quote(groupSeparator)).length; + int contextCount = context.split(Pattern.quote(groupSeparator)).length; + + if (groupCount > contextCount) { + // moved from a to a/b + // - increase padding + writer.indent(); + } else if (groupCount == contextCount) { + // moved from a/b to a/c + // - close off previous subgraph + if (context.length() > 1) { + endGroupBoundary(view, writer); + } + } else { + // moved from a/b/c to a/b or a + // - close off previous subgraphs + // - close off current subgraph + for (int i = 0; i < (contextCount - groupCount); i++) { + endGroupBoundary(view, writer); + writer.outdent(); + } + endGroupBoundary(view, writer); + } + + startGroupBoundary(view, group, writer); + + for (GroupableElement element : elements) { + if (group.equals(element.getGroup())) { + write(view, element, writer); + } + } + + context = group; + } + + int contextCount = context.split(Pattern.quote(groupSeparator)).length; + for (int i = 0; i < contextCount; i++) { + endGroupBoundary(view, writer); + + if (i < contextCount-1) { + writer.outdent(); + } + } + } else { + for (String group : groupsAsList) { + startGroupBoundary(view, group, writer); + + for (GroupableElement element : elements) { + if (group.equals(element.getGroup())) { + write(view, element, writer); + } + } + + endGroupBoundary(view, writer); + } + } + } + + // then render ungrouped elements + for (GroupableElement element : elements) { + if (StringUtils.isNullOrEmpty(element.getGroup())) { + write(view, element, writer); + } + } + } + + protected void writeRelationships(ModelView view, IndentingWriter writer) { + Collection<RelationshipView> relationshipList; + + if (view instanceof DynamicView) { + relationshipList = view.getRelationships(); + } else { + relationshipList = view.getRelationships().stream().sorted(Comparator.comparing(rv -> rv.getRelationship().getId())).collect(Collectors.toList()); + } + + for (RelationshipView relationshipView : relationshipList) { + writeRelationship(view, relationshipView, writer); + } + } + + protected abstract void writeHeader(ModelView view, IndentingWriter writer); + protected abstract void writeFooter(ModelView view, IndentingWriter writer); + + protected abstract void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer); + protected abstract void endEnterpriseBoundary(ModelView view, IndentingWriter writer); + + protected abstract void startGroupBoundary(ModelView view, String group, IndentingWriter writer); + protected abstract void endGroupBoundary(ModelView view, IndentingWriter writer); + + protected abstract void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer); + protected abstract void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer); + + protected abstract void startContainerBoundary(ModelView view, Container container, IndentingWriter writer); + protected abstract void endContainerBoundary(ModelView view, IndentingWriter writer); + + protected abstract void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer); + protected abstract void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer); + + private void write(ModelView view, Element element, IndentingWriter writer) { + if (view instanceof DeploymentView && element instanceof DeploymentNode) { + writeDeploymentNode((DeploymentView)view, (DeploymentNode)element, writer); + } else { + writeElement(view, element, writer); + } + } + + private void writeDeploymentNode(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { + startDeploymentNodeBoundary(view, deploymentNode, writer); + + List<GroupableElement> elements = new ArrayList<>(); + + List<DeploymentNode> children = new ArrayList<>(deploymentNode.getChildren()); + children.sort(Comparator.comparing(DeploymentNode::getName)); + for (DeploymentNode child : children) { + if (view.isElementInView(child)) { + elements.add(child); + } + } + + List<InfrastructureNode> infrastructureNodes = new ArrayList<>(deploymentNode.getInfrastructureNodes()); + infrastructureNodes.sort(Comparator.comparing(InfrastructureNode::getName)); + for (InfrastructureNode infrastructureNode : infrastructureNodes) { + if (view.isElementInView(infrastructureNode)) { + elements.add(infrastructureNode); + } + } + + List<SoftwareSystemInstance> softwareSystemInstances = new ArrayList<>(deploymentNode.getSoftwareSystemInstances()); + softwareSystemInstances.sort(Comparator.comparing(SoftwareSystemInstance::getName)); + for (SoftwareSystemInstance softwareSystemInstance : softwareSystemInstances) { + if (view.isElementInView(softwareSystemInstance)) { + elements.add(softwareSystemInstance); + } + } + + List<ContainerInstance> containerInstances = new ArrayList<>(deploymentNode.getContainerInstances()); + containerInstances.sort(Comparator.comparing(ContainerInstance::getName)); + for (ContainerInstance containerInstance : containerInstances) { + if (view.isElementInView(containerInstance)) { + elements.add(containerInstance); + } + } + + writeElements(view, elements, writer); + + endDeploymentNodeBoundary(view, writer); + } + + protected abstract void writeElement(ModelView view, Element element, IndentingWriter writer); + protected abstract void writeRelationship(ModelView view, RelationshipView relationshipView, IndentingWriter writer); + + protected boolean isAnimationSupported(ModelView view) { + return false; + } + + protected boolean isVisible(ModelView view, Element element) { + if (frame != null) { + Set<String> elementIds = new HashSet<>(); + + if (view instanceof StaticView) { + int step = (int)frame; + if (step > 0) { + StaticView staticView = (StaticView) view; + staticView.getAnimations().stream().filter(a -> a.getOrder() <= step).forEach(a -> { + elementIds.addAll(a.getElements()); + }); + + return elementIds.contains(element.getId()); + } + } else if (view instanceof DeploymentView) { + int step = (int)frame; + if (step > 0) { + DeploymentView deploymentView = (DeploymentView) view; + deploymentView.getAnimations().stream().filter(a -> a.getOrder() <= step).forEach(a -> { + elementIds.addAll(a.getElements()); + }); + + return elementIds.contains(element.getId()); + } + } else if (view instanceof DynamicView) { + String order = (String)frame; + view.getRelationships().stream().filter(rv -> order.equals(rv.getOrder())).forEach(rv -> { + elementIds.add(rv.getRelationship().getSourceId()); + elementIds.add(rv.getRelationship().getDestinationId()); + }); + + return elementIds.contains(element.getId()); + } + } + + return true; + } + + protected boolean isVisible(ModelView view, RelationshipView relationshipView) { + if (view instanceof DynamicView && frame != null) { + return frame.equals(relationshipView.getOrder()); + } + + return true; + } + + protected abstract Diagram createDiagram(ModelView view, String definition); + + protected Legend createLegend(ModelView view) { + return null; + } + + protected String getViewOrViewSetProperty(ModelView view, String name, String defaultValue) { + ViewSet views = view.getViewSet(); + + return + view.getProperties().getOrDefault(name, + views.getConfiguration().getProperties().getOrDefault(name, defaultValue) + ); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractExporter.java new file mode 100644 index 000000000..d58d573d6 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractExporter.java @@ -0,0 +1,119 @@ +package com.structurizr.export; + +import com.structurizr.Workspace; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +public abstract class AbstractExporter { + + protected String breakText(int maxWidth, int fontSize, String s) { + if (StringUtils.isNullOrEmpty(s)) { + return ""; + } + + StringBuilder buf = new StringBuilder(); + + double characterWidth = fontSize * 0.6; + int maxCharacters = (int)(maxWidth / characterWidth); + + if (s.length() < maxCharacters) { + return s; + } + + String[] words = s.split(" "); + String line = null; + for (String word : words) { + if (line == null) { + line = word; + } else { + if ((line.length() + word.length() + 1) < maxCharacters) { + line += " "; + line += word; + } else { + buf.append(line); + buf.append("<br />"); + line = word; + } + } + } + + if (line != null) { + buf.append(line); + } + + return buf.toString(); + } + + protected String typeOf(Workspace workspace, Element e, boolean includeMetadataSymbols) { + return typeOf(workspace.getViews().getConfiguration(), e, includeMetadataSymbols); + } + + protected String typeOf(ModelView view, Element e, boolean includeMetadataSymbols) { + return typeOf(view.getViewSet().getConfiguration(), e, includeMetadataSymbols); + } + + private String typeOf(Configuration configuration, Element e, boolean includeMetadataSymbols) { + String type = ""; + + if (e instanceof Person) { + type = configuration.getTerminology().findTerminology(e); + } else if (e instanceof SoftwareSystem) { + type = configuration.getTerminology().findTerminology(e); + } else if (e instanceof Container) { + Container container = (Container)e; + type = configuration.getTerminology().findTerminology(e) + (hasValue(container.getTechnology()) ? ": " + container.getTechnology() : ""); + } else if (e instanceof Component) { + Component component = (Component)e; + type = configuration.getTerminology().findTerminology(e) + (hasValue(component.getTechnology()) ? ": " + component.getTechnology() : ""); + } else if (e instanceof DeploymentNode) { + DeploymentNode deploymentNode = (DeploymentNode)e; + type = configuration.getTerminology().findTerminology(e) + (hasValue(deploymentNode.getTechnology()) ? ": " + deploymentNode.getTechnology() : ""); + } else if (e instanceof InfrastructureNode) { + InfrastructureNode infrastructureNode = (InfrastructureNode)e; + type = configuration.getTerminology().findTerminology(e) + (hasValue(infrastructureNode.getTechnology()) ? ": " + infrastructureNode.getTechnology() : ""); + } else if (e instanceof CustomElement) { + type = ((CustomElement)e).getMetadata(); + } + + if (StringUtils.isNullOrEmpty(type)) { + return type; + } + + if (includeMetadataSymbols) { + if (configuration.getMetadataSymbols() == null) { + configuration.setMetadataSymbols(MetadataSymbols.SquareBrackets); + } + + switch (configuration.getMetadataSymbols()) { + case RoundBrackets: + return "(" + type + ")"; + case CurlyBrackets: + return "{" + type + "}"; + case AngleBrackets: + return "<" + type + ">"; + case DoubleAngleBrackets: + return "<<" + type + ">>"; + case None: + return type; + default: + return "[" + type + "]"; + } + } else { + return type; + } + } + + protected boolean hasValue(String s) { + return !StringUtils.isNullOrEmpty(s); + } + + protected ElementStyle findElementStyle(ModelView view, Element element) { + return view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + } + + protected RelationshipStyle findRelationshipStyle(ModelView view, Relationship relationship) { + return view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractWorkspaceExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractWorkspaceExporter.java new file mode 100644 index 000000000..6190149ea --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractWorkspaceExporter.java @@ -0,0 +1,4 @@ +package com.structurizr.export; + +public abstract class AbstractWorkspaceExporter extends AbstractExporter implements WorkspaceExporter { +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/Diagram.java b/structurizr-export/src/main/java/com/structurizr/export/Diagram.java new file mode 100644 index 000000000..271b7b925 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/Diagram.java @@ -0,0 +1,51 @@ +package com.structurizr.export; + +import com.structurizr.view.View; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Diagram { + + private View view; + private String definition; + + private List<Diagram> frames = new ArrayList<>(); + private Legend legend; + + public Diagram(View view, String definition) { + this.view = view; + this.definition = definition; + } + + public String getKey() { + return view.getKey(); + } + + public View getView() { + return view; + } + + public String getDefinition() { + return definition; + } + + public void addFrame(Diagram frame) { + frames.add(frame); + } + + public List<Diagram> getFrames() { + return new ArrayList<>(frames); + } + + public Legend getLegend() { + return legend; + } + + public void setLegend(Legend legend) { + this.legend = legend; + } + + public abstract String getFileExtension(); + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/DiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/DiagramExporter.java new file mode 100644 index 000000000..73a43ad5b --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/DiagramExporter.java @@ -0,0 +1,17 @@ +package com.structurizr.export; + +import com.structurizr.Workspace; + +import java.util.Collection; + +public interface DiagramExporter extends Exporter { + + /** + * Exports all views in the workspace. + * + * @param workspace the workspace containing the views to be written + * @return a collection of diagram definitions, one per view + */ + Collection<Diagram> export(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/Exporter.java b/structurizr-export/src/main/java/com/structurizr/export/Exporter.java new file mode 100644 index 000000000..45bf1b263 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/Exporter.java @@ -0,0 +1,4 @@ +package com.structurizr.export; + +public interface Exporter { +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/IndentType.java b/structurizr-export/src/main/java/com/structurizr/export/IndentType.java new file mode 100644 index 000000000..16534cb5c --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/IndentType.java @@ -0,0 +1,8 @@ +package com.structurizr.export; + +public enum IndentType { + + Spaces, + Tabs + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java b/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java new file mode 100644 index 000000000..f7bf0a0bc --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java @@ -0,0 +1,62 @@ +package com.structurizr.export; + +public final class IndentingWriter { + + private int indent = 0; + private IndentType indentType = IndentType.Spaces; + private int indentQuantity = 2; + + private StringBuilder buf = new StringBuilder(); + + public IndentingWriter() { + } + + public void setIndentType(IndentType indentType) { + this.indentType = indentType; + } + + public void setIndentQuantity(int indentQuantity) { + this.indentQuantity = indentQuantity; + } + + public void indent() { + indent++; + } + + public void outdent() { + indent--; + } + + private String padding() { + StringBuilder buf = new StringBuilder(); + + for (int i = 0; i < indent * indentQuantity; i++) { + if (indentType == IndentType.Spaces) { + buf.append(" "); + } else { + buf.append("\t"); + } + } + + return buf.toString(); + } + + public void writeLine() { + buf.append("\n"); + } + + public void writeLine(String content) { + buf.append(String.format("%s%s\n", padding(), content.replace("\n", "\\n"))); + } + + @Override + public String toString() { + String s = buf.toString(); + if (s.endsWith("\n")) { + s = s.substring(0, s.length()-1); + } + + return s; + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/Legend.java b/structurizr-export/src/main/java/com/structurizr/export/Legend.java new file mode 100644 index 000000000..49e552381 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/Legend.java @@ -0,0 +1,15 @@ +package com.structurizr.export; + +public final class Legend { + + private final String definition; + + public Legend(String definition) { + this.definition = definition; + } + + public String getDefinition() { + return definition; + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/WorkspaceExport.java b/structurizr-export/src/main/java/com/structurizr/export/WorkspaceExport.java new file mode 100644 index 000000000..1f25745fd --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/WorkspaceExport.java @@ -0,0 +1,17 @@ +package com.structurizr.export; + +public abstract class WorkspaceExport { + + private String definition; + + public WorkspaceExport(String definition) { + this.definition = definition; + } + + public String getDefinition() { + return definition; + } + + public abstract String getFileExtension(); + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/WorkspaceExporter.java b/structurizr-export/src/main/java/com/structurizr/export/WorkspaceExporter.java new file mode 100644 index 000000000..bd59cb3a5 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/WorkspaceExporter.java @@ -0,0 +1,15 @@ +package com.structurizr.export; + +import com.structurizr.Workspace; + +public interface WorkspaceExporter extends Exporter { + + /** + * Exports the entire workspace to a single String. + * + * @param workspace the workspace to be exported + * @return a String export of the workspace + */ + WorkspaceExport export(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/dot/DOTDiagram.java b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTDiagram.java new file mode 100644 index 000000000..a66af0ea9 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTDiagram.java @@ -0,0 +1,17 @@ +package com.structurizr.export.dot; + +import com.structurizr.export.Diagram; +import com.structurizr.view.ModelView; + +public class DOTDiagram extends Diagram { + + public DOTDiagram(ModelView view, String definition) { + super(view, definition); + } + + @Override + public String getFileExtension() { + return "dot"; + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java new file mode 100644 index 000000000..b4671c4ca --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java @@ -0,0 +1,434 @@ +package com.structurizr.export.dot; + +import com.structurizr.export.AbstractDiagramExporter; +import com.structurizr.export.Diagram; +import com.structurizr.export.IndentingWriter; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +/** + * Exports Structurizr views to Graphviz DOT definitions. + */ +public class DOTExporter extends AbstractDiagramExporter { + + private static final String DEFAULT_FONT = "Arial"; + + private int clusterInternalMargin = 25; + + public DOTExporter() { + } + + public void setClusterInternalMargin(int clusterInternalMargin) { + this.clusterInternalMargin = clusterInternalMargin; + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + String title = view.getTitle(); + if (StringUtils.isNullOrEmpty(title)) { + title = view.getName(); + } + + String description = view.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + description = ""; + } else { + description = String.format("<br /><font point-size=\"24\">%s</font>", description); + } + + String fontName = DEFAULT_FONT; + Font font = view.getViewSet().getConfiguration().getBranding().getFont(); + if (font != null) { + fontName = font.getName(); + } + + RankDirection rankDirection = RankDirection.TopBottom; + + if (view.getAutomaticLayout() != null) { + switch (view.getAutomaticLayout().getRankDirection()) { + case TopBottom: + rankDirection = RankDirection.TopBottom; + break; + case BottomTop: + rankDirection = RankDirection.BottomTop; + break; + case LeftRight: + rankDirection = RankDirection.LeftRight; + break; + case RightLeft: + rankDirection = RankDirection.RightLeft; + break; + } + } + + writer.writeLine("digraph {"); + writer.indent(); + writer.writeLine("compound=true"); + writer.writeLine(String.format("graph [fontname=\"%s\", rankdir=%s, ranksep=1.0, nodesep=1.0]", fontName, rankDirection.getCode())); + writer.writeLine(String.format("node [fontname=\"%s\", shape=box, margin=\"0.4,0.3\"]", fontName)); + writer.writeLine(String.format("edge [fontname=\"%s\"]", fontName)); + writer.writeLine(String.format("label=<<br /><font point-size=\"34\">%s</font>%s>", title, description)); + writer.writeLine(); + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + } + + @Override + protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { + writer.writeLine("subgraph cluster_enterprise {"); + + writer.indent(); + writer.writeLine("margin=" + clusterInternalMargin); + writer.writeLine(String.format("label=<<font point-size=\"24\"><br />%s</font><br /><font point-size=\"19\">[Enterprise]</font>>", enterpriseName)); + writer.writeLine("labelloc=b"); + writer.writeLine("color=\"#444444\""); + writer.writeLine("fontcolor=\"#444444\""); + writer.writeLine("fillcolor=\"#ffffff\""); + writer.writeLine(); + } + + @Override + protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + String color = "#cccccc"; + + String groupName = group; + + String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); + if (!StringUtils.isNullOrEmpty(groupSeparator)) { + groupName = group.substring(group.lastIndexOf(groupSeparator) + groupSeparator.length()); + } + + // is there a style for the group? + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group:" + group); + + if (elementStyle == null || StringUtils.isNullOrEmpty(elementStyle.getColor())) { + // no, so is there a default group style? + elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group"); + } + + if (elementStyle != null && !StringUtils.isNullOrEmpty(elementStyle.getColor())) { + color = elementStyle.getColor(); + } + + writer.writeLine("subgraph \"cluster_group_" + groupName + "\" {"); + + writer.indent(); + writer.writeLine("margin=" + clusterInternalMargin); + writer.writeLine(String.format("label=<<font point-size=\"24\"><br />%s</font>>", groupName)); + writer.writeLine("labelloc=b"); + writer.writeLine(String.format("color=\"%s\"", color)); + writer.writeLine(String.format("fontcolor=\"%s\"", color)); + writer.writeLine("fillcolor=\"#ffffff\""); + writer.writeLine("style=\"dashed\""); + writer.writeLine(); + } + + @Override + protected void endGroupBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { + String color; + if (softwareSystem.equals(view.getSoftwareSystem())) { + color = "#444444"; + } else { + color = "#cccccc"; + } + + writer.writeLine(String.format("subgraph cluster_%s {", softwareSystem.getId())); + writer.indent(); + writer.writeLine("margin=" + clusterInternalMargin); + writer.writeLine(String.format("label=<<font point-size=\"24\"><br />%s</font><br /><font point-size=\"19\">%s</font>>", softwareSystem.getName(), typeOf(view, softwareSystem, true))); + writer.writeLine("labelloc=b"); + writer.writeLine(String.format("color=\"%s\"", color)); + writer.writeLine(String.format("fontcolor=\"%s\"", color)); + writer.writeLine(String.format("fillcolor=\"%s\"", color)); + writer.writeLine(); + } + + @Override + protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { + String color = "#444444"; + if (view instanceof ComponentView) { + if (container.equals(((ComponentView)view).getContainer())) { + color = "#444444"; + } else { + color = "#cccccc"; + } + } else if (view instanceof DynamicView) { + if (container.equals(((DynamicView)view).getElement())) { + color = "#444444"; + } else { + color = "#cccccc"; + } + } + + writer.writeLine(String.format("subgraph cluster_%s {", container.getId())); + writer.indent(); + writer.writeLine("margin=" + clusterInternalMargin); + writer.writeLine(String.format("label=<<font point-size=\"24\"><br />%s</font><br /><font point-size=\"19\">%s</font>>", container.getName(), typeOf(view, container, true))); + writer.writeLine("labelloc=b"); + writer.writeLine(String.format("color=\"%s\"", color)); + writer.writeLine(String.format("fontcolor=\"%s\"", color)); + writer.writeLine(String.format("fillcolor=\"%s\"", color)); + writer.writeLine(); + } + + @Override + protected void endContainerBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(deploymentNode); + + writer.writeLine(String.format("subgraph cluster_%s {", deploymentNode.getId())); + writer.indent(); + writer.writeLine("margin=" + clusterInternalMargin); + writer.writeLine(String.format("label=<<font point-size=\"24\">%s</font><br /><font point-size=\"19\">%s</font>>", deploymentNode.getName(), typeOf(view, deploymentNode, true))); + writer.writeLine("labelloc=b"); + writer.writeLine(String.format("color=\"%s\"", elementStyle.getStroke())); + writer.writeLine(String.format("fontcolor=\"%s\"", elementStyle.getColor())); + writer.writeLine("fillcolor=\"#ffffff\""); + writer.writeLine(); + } + + @Override + protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void writeElement(ModelView view, Element element, IndentingWriter writer) { + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + + int nameFontSize = elementStyle.getFontSize() + 10; + int metadataFontSize = elementStyle.getFontSize() - 5; + int descriptionFontSize = elementStyle.getFontSize(); + + + String shape = shapeOf(view, element); + String name = element.getName(); + String description = element.getDescription(); + String type = typeOf(view, element, true); + + if (element instanceof StaticStructureElementInstance) { + StaticStructureElementInstance elementInstance = (StaticStructureElementInstance)element; + name = elementInstance.getElement().getName(); + description = elementInstance.getElement().getDescription(); + type = typeOf(view, elementInstance.getElement(), true); + shape = shapeOf(view, elementInstance.getElement()); + } + + if (StringUtils.isNullOrEmpty(name)) { + name = ""; + } else { + name = String.format("<font point-size=\"%s\">%s</font>", nameFontSize, breakText(elementStyle.getWidth(), nameFontSize, escape(name))); + } + + if (StringUtils.isNullOrEmpty(description) || false == elementStyle.getDescription()) { + description = ""; + } else { + description = String.format("<br /><br /><font point-size=\"%s\">%s</font>", descriptionFontSize, breakText(elementStyle.getWidth(), descriptionFontSize, escape(description))); + } + + if (StringUtils.isNullOrEmpty(type) || false == elementStyle.getMetadata()) { + type = ""; + } else { + type = String.format("<br /><font point-size=\"%s\">%s</font>", metadataFontSize, type); + } + + writer.writeLine(String.format("%s [id=%s,shape=%s, label=<%s%s%s>, style=filled, color=\"%s\", fillcolor=\"%s\", fontcolor=\"%s\"]", + element.getId(), + element.getId(), + shape, + name, + type, + description, + elementStyle.getStroke(), + elementStyle.getBackground(), + elementStyle.getColor() + )); + } + + @Override + protected void writeRelationship(ModelView view, RelationshipView relationshipView, IndentingWriter writer) { + Element source; + Element destination; + + RelationshipStyle relationshipStyle = view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationshipView.getRelationship()); + relationshipStyle.setWidth(400); + int descriptionFontSize = relationshipStyle.getFontSize(); + int metadataFontSize = relationshipStyle.getFontSize() - 5; + + String description = relationshipView.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + description = relationshipView.getRelationship().getDescription(); + } + + if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { + description = relationshipView.getOrder() + ". " + description; + } + + if (StringUtils.isNullOrEmpty(description)) { + description = ""; + } else { + description = breakText(relationshipStyle.getWidth(), descriptionFontSize, description); + description = String.format("<font point-size=\"%s\">%s</font>", descriptionFontSize, description); + } + + String technology = relationshipView.getRelationship().getTechnology(); + if (StringUtils.isNullOrEmpty(technology)) { + technology = ""; + } else { + technology = String.format("<br /><font point-size=\"%s\">[%s]</font>", metadataFontSize, technology); + } + + String clusterConfig = ""; + + if (relationshipView.getRelationship().getSource() instanceof DeploymentNode || relationshipView.getRelationship().getDestination() instanceof DeploymentNode) { + source = relationshipView.getRelationship().getSource(); + if (source instanceof DeploymentNode) { + source = findElementInside((DeploymentNode)source, view); + } + + destination = relationshipView.getRelationship().getDestination(); + if (destination instanceof DeploymentNode) { + destination = findElementInside((DeploymentNode)destination, view); + } + + if (source != null && destination != null) { + + if (relationshipView.getRelationship().getSource() instanceof DeploymentNode) { + clusterConfig += ",ltail=cluster_" + relationshipView.getRelationship().getSource().getId(); + } + + if (relationshipView.getRelationship().getDestination() instanceof DeploymentNode) { + clusterConfig += ",lhead=cluster_" + relationshipView.getRelationship().getDestination().getId(); + } + } + } else { + source = relationshipView.getRelationship().getSource(); + destination = relationshipView.getRelationship().getDestination(); + + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + source = relationshipView.getRelationship().getDestination(); + destination = relationshipView.getRelationship().getSource(); + } + } + + boolean solid = relationshipStyle.getStyle() == LineStyle.Solid || false == relationshipStyle.getDashed(); + + writer.writeLine(String.format("%s -> %s [id=%s, label=<%s%s>, style=\"%s\", color=\"%s\", fontcolor=\"%s\"%s]", + source.getId(), + destination.getId(), + relationshipView.getId(), + description, + technology, + solid ? "solid" : "dashed", + relationshipStyle.getColor(), + relationshipStyle.getColor(), + clusterConfig + )); + } + + private String escape(String s) { + if (StringUtils.isNullOrEmpty(s)) { + return s; + } else { + return s.replaceAll("\"", "\\\\\""); + } + } + + private String shapeOf(ModelView view, Element element) { + if (element instanceof DeploymentNode) { + return "node"; + } + + Shape shape = view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getShape(); + switch(shape) { + case Circle: + return "circle"; + case Component: + return "component"; + case Cylinder: + return "cylinder"; + case Ellipse: + return "ellipse"; + case Folder: + return "folder"; + case Hexagon: + return "hexagon"; + case Diamond: + return "diamond"; + default: + return "rect"; + } + } + + private Element findElementInside(DeploymentNode deploymentNode, ModelView view) { + for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { + if (view.isElementInView(softwareSystemInstance)) { + return softwareSystemInstance; + } + } + + for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { + if (view.isElementInView(containerInstance)) { + return containerInstance; + } + } + + for (InfrastructureNode infrastructureNode : deploymentNode.getInfrastructureNodes()) { + if (view.isElementInView(infrastructureNode)) { + return infrastructureNode; + } + } + + if (deploymentNode.hasChildren()) { + for (DeploymentNode child : deploymentNode.getChildren()) { + Element element = findElementInside(child, view); + + if (element != null) { + return element; + } + } + } + + return null; + } + + @Override + protected Diagram createDiagram(ModelView view, String definition) { + return new DOTDiagram(view, definition); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/dot/README.md b/structurizr-export/src/main/java/com/structurizr/export/dot/README.md new file mode 100644 index 000000000..f528413dc --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/dot/README.md @@ -0,0 +1,6 @@ +# DOT (Graphviz) + +The [DOTExporter](DOTExporter.java) class provides a way to export views to +diagram definitions that are compatible with [Graphviz](https://graphviz.org). + +See https://docs.structurizr.com/export for more. \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/dot/RankDirection.java b/structurizr-export/src/main/java/com/structurizr/export/dot/RankDirection.java new file mode 100644 index 000000000..e79fef327 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/dot/RankDirection.java @@ -0,0 +1,23 @@ +package com.structurizr.export.dot; + +/** + * The various rank directions used by Graphviz. + */ +enum RankDirection { + + TopBottom("TB"), + BottomTop("BT"), + LeftRight("LR"), + RightLeft("RL"); + + private String code; + + RankDirection(String code) { + this.code = code; + } + + String getCode() { + return code; + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java new file mode 100644 index 000000000..b091d40dc --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java @@ -0,0 +1,391 @@ +package com.structurizr.export.ilograph; + +import com.structurizr.Workspace; +import com.structurizr.export.AbstractWorkspaceExporter; +import com.structurizr.export.IndentingWriter; +import com.structurizr.export.WorkspaceExport; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Exports a Structurizr workspace to the Ilograph definition language, for use with https://app.ilograph.com/ + */ +public class IlographExporter extends AbstractWorkspaceExporter { + + public WorkspaceExport export(Workspace workspace) { + IndentingWriter writer = new IndentingWriter(); + writer.writeLine("resources:"); + writer.writeLine(); + writer.indent(); + + Model model = workspace.getModel(); + List<GroupableElement> elements = new ArrayList<>(); + + List<CustomElement> customElements = new ArrayList<>(model.getCustomElements()); + customElements.sort(Comparator.comparing(CustomElement::getId)); + for (CustomElement customElement : customElements) { + writeElement(writer, workspace, customElement); + elements.add(customElement); + } + + List<Person> people = new ArrayList<>(model.getPeople()); + people.sort(Comparator.comparing(Person::getId)); + for (Person person : people) { + writeElement(writer, workspace, person); + elements.add(person); + } + + List<SoftwareSystem> softwareSystems = new ArrayList<>(model.getSoftwareSystems()); + softwareSystems.sort(Comparator.comparing(SoftwareSystem::getId)); + for (SoftwareSystem softwareSystem : softwareSystems) { + writeElement(writer, workspace, softwareSystem); + elements.add(softwareSystem); + + if (!softwareSystem.getContainers().isEmpty()) { + writer.indent(); + writer.writeLine("children:"); + writer.indent(); + + List<Container> containers = new ArrayList<>(softwareSystem.getContainers()); + containers.sort(Comparator.comparing(Container::getId)); + for (Container container : containers) { + writeElement(writer, workspace, container); + elements.add(container); + + if (!container.getComponents().isEmpty()) { + writer.indent(); + writer.writeLine("children:"); + writer.indent(); + + List<Component> components = new ArrayList<>(container.getComponents()); + components.sort(Comparator.comparing(Component::getId)); + for (Component component : components) { + writeElement(writer, workspace, component); + elements.add(component); + } + + writer.outdent(); + writer.outdent(); + } + + } + + writer.outdent(); + writer.outdent(); + } + } + + List<DeploymentNode> deploymentNodes = new ArrayList<>(model.getDeploymentNodes()); + deploymentNodes.sort(Comparator.comparing(DeploymentNode::getId)); + for (DeploymentNode deploymentNode : deploymentNodes) { + writeDeploymentNode(workspace, deploymentNode, writer); + } + + Set<Relationship> relationships = new LinkedHashSet<>(); + Set<Class> elementTypes = new HashSet<>(); + + elementTypes.add(CustomElement.class); + elementTypes.add(Person.class); + elementTypes.add(SoftwareSystem.class); + for (GroupableElement element : elements) { + List<Relationship> sortedRelationships = new ArrayList<>(element.getRelationships()); + sortedRelationships.sort(Comparator.comparing(Relationship::getId)); + for (Relationship relationship : sortedRelationships) { + if (include(relationship, elementTypes)) { + relationships.add(relationship); + } + } + } + + elementTypes.add(Container.class); + for (GroupableElement element : elements) { + List<Relationship> sortedRelationships = new ArrayList<>(element.getRelationships()); + sortedRelationships.sort(Comparator.comparing(Relationship::getId)); + for (Relationship relationship : sortedRelationships) { + if (include(relationship, elementTypes)) { + relationships.add(relationship); + } + } + } + + elementTypes.add(Component.class); + for (GroupableElement element : elements) { + List<Relationship> sortedRelationships = new ArrayList<>(element.getRelationships()); + sortedRelationships.sort(Comparator.comparing(Relationship::getId)); + for (Relationship relationship : sortedRelationships) { + if (include(relationship, elementTypes)) { + relationships.add(relationship); + } + } + } + + writer.outdent(); + + writeRelationshipsForStaticStructurePerspective(workspace.getViews().getConfiguration(), relationships, writer); + + for (DynamicView dynamicView : workspace.getViews().getDynamicViews()) { + writeDynamicView(dynamicView, writer); + } + + Set<String> deploymentEnvironments = new HashSet<>(); + for (DeploymentNode deploymentNode : model.getDeploymentNodes()) { + deploymentEnvironments.add(deploymentNode.getEnvironment()); + } + List<String> sortedDeploymentEnvironments = new ArrayList<>(deploymentEnvironments); + sortedDeploymentEnvironments.sort(Comparator.comparing(String::toString)); + for (String deploymentEnvironment : sortedDeploymentEnvironments) { + writeDeploymentEnvironment(workspace, deploymentEnvironment, writer); + } + + return new IlographWorkspaceExport(writer.toString()); + } + + private void writeDeploymentNode(Workspace workspace, DeploymentNode deploymentNode, IndentingWriter writer) { + writeElement(writer, workspace, deploymentNode); + + boolean hasChildren = !deploymentNode.getChildren().isEmpty() || !deploymentNode.getInfrastructureNodes().isEmpty() || !deploymentNode.getSoftwareSystemInstances().isEmpty() || !deploymentNode.getContainerInstances().isEmpty(); + + if (hasChildren) { + writer.indent(); + writer.writeLine("children:"); + writer.indent(); + } + + List<DeploymentNode> deploymentNodes = new ArrayList<>(deploymentNode.getChildren()); + deploymentNodes.sort(Comparator.comparing(DeploymentNode::getId)); + for (DeploymentNode child : deploymentNodes) { + writeDeploymentNode(workspace, child, writer); + } + + List<InfrastructureNode> infrastructureNodes = new ArrayList<>(deploymentNode.getInfrastructureNodes()); + infrastructureNodes.sort(Comparator.comparing(InfrastructureNode::getId)); + for (InfrastructureNode infrastructureNode : infrastructureNodes) { + writeElement(writer, workspace, infrastructureNode); + } + + List<SoftwareSystemInstance> softwareSystemInstances = new ArrayList<>(deploymentNode.getSoftwareSystemInstances()); + softwareSystemInstances.sort(Comparator.comparing(SoftwareSystemInstance::getId)); + for (SoftwareSystemInstance softwareSystemInstance : softwareSystemInstances) { + writeElement(writer, workspace, softwareSystemInstance); + } + + List<ContainerInstance> containerInstances = new ArrayList<>(deploymentNode.getContainerInstances()); + containerInstances.sort(Comparator.comparing(ContainerInstance::getId)); + for (ContainerInstance containerInstance : containerInstances) { + writeElement(writer, workspace, containerInstance); + } + + writer.outdent(); + writer.outdent(); + } + + private void writeElement(IndentingWriter writer, Workspace workspace, Element element) { + writer.writeLine(String.format("- id: \"%s\"", element.getId())); + + String name; + String type; + String description; + ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().findElementStyle(element); + + if (element instanceof StaticStructureElementInstance) { + StaticStructureElementInstance elementInstance = (StaticStructureElementInstance)element; + name = elementInstance.getElement().getName(); + type = typeOf(workspace, elementInstance.getElement(), true); + description = elementInstance.getElement().getDescription(); + } else { + name = element.getName(); + type = typeOf(workspace, element, true); + description = element.getDescription(); + } + + writer.indent(); + writer.writeLine(String.format("name: \"%s\"", name)); + writer.writeLine(String.format("subtitle: \"%s\"", type)); + + if (!StringUtils.isNullOrEmpty(description)) { + writer.writeLine(String.format("description: \"%s\"", description)); + } + + if (element instanceof DeploymentNode) { + writer.writeLine(String.format("backgroundColor: \"%s\"", "#ffffff")); + } else { + writer.writeLine(String.format("backgroundColor: \"%s\"", elementStyle.getBackground())); + } + writer.writeLine(String.format("color: \"%s\"", elementStyle.getColor())); + writer.writeLine(); + writer.outdent(); + } + + private void writeRelationshipsForStaticStructurePerspective(Configuration configuration, Collection<Relationship> relationships, IndentingWriter writer) { + writer.writeLine("perspectives:"); + writer.indent(); + writer.writeLine("- name: Static Structure"); + writer.indent(); + writer.writeLine("relations:"); + writer.indent(); + + for (Relationship relationship : relationships) { + RelationshipStyle relationshipStyle = configuration.getStyles().findRelationshipStyle(relationship); + + writer.writeLine(String.format("- from: \"%s\"", relationship.getSourceId())); + writer.indent(); + writer.writeLine(String.format("to: \"%s\"", relationship.getDestinationId())); + + if (!StringUtils.isNullOrEmpty(relationship.getDescription())) { + writer.writeLine(String.format("label: \"%s\"", relationship.getDescription())); + } + + if (!StringUtils.isNullOrEmpty(relationship.getTechnology())) { + writer.writeLine(String.format("description: \"%s\"", relationship.getTechnology())); + } + + if (!StringUtils.isNullOrEmpty(relationshipStyle.getColor())) { + writer.writeLine(String.format("color: \"%s\"", relationshipStyle.getColor())); + } + + writer.writeLine(); + writer.outdent(); + } + + writer.outdent(); + writer.outdent(); + writer.outdent(); + } + + private void writeDynamicView(DynamicView dynamicView, IndentingWriter writer) { + writer.indent(); + writer.writeLine("- name: Dynamic - " + dynamicView.getName()); + writer.indent(); + writer.writeLine("sequence:"); + + int count = 0; + for (RelationshipView relationshipView : dynamicView.getRelationships()) { + Relationship relationship = relationshipView.getRelationship(); + RelationshipStyle relationshipStyle = dynamicView.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship); + + if (count == 0) { + writer.indent(); + writer.writeLine(String.format("start: \"%s\"", relationship.getSourceId())); + writer.writeLine("steps:"); + writer.writeLine(String.format("- to: \"%s\"", relationship.getDestinationId())); + } else { + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + writer.writeLine(String.format("- to: \"%s\"", relationship.getSourceId())); + } else { + writer.writeLine(String.format("- to: \"%s\"", relationship.getDestinationId())); + } + } + + writer.indent(); + if (!StringUtils.isNullOrEmpty(relationshipView.getDescription())) { + writer.writeLine(String.format("label: \"%s. %s\"", relationshipView.getOrder(), relationshipView.getDescription())); + } else if (!StringUtils.isNullOrEmpty(relationship.getDescription())) { + writer.writeLine(String.format("label: \"%s. %s\"", relationshipView.getOrder(), relationship.getDescription())); + } + + if (!StringUtils.isNullOrEmpty(relationship.getTechnology())) { + writer.writeLine(String.format("description: \"%s\"", relationship.getTechnology())); + } + + if (!StringUtils.isNullOrEmpty(relationshipStyle.getColor())) { + writer.writeLine(String.format("color: \"%s\"", relationshipStyle.getColor())); + } + + writer.outdent(); + + writer.writeLine(); + + count++; + } + + writer.outdent(); + writer.outdent(); + writer.outdent(); + } + + private void writeDeploymentEnvironment(Workspace workspace, String deploymentEnvironment, IndentingWriter writer) { + writer.indent(); + writer.writeLine("- name: Deployment - " + deploymentEnvironment); + writer.indent(); + writer.writeLine("relations:"); + + List<DeploymentNode> topLevelDeploymentNodes = workspace.getModel().getDeploymentNodes().stream().filter(dn -> dn.getEnvironment().equals(deploymentEnvironment)).sorted(Comparator.comparing(DeploymentNode::getId)).collect(Collectors.toList()); + List<Element> deploymentElementsInEnvironment = new ArrayList<>(topLevelDeploymentNodes); + for (DeploymentNode deploymentNode : topLevelDeploymentNodes) { + deploymentElementsInEnvironment.addAll(findAllChildren(deploymentNode)); + } + + Collection<Relationship> relationships = findRelationships(deploymentElementsInEnvironment); + writer.indent(); + + for (Relationship relationship : relationships) { + RelationshipStyle relationshipStyle = workspace.getViews().getConfiguration().getStyles().findRelationshipStyle(relationship); + + writer.writeLine(String.format("- from: \"%s\"", relationship.getSourceId())); + writer.indent(); + writer.writeLine(String.format("to: \"%s\"", relationship.getDestinationId())); + + if (!StringUtils.isNullOrEmpty(relationship.getDescription())) { + writer.writeLine(String.format("label: \"%s\"", relationship.getDescription())); + } + + if (!StringUtils.isNullOrEmpty(relationship.getTechnology())) { + writer.writeLine(String.format("description: \"%s\"", relationship.getTechnology())); + } + + if (!StringUtils.isNullOrEmpty(relationshipStyle.getColor())) { + writer.writeLine(String.format("color: \"%s\"", relationshipStyle.getColor())); + } + + writer.outdent(); + } + + writer.outdent(); + writer.outdent(); + writer.outdent(); + } + + private Collection<Element> findAllChildren(DeploymentNode deploymentNode) { + List<Element> deploymentElements = new ArrayList<>(); + + List<DeploymentNode> deploymentNodes = new ArrayList<>(deploymentNode.getChildren()); + deploymentNodes.sort(Comparator.comparing(DeploymentNode::getId)); + for (DeploymentNode child : deploymentNodes) { + deploymentElements.addAll(findAllChildren(child)); + } + + deploymentElements.addAll(deploymentNode.getSoftwareSystemInstances().stream().sorted(Comparator.comparing(SoftwareSystemInstance::getId)).collect(Collectors.toList())); + deploymentElements.addAll(deploymentNode.getContainerInstances().stream().sorted(Comparator.comparing(ContainerInstance::getId)).collect(Collectors.toList())); + deploymentElements.addAll(deploymentNode.getInfrastructureNodes().stream().sorted(Comparator.comparing(InfrastructureNode::getId)).collect(Collectors.toList())); + + return deploymentElements; + } + + private Collection<Relationship> findRelationships(Collection<Element> elements) { + List<Relationship> relationships = new ArrayList<>(); + + for (Element element : elements) { + List<Relationship> sortedRelationships = new ArrayList<>(element.getRelationships()); + sortedRelationships.sort(Comparator.comparing(Relationship::getId)); + for (Relationship relationship : sortedRelationships) { + if (elements.contains(relationship.getSource()) && elements.contains(relationship.getDestination())) { + relationships.add(relationship); + } + } + } + + return relationships; + } + + private boolean include(Relationship relationship, Set<Class> elementTypes) { + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + return elementTypes.contains(source.getClass()) && elementTypes.contains(destination.getClass()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographWorkspaceExport.java b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographWorkspaceExport.java new file mode 100644 index 000000000..a1ae4942c --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographWorkspaceExport.java @@ -0,0 +1,16 @@ +package com.structurizr.export.ilograph; + +import com.structurizr.export.WorkspaceExport; + +public class IlographWorkspaceExport extends WorkspaceExport { + + public IlographWorkspaceExport(String definition) { + super(definition); + } + + @Override + public String getFileExtension() { + return "idl"; + } + +} diff --git a/structurizr-export/src/main/java/com/structurizr/export/ilograph/README.md b/structurizr-export/src/main/java/com/structurizr/export/ilograph/README.md new file mode 100644 index 000000000..22ddfa09b --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/README.md @@ -0,0 +1,7 @@ +# Ilograph + +The [IlographExporter](IlographExporter.java) class provides a way to export the software architecture model +to the YAML format used by [Ilograph](https://www.ilograph.com), which provides an interactive way to explore +a hierarchical dataset (which the C4 model is). + +See https://docs.structurizr.com/export for more. \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagram.java b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagram.java new file mode 100644 index 000000000..9863eef19 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagram.java @@ -0,0 +1,17 @@ +package com.structurizr.export.mermaid; + +import com.structurizr.export.Diagram; +import com.structurizr.view.ModelView; + +public class MermaidDiagram extends Diagram { + + public MermaidDiagram(ModelView view, String definition) { + super(view, definition); + } + + @Override + public String getFileExtension() { + return "mmd"; + } + +} diff --git a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java new file mode 100644 index 000000000..54b6b2652 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java @@ -0,0 +1,412 @@ +package com.structurizr.export.mermaid; + +import com.structurizr.export.AbstractDiagramExporter; +import com.structurizr.export.Diagram; +import com.structurizr.export.IndentingWriter; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static java.lang.String.format; + +/** + * Exports diagram definitions that can be used to create diagrams + * using mermaid (https://mermaidjs.github.io). + * + * System landscape, system context, container, component, dynamic and deployment diagrams are supported. + * Deployment node -> deployment node relationships are not rendered. + */ +public class MermaidDiagramExporter extends AbstractDiagramExporter { + + public static final String MERMAID_TITLE_PROPERTY = "mermaid.title"; + public static final String MERMAID_SEQUENCE_DIAGRAM_PROPERTY = "mermaid.sequenceDiagram"; + public static final String MERMAID_ICONS_PROPERTY = "mermaid.icons"; + + private int groupId = 0; + + public MermaidDiagramExporter() { + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + groupId = 0; + String direction = "TB"; + + if (view.getAutomaticLayout() != null) { + switch (view.getAutomaticLayout().getRankDirection()) { + case TopBottom: + direction = "TB"; + break; + case BottomTop: + direction = "BT"; + break; + case LeftRight: + direction = "LR"; + break; + case RightLeft: + direction = "RL"; + break; + } + } + + writer.writeLine("graph " + direction); + writer.indent(); + writer.writeLine("linkStyle default fill:#ffffff"); + writer.writeLine(); + + String viewTitle = " "; + if (includeTitle(view)) { + viewTitle = view.getTitle(); + if (StringUtils.isNullOrEmpty(viewTitle)) { + viewTitle = view.getName(); + } + } + + writer.writeLine("subgraph diagram [\"" + viewTitle + "\"]"); + writer.indent(); + writer.writeLine("style diagram fill:#ffffff,stroke:#ffffff"); + writer.writeLine(); + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("end"); + writer.outdent(); + } + + @Override + protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { + writer.writeLine("subgraph enterprise [" + enterpriseName + "]"); + writer.indent(); + writer.writeLine("style enterprise fill:#ffffff,stroke:#444444,color:#444444"); + writer.writeLine(); + } + + @Override + protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("end"); + writer.writeLine(); + } + + @Override + protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + groupId++; + + String groupName = group; + + String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); + if (!StringUtils.isNullOrEmpty(groupSeparator)) { + groupName = group.substring(group.lastIndexOf(groupSeparator) + groupSeparator.length()); + } + + String color = "#cccccc"; + + // is there a style for the group? + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group:" + group); + + if (elementStyle == null || StringUtils.isNullOrEmpty(elementStyle.getColor())) { + // no, so is there a default group style? + elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group"); + } + + if (elementStyle != null && !StringUtils.isNullOrEmpty(elementStyle.getColor())) { + color = elementStyle.getColor(); + } + + writer.writeLine(String.format("subgraph group%s [" + groupName + "]", groupId)); + writer.indent(); + writer.writeLine(String.format("style group%s fill:#ffffff,stroke:%s,color:%s,stroke-dasharray:5", groupId, color, color)); + writer.writeLine(); + } + + @Override + protected void endGroupBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("end"); + writer.writeLine(); + } + + @Override + protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(softwareSystem); + String color = elementStyle.getStroke(); + + writer.writeLine(String.format("subgraph %s [%s]", softwareSystem.getId(), softwareSystem.getName())); + writer.indent(); + writer.writeLine(String.format("style %s fill:#ffffff,stroke:%s,color:%s", softwareSystem.getId(), color, color)); + writer.writeLine(); + } + + @Override + protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("end"); + writer.writeLine(); + } + + @Override + protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(container); + String color = elementStyle.getStroke(); + + writer.writeLine(String.format("subgraph %s [%s]", container.getId(), container.getName())); + writer.indent(); + writer.writeLine(String.format("style %s fill:#ffffff,stroke:%s,color:%s", container.getId(), color, color)); + writer.writeLine(); + } + + @Override + protected void endContainerBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("end"); + writer.writeLine(); + } + + @Override + protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(deploymentNode); + + writer.writeLine(String.format("subgraph %s [%s]", deploymentNode.getId(), deploymentNode.getName())); + writer.indent(); + writer.writeLine(String.format("style %s fill:#ffffff,stroke:%s,color:%s", deploymentNode.getId(), elementStyle.getStroke(), elementStyle.getColor())); + writer.writeLine(); + } + + @Override + protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("end"); + writer.writeLine(); + } + + @Override + public Diagram export(DynamicView view) { + if (renderAsSequenceDiagram(view)) { + IndentingWriter writer = new IndentingWriter(); + writer.writeLine("sequenceDiagram"); + writer.writeLine(); + writer.indent(); + + Set<Element> elements = new LinkedHashSet<>(); + for (RelationshipView relationshipView : view.getRelationships()) { + elements.add(relationshipView.getRelationship().getSource()); + elements.add(relationshipView.getRelationship().getDestination()); + } + + for (Element element : elements) { + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + String shape = "participant"; + if (elementStyle.getShape() == Shape.Person) { + shape = "actor"; + } + + String type = typeOf(view, element, true); + + if (StringUtils.isNullOrEmpty(type) || false == elementStyle.getMetadata()) { + type = ""; + } else { + type = "<br />" + type; + } + + writer.writeLine(String.format("%s %s as %s%s", shape, element.getId(), element.getName(), type)); + } + + writer.writeLine(); + + for (RelationshipView relationshipView : view.getRelationships()) { + Relationship relationship = relationshipView.getRelationship(); + RelationshipStyle style = view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship); + + String description = relationshipView.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + description = relationship.getDescription(); + } + + String sourceId = relationship.getSourceId(); + String destinationId = relationship.getDestinationId(); + + if (relationshipView.isResponse()) { + sourceId = relationship.getDestinationId(); + destinationId = relationship.getSourceId(); + } + + String technology = !StringUtils.isNullOrEmpty(relationship.getTechnology()) ? "<br />[" + relationship.getTechnology() + "]" : ""; + + String arrow; + + if (!relationshipView.isResponse()) { + arrow = "->>"; + } else { + arrow = "-->>"; + } + + writer.writeLine(String.format("%s%s%s: %s%s", + sourceId, + arrow, + destinationId, + description, + technology)); + } + + return createDiagram(view, writer.toString()); + } else { + return super.export(view); + } + } + + @Override + protected void writeElement(ModelView view, Element element, IndentingWriter writer) { + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + + String name = element.getName(); + String description = element.getDescription(); + String type = typeOf(view, element, true); + String icon = ""; + + if (element instanceof StaticStructureElementInstance) { + StaticStructureElementInstance elementInstance = (StaticStructureElementInstance)element; + name = elementInstance.getElement().getName(); + description = elementInstance.getElement().getDescription(); + type = typeOf(view, elementInstance.getElement(), true); + } + + String nodeOpeningSymbol = "["; + String nodeClosingSymbol = "]"; + + if (elementStyle.getShape() == Shape.RoundedBox) { + nodeOpeningSymbol = "("; + nodeClosingSymbol = ")"; + } else if (elementStyle.getShape() == Shape.Cylinder) { + nodeOpeningSymbol = "[("; + nodeClosingSymbol = ")]"; + } + + if (StringUtils.isNullOrEmpty(description) || false == elementStyle.getDescription()) { + description = ""; + } else { + description = String.format("<div style='font-size: 80%%; margin-top:10px'>%s</div>", lines(description)); + } + + if (false == elementStyle.getMetadata()) { + type = ""; + } else { + type = String.format("<div style='font-size: 70%%; margin-top: 0px'>%s</div>", type); + } + + if ("true".equals(getViewOrViewSetProperty(view, MERMAID_ICONS_PROPERTY, "false")) && elementStyleHasSupportedIcon(elementStyle)) { + icon = "<div><img src='" + elementStyle.getIcon() + "' style='max-height: 50px; margin: auto; margin-top:10px'/></div>"; + } + + writer.writeLine(format("%s%s\"<div style='font-weight: bold'>%s</div>%s%s%s\"%s", + element.getId(), + nodeOpeningSymbol, + name, + type, + description, + icon, + nodeClosingSymbol + )); + + if (!StringUtils.isNullOrEmpty(element.getUrl())) { + writer.writeLine(format("click %s %s \"%s\"", element.getId(), element.getUrl(), element.getUrl())); + } + + if (element instanceof StaticStructureElementInstance) { + Element e = ((StaticStructureElementInstance)element).getElement(); + writer.writeLine(format("style %s fill:%s,stroke:%s,color:%s", element.getId(), elementStyle.getBackground(), elementStyle.getStroke(), elementStyle.getColor())); + } else { + writer.writeLine(format("style %s fill:%s,stroke:%s,color:%s", element.getId(), elementStyle.getBackground(), elementStyle.getStroke(), elementStyle.getColor())); + } + } + + @Override + protected void writeRelationship(ModelView view, RelationshipView relationshipView, IndentingWriter writer) { + Relationship relationship = relationshipView.getRelationship(); + RelationshipStyle style = view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship); + + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + if (source instanceof DeploymentNode || destination instanceof DeploymentNode) { + return; + } + + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + source = relationship.getDestination(); + destination = relationship.getSource(); + } + + boolean solid = style.getStyle() == LineStyle.Solid || false == style.getDashed(); + // solid: A-- text -->B + // dotted: A-. text .->B + + String description = relationshipView.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + description = relationshipView.getRelationship().getDescription(); + } + + if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { + description = relationshipView.getOrder() + ". " + description; + } + + writer.writeLine( + format("%s-%s \"<div>%s</div><div style='font-size: 70%%'>%s</div>\" %s->%s", + source.getId(), + solid ? "-" : ".", + lines(description), + !StringUtils.isNullOrEmpty(relationship.getTechnology()) ? "[" + relationship.getTechnology() + "]" : "", + solid ? "-" : ".", + destination.getId() + ) + ); + } + + private String lines(final String text) { + StringBuilder buf = new StringBuilder(); + if (text != null) { + final String[] words = text.trim().split("\\s+"); + + final StringBuilder line = new StringBuilder(); + for (final String word : words) { + if (line.length() == 0) { + line.append(word); + } else if (line.length() + word.length() + 1 < 30) { + line.append(' ').append(word); + } else { + buf.append(line.toString()); + buf.append("<br />"); + line.setLength(0); + line.append(word); + } + } + if (line.length() > 0) { + buf.append(line.toString()); + } + } + + return buf.toString(); + } + + @Override + protected Diagram createDiagram(ModelView view, String definition) { + return new MermaidDiagram(view, definition); + } + + protected boolean includeTitle(ModelView view) { + return "true".equals(getViewOrViewSetProperty(view, MERMAID_TITLE_PROPERTY, "true")); + } + + protected boolean renderAsSequenceDiagram(ModelView view) { + return "true".equalsIgnoreCase(getViewOrViewSetProperty(view, MERMAID_SEQUENCE_DIAGRAM_PROPERTY, "false")); + } + + private boolean elementStyleHasSupportedIcon(ElementStyle elementStyle) { + return !StringUtils.isNullOrEmpty(elementStyle.getIcon()) && elementStyle.getIcon().startsWith("http"); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java new file mode 100644 index 000000000..3f1e9fe9d --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java @@ -0,0 +1,18 @@ +package com.structurizr.export.mermaid; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Encodes a Mermaid diagram definition to base64 format, for use with image URLs, etc. + */ +public class MermaidEncoder { + + private static final String TEMPLATE = "{ \"code\":\"%s\", \"mermaid\":{\"theme\":\"default\", \"securityLevel\": \"loose\"}}"; + + public String encode(String mermaidDefinition) { + String s = String.format(TEMPLATE, mermaidDefinition.replaceAll("\n", "\\\\n").replaceAll("\"", "\\\\\"")); + return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/mermaid/README.md b/structurizr-export/src/main/java/com/structurizr/export/mermaid/README.md new file mode 100644 index 000000000..73b43c1c4 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/mermaid/README.md @@ -0,0 +1,6 @@ +# Mermaid + +The [MermaidDiagramExporter](MermaidDiagramExporter.java) provides a way to export views that are compatible with the +[Mermaid](https://mermaid-js.github.io/) diagramming tool. + +See https://docs.structurizr.com/export for more. \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java new file mode 100644 index 000000000..7424cbd2e --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java @@ -0,0 +1,251 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.AbstractDiagramExporter; +import com.structurizr.export.Diagram; +import com.structurizr.export.IndentingWriter; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.ElementStyle; +import com.structurizr.view.ModelView; +import com.structurizr.view.Shape; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; + +import static java.lang.String.format; + +public abstract class AbstractPlantUMLExporter extends AbstractDiagramExporter { + + public static final String PLANTUML_TITLE_PROPERTY = "plantuml.title"; + public static final String PLANTUML_INCLUDES_PROPERTY = "plantuml.includes"; + public static final String PLANTUML_ANIMATION_PROPERTY = "plantuml.animation"; + public static final String PLANTUML_SEQUENCE_DIAGRAM_PROPERTY = "plantuml.sequenceDiagram"; + + private static final double MAX_ICON_SIZE = 30.0; + + private final Map<String, String> skinParams = new LinkedHashMap<>(); + + protected Map<String, String> getSkinParams() { + return skinParams; + } + + public void addSkinParam(String name, String value) { + skinParams.put(name, value); + } + + public void clearSkinParams() { + skinParams.clear(); + } + + String plantUMLShapeOf(ModelView view, Element element) { + Shape shape = findElementStyle(view, element).getShape(); + + return plantUMLShapeOf(shape); + } + + String plantUMLShapeOf(Shape shape) { + switch(shape) { + case Person: + case Robot: + return "person"; + case Component: + return "component"; + case Cylinder: + return "database"; + case Folder: + return "folder"; + case Ellipse: + case Circle: + return "storage"; + case Hexagon: + return "hexagon"; + case Pipe: + return "queue"; + default: + return "rectangle"; + } + } + + String plantumlSequenceType(ModelView view, Element element) { + Shape shape = findElementStyle(view, element).getShape(); + + switch(shape) { + case Box: + return "participant"; + case Person: + return "actor"; + case Cylinder: + return "database"; + case Folder: + return "collections"; + case Ellipse: + case Circle: + return "entity"; + default: + return "participant"; + } + } + + String idOf(ModelItem modelItem) { + if (modelItem instanceof Element) { + Element element = (Element)modelItem; + if (element.getParent() == null) { + if (element instanceof DeploymentNode) { + DeploymentNode dn = (DeploymentNode)element; + return filter(dn.getEnvironment()) + "." + id(dn); + } else { + return id(element); + } + } else { + return idOf(element.getParent()) + "." + id(modelItem); + } + } + + return id(modelItem); + } + + private String id(ModelItem modelItem) { + if (modelItem instanceof Person) { + return id((Person)modelItem); + } else if (modelItem instanceof SoftwareSystem) { + return id((SoftwareSystem)modelItem); + } else if (modelItem instanceof Container) { + return id((Container)modelItem); + } else if (modelItem instanceof Component) { + return id((Component)modelItem); + } else if (modelItem instanceof DeploymentNode) { + return id((DeploymentNode)modelItem); + } else if (modelItem instanceof InfrastructureNode) { + return id((InfrastructureNode)modelItem); + } else if (modelItem instanceof SoftwareSystemInstance) { + return id((SoftwareSystemInstance)modelItem); + } else if (modelItem instanceof ContainerInstance) { + return id((ContainerInstance)modelItem); + } + + return modelItem.getId(); + } + + private String id(Person person) { + return filter(person.getName()); + } + + private String id(SoftwareSystem softwareSystem) { + return filter(softwareSystem.getName()); + } + + private String id(Container container) { + return filter(container.getName()); + } + + private String id(Component component) { + return filter(component.getName()); + } + + private String id(DeploymentNode deploymentNode) { + return filter(deploymentNode.getName()); + } + + private String id(InfrastructureNode infrastructureNode) { + return filter(infrastructureNode.getName()); + } + + private String id(SoftwareSystemInstance softwareSystemInstance) { + return filter(softwareSystemInstance.getName()) + "_" + softwareSystemInstance.getInstanceId(); + } + + private String id(ContainerInstance containerInstance) { + return filter(containerInstance.getName()) + "_" + containerInstance.getInstanceId(); + } + + private String filter(String s) { + return s.replaceAll("(?U)\\W", ""); + } + + protected boolean includeTitle(ModelView view) { + return "true".equals(getViewOrViewSetProperty(view, PLANTUML_TITLE_PROPERTY, "true")); + } + + @Override + protected boolean isAnimationSupported(ModelView view) { + return "true".equalsIgnoreCase(getViewOrViewSetProperty(view, PLANTUML_ANIMATION_PROPERTY, "false")); + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + writer.writeLine("@startuml"); + writer.writeLine("set separator none"); + + if (includeTitle(view)) { + String viewTitle = view.getTitle(); + if (StringUtils.isNullOrEmpty(viewTitle)) { + viewTitle = view.getName(); + } + writer.writeLine("title " + viewTitle); + } + + writer.writeLine(); + } + + protected void writeSkinParams(IndentingWriter writer) { + if (!skinParams.isEmpty()) { + writer.writeLine("skinparam {"); + writer.indent(); + for (final String name : skinParams.keySet()) { + writer.writeLine(format("%s %s", name, skinParams.get(name))); + } + writer.outdent(); + writer.writeLine("}"); + } + } + + protected void writeIncludes(ModelView view, IndentingWriter writer) { + String[] includes = getViewOrViewSetProperty(view, PLANTUML_INCLUDES_PROPERTY, "").split(","); + for (String include : includes) { + if (!StringUtils.isNullOrEmpty(include)) { + include = include.trim(); + writer.writeLine("!include " + include); + } + } + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + writer.writeLine("@enduml"); + } + + @Override + protected Diagram createDiagram(ModelView view, String definition) { + return new PlantUMLDiagram(view, definition); + } + + protected boolean elementStyleHasSupportedIcon(ElementStyle elementStyle) { + return !StringUtils.isNullOrEmpty(elementStyle.getIcon()) && elementStyle.getIcon().startsWith("http"); + } + + protected double calculateIconScale(String iconUrl) { + double scale = 0.5; + + try { + URL url = new URL(iconUrl); + BufferedImage bi = ImageIO.read(url); + + int width = bi.getWidth(); + int height = bi.getHeight(); + + scale = MAX_ICON_SIZE / Math.max(width, height); + } catch (UnsupportedOperationException | UnsatisfiedLinkError | IIOException e) { + // This is a known issue on native builds since AWT packages aren't available. + // So we just swallow the error and use the default scale + } catch (Exception e) { + e.printStackTrace(); + } + + return scale; + } + +} diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java new file mode 100644 index 000000000..bf67727da --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -0,0 +1,682 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.Diagram; +import com.structurizr.export.IndentingWriter; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.*; +import java.util.stream.Collectors; + +import static java.lang.String.format; + +public class C4PlantUMLExporter extends AbstractPlantUMLExporter { + + private static final String STRUCTURIZR_PROPERTY_NAME = "structurizr."; + + public static final String C4PLANTUML_LEGEND_PROPERTY = "c4plantuml.legend"; + public static final String C4PLANTUML_STEREOTYPES_PROPERTY = "c4plantuml.stereotypes"; + public static final String C4PLANTUML_TAGS_PROPERTY = "c4plantuml.tags"; + 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"; + + /** + * <p>Set this property to <code>true</code> by calling {@link Configuration#addProperty(String, String)} in your + * {@link ViewSet} in order to have all {@link ModelItem#getProperties()} for {@link Component}s + * being printed in the PlantUML diagrams.</p> + * + * <p>The default value is <code>false</code>.</p> + * + * @see ViewSet#getConfiguration() + * @see Configuration#getProperties() + */ + public static final String C4PLANTUML_ELEMENT_PROPERTIES_PROPERTY = "c4plantuml.elementProperties"; + + /** + * <p>Set this property to <code>true</code> by calling {@link Configuration#addProperty(String, String)} in your + * {@link ViewSet} in order to have all {@link ModelItem#getProperties()} for {@link Relationship}s being + * printed in the PlantUML diagrams.</p> + * + * <p>The default value is <code>false</code>.</p> + * + * @see ViewSet#getConfiguration() + * @see Configuration#getProperties() + */ + public static final String C4PLANTUML_RELATIONSHIP_PROPERTIES_PROPERTY = "c4plantuml.relationshipProperties"; + + private int groupId = 0; + + public C4PlantUMLExporter() { + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + super.writeHeader(view, writer); + groupId = 0; + + Font font = view.getViewSet().getConfiguration().getBranding().getFont(); + if (font != null) { + String fontName = font.getName(); + if (!StringUtils.isNullOrEmpty(fontName)) { + addSkinParam("defaultFontName", "\"" + fontName + "\""); + } + } + + writeSkinParams(writer); + + 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 { + 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"); + } + + writer.writeLine(); + + 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"); + } + + 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.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"); + } + } + } + + writeIncludes(view, writer); + + if (includeTags(view)) { + Map<String,ElementStyle> elementStyles = new HashMap<>(); + Map<String,RelationshipStyle> relationshipStyles = new HashMap<>(); + Map<String,ElementStyle> boundaryStyles = new HashMap<>(); + + // elements + for (ElementView elementView : view.getElements()) { + Element element = elementView.getElement(); + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + + elementStyles.put(elementStyle.getTag(), elementStyle); + } + + // relationships + for (RelationshipView relationshipView : view.getRelationships()) { + Relationship relationship = relationshipView.getRelationship(); + RelationshipStyle relationshipStyle = view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship); + + relationshipStyles.put(relationshipStyle.getTag(), relationshipStyle); + } + + if (renderAsSequenceDiagram(view)) { + // no boundaries, do nothing + } else { + // 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) { + 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); + } + } + + if (!elementStyles.isEmpty()) { + writer.writeLine(); + + for (String tagList : elementStyles.keySet()) { + ElementStyle elementStyle = elementStyles.get(tagList); + tagList = tagList.replaceFirst("Element,", ""); + + String sprite = ""; + if (elementStyleHasSupportedIcon(elementStyle)) { + double scale = calculateIconScale(elementStyle.getIcon()); + sprite = "img:" + elementStyle.getIcon() + "{scale=" + scale + "}"; + } + sprite = elementStyle.getProperties().getOrDefault(C4PLANTUML_SPRITE, sprite); + + int borderThickness = 1; + if (elementStyle.getStrokeWidth() != null) { + borderThickness = elementStyle.getStrokeWidth(); + } + + writer.writeLine(String.format("AddElementTag(\"%s\", $bgColor=\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $sprite=\"%s\", $shadowing=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", + tagList, + elementStyle.getBackground(), + elementStyle.getStroke(), + elementStyle.getColor(), + sprite, + elementStyle.getProperties().getOrDefault(C4PLANTUML_SHADOW, ""), + elementStyle.getBorder(), + borderThickness + )); + } + } + + if (!relationshipStyles.isEmpty()) { + writer.writeLine(); + + for (String tagList : relationshipStyles.keySet()) { + RelationshipStyle relationshipStyle = relationshipStyles.get(tagList); + tagList = tagList.replaceFirst("Relationship,", ""); + + String lineStyle = "\"\""; + if (relationshipStyle.getStyle() == LineStyle.Dashed) { + lineStyle = "DashedLine()"; + } else if (relationshipStyle.getStyle() == LineStyle.Dotted) { + lineStyle = "DottedLine()"; + } + + writer.writeLine(String.format("AddRelTag(\"%s\", $textColor=\"%s\", $lineColor=\"%s\", $lineStyle = %s)", + tagList, + relationshipStyle.getColor(), + relationshipStyle.getColor(), + lineStyle + )); + } + } + + if (!boundaryStyles.isEmpty()) { + writer.writeLine(); + + for (String tagList : boundaryStyles.keySet()) { + ElementStyle elementStyle = boundaryStyles.get(tagList); + tagList = tagList.replaceFirst("Element,", ""); + + int borderThickness = 1; + if (elementStyle.getStrokeWidth() != null) { + borderThickness = elementStyle.getStrokeWidth(); + } + + writer.writeLine(String.format("AddBoundaryTag(\"%s\", $bgColor=\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $shadowing=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", + tagList, + "#ffffff", + elementStyle.getStroke(), + elementStyle.getStroke(), + elementStyle.getProperties().getOrDefault(C4PLANTUML_SHADOW, ""), + elementStyle.getBorder(), + borderThickness + )); + } + } + } + + writer.writeLine(); + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + if (includeLegend(view)) { + writer.writeLine(); + writer.writeLine("SHOW_LEGEND(" + !(includeStereotypes(view)) + ")"); + } else { + writer.writeLine(); + writer.writeLine((includeStereotypes(view) ? "show" : "hide") + " stereotypes"); + } + + super.writeFooter(view, writer); + } + + @Override + protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { + writer.writeLine(String.format("Enterprise_Boundary(enterprise, \"%s\") {", enterpriseName)); + writer.indent(); + } + + @Override + protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + groupId++; + String groupName = group; + + String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); + if (!StringUtils.isNullOrEmpty(groupSeparator)) { + groupName = group.substring(group.lastIndexOf(groupSeparator) + groupSeparator.length()); + } + + String color = "#cccccc"; + String borderStyle = "Dashed"; + int borderThickness = 1; +// String icon = ""; + + ElementStyle elementStyleForGroup = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group:" + group); + ElementStyle elementStyleForAllGroups = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group"); + + if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getColor())) { + color = elementStyleForGroup.getColor(); + } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getColor())) { + color = elementStyleForAllGroups.getColor(); + } + + if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getStroke())) { + borderStyle = elementStyleForGroup.getStroke(); + } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getStroke())) { + borderStyle = elementStyleForAllGroups.getStroke(); + } + + if (elementStyleForGroup != null && elementStyleForGroup.getStrokeWidth() != null) { + borderThickness = elementStyleForGroup.getStrokeWidth(); + } else if (elementStyleForAllGroups != null && elementStyleForAllGroups.getStrokeWidth() != null) { + borderThickness = elementStyleForAllGroups.getStrokeWidth(); + } + + +// todo: $sprite doesn't seem to be supported for boundary styles +// if (elementStyleForGroup != null && elementStyleHasSupportedIcon(elementStyleForGroup)) { +// icon = elementStyleForGroup.getIcon(); +// } else if (elementStyleForAllGroups != null && elementStyleHasSupportedIcon(elementStyleForAllGroups)) { +// icon = elementStyleForAllGroups.getColor(); +// } +// +// if (!StringUtils.isNullOrEmpty(icon)) { +// double scale = calculateIconScale(icon); +// icon = "\\n\\n<img:" + icon + "{scale=" + scale + "}>"; +// } + + writer.writeLine(String.format("AddBoundaryTag(\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", + group, + color, + color, + borderStyle, + borderThickness) + ); + + writer.writeLine(String.format("Boundary(group_%s, \"%s\", $tags=\"%s\") {", groupId, groupName, group)); + writer.indent(); + } + + @Override + protected void endGroupBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { + writer.writeLine(String.format("System_Boundary(\"%s_boundary\", \"%s\", $tags=\"%s\") {", idOf(softwareSystem), softwareSystem.getName(), tagsOf(view, softwareSystem))); + writer.indent(); + } + + @Override + protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { + writer.writeLine(String.format("Container_Boundary(\"%s_boundary\", \"%s\", $tags=\"%s\") {", idOf(container), container.getName(), tagsOf(view,container))); + writer.indent(); + } + + @Override + protected void endContainerBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { + String url = deploymentNode.getUrl(); + if (StringUtils.isNullOrEmpty(url)) { + url = ""; + } + + if (Boolean.TRUE.toString().equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_ELEMENT_PROPERTIES_PROPERTY, Boolean.FALSE.toString()))) { + addProperties(view, writer, deploymentNode); + } + + String technology = deploymentNode.getTechnology(); + if (StringUtils.isNullOrEmpty(technology)) { + technology = ""; + } + String description = deploymentNode.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + description = ""; + } + + // Deployment_Node(alias, label, ?type, ?descr, ?sprite, ?tags, ?link) + writer.writeLine( + format("Deployment_Node(%s, \"%s\", $type=\"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\") {", + idOf(deploymentNode), + deploymentNode.getName() + (!"1".equals(deploymentNode.getInstances()) ? " (x" + deploymentNode.getInstances() + ")" : ""), + technology, + description, + tagsOf(view, deploymentNode), + url + ) + ); + writer.indent(); + + if (!isVisible(view, deploymentNode)) { + writer.writeLine("hide " + idOf(deploymentNode)); + } + } + + @Override + protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + 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) { + return; + } + + Element elementToWrite = element; + ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + String id = idOf(element); + + String url = element.getUrl(); + if (StringUtils.isNullOrEmpty(url)) { + url = ""; + } + + if (Boolean.TRUE.toString().equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_ELEMENT_PROPERTIES_PROPERTY, Boolean.FALSE.toString()))) { + addProperties(view, writer, element); + } + + if (element instanceof StaticStructureElementInstance) { + StaticStructureElementInstance elementInstance = (StaticStructureElementInstance)element; + element = elementInstance.getElement(); + + if (StringUtils.isNullOrEmpty(url)) { + url = element.getUrl(); + if (StringUtils.isNullOrEmpty(url)) { + url = ""; + } + } + } + + String name = element.getName(); + String description = element.getDescription(); + + if (StringUtils.isNullOrEmpty(description)) { + description = ""; + } + + if (element instanceof Person) { + Person person = (Person)element; + String location = ""; + if (person.getLocation() == Location.External) { + location = "_Ext"; + } + + // Person(alias, label, ?descr, ?sprite, ?tags, ?link, ?type) + writer.writeLine( + String.format("Person%s(%s, \"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", + location, id, name, description, tagsOf(view, elementToWrite), url) + ); + } else if (element instanceof SoftwareSystem) { + SoftwareSystem softwareSystem = (SoftwareSystem)element; + String location = ""; + if (softwareSystem.getLocation() == Location.External) { + location = "_Ext"; + } + + // System(alias, label, ?descr, ?sprite, ?tags, ?link, ?type) + writer.writeLine( + String.format("System%s(%s, \"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", + location, id, name, description, tagsOf(view, elementToWrite), url) + ); + } else if (element instanceof Container) { + Container container = (Container)element; + String shape = ""; + if (elementStyle.getShape() == Shape.Cylinder) { + shape = "Db"; + } else if (elementStyle.getShape() == Shape.Pipe) { + shape = "Queue"; + } + + String technology = container.getTechnology(); + if (StringUtils.isNullOrEmpty(technology)) { + technology = ""; + } + + // Container(alias, label, ?techn, ?descr, ?sprite, ?tags, ?link) + writer.writeLine( + String.format("Container%s(%s, \"%s\", $techn=\"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", + shape, id, name, technology, description, tagsOf(view, elementToWrite), url) + ); + } else if (element instanceof Component) { + Component component = (Component)element; + String shape = ""; + + if (elementStyle.getShape() == Shape.Cylinder) { + shape = "Db"; + } else if (elementStyle.getShape() == Shape.Pipe) { + shape = "Queue"; + } + + String technology = component.getTechnology(); + if (StringUtils.isNullOrEmpty(technology)) { + technology = ""; + } + + // Component(alias, label, ?techn, ?descr, ?sprite, ?tags, ?link) + writer.writeLine( + String.format("Component%s(%s, \"%s\", $techn=\"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", + shape, id, name, technology, description, tagsOf(view, elementToWrite), url) + ); + } else if (element instanceof InfrastructureNode) { + InfrastructureNode infrastructureNode = (InfrastructureNode)element; + String technology = infrastructureNode.getTechnology(); + if (StringUtils.isNullOrEmpty(technology)) { + technology = ""; + } + + // Deployment_Node(alias, label, ?type, ?descr, ?sprite, ?tags, ?link) + writer.writeLine( + String.format("Deployment_Node(%s, \"%s\", $type=\"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", + idOf(infrastructureNode), name, technology, description, tagsOf(view, elementToWrite), url) + ); + } + + if (!isVisible(view, elementToWrite)) { + writer.writeLine("hide " + id); + } + } + + private String tagsOf(ModelView view, Element element) { + if (includeTags(view)) { + return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getTag().replaceFirst("Element,", ""); + } else { + return ""; + } + } + + private String tagsOf(ModelView view, Relationship relationship) { + if (includeTags(view)) { + return view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship).getTag().replaceFirst("Relationship,", ""); + } else { + return ""; + } + } + + @Override + protected void writeRelationship(ModelView view, RelationshipView relationshipView, IndentingWriter writer) { + Relationship relationship = relationshipView.getRelationship(); + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + if (source instanceof CustomElement || destination instanceof CustomElement) { + return; + } + + if (Boolean.TRUE.toString().equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_RELATIONSHIP_PROPERTIES_PROPERTY, Boolean.FALSE.toString()))) { + addProperties(view, writer, relationship); + } + + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + source = relationship.getDestination(); + destination = relationship.getSource(); + } + + String description = ""; + + 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() : ""); + + String technology = relationship.getTechnology(); + if (StringUtils.isNullOrEmpty(technology)) { + technology = ""; + } + + String url = relationship.getUrl(); + if (StringUtils.isNullOrEmpty(url)) { + url = ""; + } + + // Rel(from, to, label, ?techn, ?descr, ?sprite, ?tags, ?link) + writer.writeLine( + format("Rel(%s, %s, \"%s\", $techn=\"%s\", $tags=\"%s\", $link=\"%s\")", + idOf(source), idOf(destination), description, technology, tagsOf(view, relationship), url) + ); + } + + private void addProperties(ModelView view, IndentingWriter writer, ModelItem element) { + Map<String, String> properties = new HashMap<>(); + for (String key : element.getProperties().keySet()) { + // don't include any internal Structurizr properties (e.g. structurizr.dsl.identifier) + if (!key.startsWith(STRUCTURIZR_PROPERTY_NAME)) { + properties.put(key, element.getProperties().get(key)); + } + } + + if (!properties.isEmpty()) { + writer.writeLine("WithoutPropertyHeader()"); + properties.keySet().stream().sorted().forEach(key -> + writer.writeLine(String.format("AddProperty(\"%s\",\"%s\")", key, properties.get(key))) + ); + } + } + + @Override + protected boolean isAnimationSupported(ModelView view) { + return !(view instanceof DynamicView) && super.isAnimationSupported(view); + } + + protected boolean includeLegend(ModelView view) { + return "true".equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_LEGEND_PROPERTY, "true")); + } + + protected boolean includeStereotypes(ModelView view) { + return "true".equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_STEREOTYPES_PROPERTY, "false")); + } + + protected boolean includeTags(ModelView view) { + return "true".equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_TAGS_PROPERTY, "false")); + } + + 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, PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "false")); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDiagram.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDiagram.java new file mode 100644 index 000000000..b35e854e1 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDiagram.java @@ -0,0 +1,17 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.Diagram; +import com.structurizr.view.ModelView; + +public class PlantUMLDiagram extends Diagram { + + public PlantUMLDiagram(ModelView view, String definition) { + super(view, definition); + } + + @Override + public String getFileExtension() { + return "puml"; + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java new file mode 100644 index 000000000..1b5d24da7 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java @@ -0,0 +1,73 @@ +package com.structurizr.export.plantuml; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +/** + * A Java implementation of http://plantuml.com/code-javascript-synchronous + * that uses Java's built-in Deflate algorithm. + */ +public class PlantUMLEncoder { + + public String encode(String plantUMLDefinition) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true); + + DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, true); + dos.write(plantUMLDefinition.getBytes(StandardCharsets.UTF_8)); + dos.finish(); + + return encode(baos.toByteArray()); + } + + private String encode(byte[] bytes) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < bytes.length; i += 3) { + int b1 = (bytes[i]) & 0xFF; + int b2 = (i + 1 < bytes.length ? bytes[i + 1] : (byte)0) & 0xFF; + int b3 = (i + 2 < bytes.length ? bytes[i + 2] : (byte)0) & 0xFF; + + append3bytes(buf, b1, b2, b3); + } + + return buf.toString(); + } + + private char encode6bit(byte b) { + if (b < 10) { + return (char) ('0' + b); + } + b -= 10; + if (b < 26) { + return (char) ('A' + b); + } + b -= 26; + if (b < 26) { + return (char) ('a' + b); + } + b -= 26; + if (b == 0) { + return '-'; + } + if (b == 1) { + return '_'; + } + + return '?'; + } + + private void append3bytes(StringBuilder buf, int b1, int b2, int b3) { + int c1 = b1 >> 2; + int c2 = (b1 & 0x3) << 4 | b2 >> 4; + int c3 = (b2 & 0xF) << 2 | b3 >> 6; + int c4 = b3 & 0x3F; + + buf.append(encode6bit((byte)(c1 & 0x3F))); + buf.append(encode6bit((byte)(c2 & 0x3F))); + buf.append(encode6bit((byte)(c3 & 0x3F))); + buf.append(encode6bit((byte)(c4 & 0x3F))); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md b/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md new file mode 100644 index 000000000..616e11ca9 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md @@ -0,0 +1,7 @@ +# PlantUML + +There are two PlantUML exporters in this package - [StructurizrPlantUMLExporter](StructurizrPlantUMLExporter.java) and [C4PlantUMLExporter](C4PlantUMLExporter.java). + +If neither of these provide the features you are looking for, an alternative PlantUML exporter can be found at [https://github.com/cloudflightio/structurizr-export-c4plantuml](https://github.com/cloudflightio/structurizr-export-c4plantuml). + +See https://docs.structurizr.com/export for more. \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java new file mode 100644 index 000000000..97d92a818 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -0,0 +1,626 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.Diagram; +import com.structurizr.export.IndentingWriter; +import com.structurizr.export.Legend; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.*; +import java.util.stream.Collectors; + +import static java.lang.String.format; + +public class StructurizrPlantUMLExporter extends AbstractPlantUMLExporter { + + public static final String PLANTUML_SHADOW = "plantuml.shadow"; + + private int groupId = 0; + + public StructurizrPlantUMLExporter() { + addSkinParam("arrowFontSize", "10"); + addSkinParam("defaultTextAlignment", "center"); + addSkinParam("wrapWidth", "200"); + addSkinParam("maxMessageSize", "100"); + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + super.writeHeader(view, writer); + groupId = 0; + + if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + // do nothing + } else { + 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"); + } + + writer.writeLine(); + } + + Font font = view.getViewSet().getConfiguration().getBranding().getFont(); + if (font != null) { + String fontName = font.getName(); + if (!StringUtils.isNullOrEmpty(fontName)) { + addSkinParam("defaultFontName", "\"" + fontName + "\""); + } + } + + writeSkinParams(writer); + writeIncludes(view, writer); + + writer.writeLine(); + writer.writeLine("hide stereotype"); + writer.writeLine(); + + List<Element> elements = view.getElements().stream().map(ElementView::getElement).sorted(Comparator.comparing(Element::getName)).collect(Collectors.toList()); + for (Element element : elements) { + String id = idOf(element); + + String type = plantUMLShapeOf(view, element); + if ("actor".equals(type)) { + type = "rectangle"; // the actor shape is not supported in this implementation + } + + ElementStyle elementStyle = findElementStyle(view, element); + + String background = elementStyle.getBackground(); + String stroke = elementStyle.getStroke(); + String color = elementStyle.getColor(); + Shape shape = elementStyle.getShape(); + + if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + type = "sequenceParticipant"; + } + + writer.writeLine(format("skinparam %s<<%s>> {", type, id)); + writer.indent(); + if (element instanceof DeploymentNode) { + writer.writeLine("BackgroundColor #ffffff"); + } else { + writer.writeLine(String.format("BackgroundColor %s", background)); + } + writer.writeLine(String.format("FontColor %s", color)); + writer.writeLine(String.format("BorderColor %s", stroke)); + + if (shape == Shape.RoundedBox) { + writer.writeLine("roundCorner 20"); + } + + boolean shadow = "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")); + writer.writeLine(String.format("shadowing %s", shadow)); + + writer.outdent(); + writer.writeLine("}"); + } + + if (!renderAsSequenceDiagram(view)) { + // 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) { + 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); + String id = idOf(boundaryElement); + String color = elementStyle.getStroke(); + boolean shadow = "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")); + + writer.writeLine(format("skinparam rectangle<<%s>> {", id)); + writer.indent(); + writer.writeLine(String.format("BorderColor %s", color)); + writer.writeLine(String.format("FontColor %s", color)); + writer.writeLine(String.format("shadowing %s", shadow)); + writer.outdent(); + writer.writeLine("}"); + } + } + + writer.writeLine(); + } + + @Override + protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { + if (!renderAsSequenceDiagram(view)) { + writer.writeLine(String.format("rectangle \"%s\" <<enterprise>> {", enterpriseName)); + writer.indent(); + writer.writeLine("skinparam RectangleBorderColor<<enterprise>> #444444"); + writer.writeLine("skinparam RectangleFontColor<<enterprise>> #444444"); + writer.writeLine(); + } + } + + @Override + protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { + if (!renderAsSequenceDiagram(view)) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + } + + @Override + protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + groupId++; + String groupName = group; + + String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); + if (!StringUtils.isNullOrEmpty(groupSeparator)) { + groupName = group.substring(group.lastIndexOf(groupSeparator) + groupSeparator.length()); + } + + if (!renderAsSequenceDiagram(view)) { + String color = "#cccccc"; + String icon = ""; + + ElementStyle elementStyleForGroup = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group:" + group); + ElementStyle elementStyleForAllGroups = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group"); + + if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getColor())) { + color = elementStyleForGroup.getColor(); + } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getColor())) { + color = elementStyleForAllGroups.getColor(); + } + + if (elementStyleForGroup != null && elementStyleHasSupportedIcon(elementStyleForGroup)) { + icon = elementStyleForGroup.getIcon(); + } else if (elementStyleForAllGroups != null && elementStyleHasSupportedIcon(elementStyleForAllGroups)) { + icon = elementStyleForAllGroups.getColor(); + } + + if (!StringUtils.isNullOrEmpty(icon)) { + double scale = calculateIconScale(icon); + icon = "\\n\\n<img:" + icon + "{scale=" + scale + "}>"; + } + + writer.writeLine(String.format("rectangle \"%s%s\" <<group%s>> {", groupName, icon, groupId)); + writer.indent(); + writer.writeLine(String.format("skinparam RectangleBorderColor<<group%s>> %s", groupId, color)); + writer.writeLine(String.format("skinparam RectangleFontColor<<group%s>> %s", groupId, color)); + writer.writeLine(String.format("skinparam RectangleBorderStyle<<group%s>> dashed", groupId)); + + writer.writeLine(); + } + } + + @Override + protected void endGroupBoundary(ModelView view, IndentingWriter writer) { + if (!renderAsSequenceDiagram(view)) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + } + + @Override + protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { + if (!renderAsSequenceDiagram(view)) { + writer.writeLine(String.format("rectangle \"%s\\n<size:10>%s</size>\" <<%s>> {", softwareSystem.getName(), typeOf(view, softwareSystem, true), idOf(softwareSystem))); + writer.indent(); + } + } + + @Override + protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) { + if (!renderAsSequenceDiagram(view)) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + } + + @Override + protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { + if (!renderAsSequenceDiagram(view)) { + writer.writeLine(String.format("rectangle \"%s\\n<size:10>%s</size>\" <<%s>> {", container.getName(), typeOf(view, container, true), idOf(container))); + writer.indent(); + } + } + + @Override + protected void endContainerBoundary(ModelView view, IndentingWriter writer) { + if (!renderAsSequenceDiagram(view)) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + } + + @Override + protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { + ElementStyle elementStyle = findElementStyle(view, deploymentNode); + + String icon = ""; + if (elementStyleHasSupportedIcon(elementStyle)) { + double scale = calculateIconScale(elementStyle.getIcon()); + icon = "\\n\\n<img:" + elementStyle.getIcon() + "{scale=" + scale + "}>"; + } + + String url = deploymentNode.getUrl(); + if (!StringUtils.isNullOrEmpty(url)) { + url = " [[" + url + "]]"; + } else { + url = ""; + } + + writer.writeLine( + format("rectangle \"%s\\n<size:10>%s</size>%s\" <<%s>> as %s%s {", + deploymentNode.getName() + (!"1".equals(deploymentNode.getInstances()) ? " (x" + deploymentNode.getInstances() + ")" : ""), + typeOf(view, deploymentNode, true), + icon, + idOf(deploymentNode), + idOf(deploymentNode), + url + ) + ); + writer.indent(); + + if (!isVisible(view, deploymentNode)) { + writer.writeLine("hide " + idOf(deploymentNode)); + } + } + + @Override + protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + public Diagram export(DynamicView view) { + if (renderAsSequenceDiagram(view)) { + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + 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); + } + + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } else { + return super.export(view); + } + } + + @Override + protected void writeElement(ModelView view, Element element, IndentingWriter writer) { + ElementStyle elementStyle = findElementStyle(view, element); + + if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + writer.writeLine(String.format("%s \"%s\\n<size:10>%s</size>\" as %s <<%s>> %s", + plantumlSequenceType(view, element), + element.getName(), + typeOf(view, element, true), + idOf(element), + idOf(element), + elementStyle.getBackground())); + } else { + String shape = plantUMLShapeOf(view, element); + if ("actor".equals(shape)) { + shape = "rectangle"; + } + String name = element.getName(); + String description = element.getDescription(); + String type = typeOf(view, element, true); + String icon = ""; + String url = element.getUrl(); + + if (element instanceof StaticStructureElementInstance) { + StaticStructureElementInstance elementInstance = (StaticStructureElementInstance) element; + name = elementInstance.getElement().getName(); + description = elementInstance.getElement().getDescription(); + type = typeOf(view, elementInstance.getElement(), true); + shape = plantUMLShapeOf(view, elementInstance.getElement()); + url = elementInstance.getUrl(); + + if (StringUtils.isNullOrEmpty(url)) { + url = elementInstance.getElement().getUrl(); + } + } + + if (!StringUtils.isNullOrEmpty(url)) { + url = " [[" + url + "]]"; + } else { + url = ""; + } + + if (StringUtils.isNullOrEmpty(description) || false == elementStyle.getDescription()) { + description = ""; + } else { + description = "\\n\\n" + description; + } + + if (StringUtils.isNullOrEmpty(type) || false == elementStyle.getMetadata()) { + type = ""; + } else { + type = String.format("\\n<size:10>%s</size>", type); + } + + if (elementStyleHasSupportedIcon(elementStyle)) { + double scale = calculateIconScale(elementStyle.getIcon()); + icon = "\\n\\n<img:" + elementStyle.getIcon() + "{scale=" + scale + "}>"; + } + + String id = idOf(element); + + writer.writeLine(format("%s \"==%s%s%s%s\" <<%s>> as %s%s", + shape, + name, + type, + description, + icon, + id, + id, + url) + ); + + if (!isVisible(view, element)) { + writer.writeLine("hide " + id); + } + } + } + + @Override + protected void writeRelationship(ModelView view, RelationshipView relationshipView, IndentingWriter writer) { + Relationship relationship = relationshipView.getRelationship(); + RelationshipStyle style = findRelationshipStyle(view, relationship); + + String description = ""; + String technology = relationship.getTechnology(); + + if (view instanceof DynamicView && 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() : ""); + + if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + String arrowStart = "-"; + String arrowEnd = ">"; + + if (relationshipView.isResponse() != null && relationshipView.isResponse() == true) { + arrowStart = "<-"; + arrowEnd = "-"; + } + + writer.writeLine( + String.format("%s %s[%s]%s %s : %s", + idOf(relationship.getSource()), + arrowStart, + style.getColor(), + arrowEnd, + idOf(relationship.getDestination()), + description)); + } else { + boolean solid = style.getStyle() == LineStyle.Solid || false == style.getDashed(); + + String arrowStart; + String arrowEnd; + String relationshipStyle = style.getColor(); + + if (style.getThickness() != null) { + relationshipStyle += ",thickness=" + style.getThickness(); + } + + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + arrowStart = solid ? "<-" : "<."; + arrowEnd = solid ? "-" : "."; + } else { + arrowStart = solid ? "-" : "."; + arrowEnd = solid ? "->" : ".>"; + } + + if (!isVisible(view, relationshipView)) { + relationshipStyle = "hidden"; + } + + // 1 .[#rrggbb,thickness=n].> 2 : "...\n<size:8>...</size> + writer.writeLine(format("%s %s[%s]%s %s : \"<color:%s>%s%s\"", + idOf(relationship.getSource()), + arrowStart, + relationshipStyle, + arrowEnd, + idOf(relationship.getDestination()), + style.getColor(), + description, + (StringUtils.isNullOrEmpty(technology) ? "" : "\\n<color:" + style.getColor() + "><size:8>[" + technology + "]</size>") + )); + } + } + + @Override + protected Legend createLegend(ModelView view) { + IndentingWriter writer = new IndentingWriter(); + int id = 0; + + writer.writeLine("@startuml"); + writer.writeLine("set separator none"); + writer.writeLine(); + + writer.writeLine("skinparam {"); + writer.indent(); + writer.writeLine("shadowing false"); + writer.writeLine("arrowFontSize 15"); + writer.writeLine("defaultTextAlignment center"); + writer.writeLine("wrapWidth 100"); + writer.writeLine("maxMessageSize 100"); + Font font = view.getViewSet().getConfiguration().getBranding().getFont(); + if (font != null) { + String fontName = font.getName(); + if (!StringUtils.isNullOrEmpty(fontName)) { + writer.writeLine("defaultFontName \"" + fontName + "\""); + } + } + writer.outdent(); + writer.writeLine("}"); + + writer.writeLine("hide stereotype"); + writer.writeLine(); + + writer.writeLine("skinparam rectangle<<_transparent>> {"); + writer.indent(); + writer.writeLine("BorderColor transparent"); + writer.writeLine("BackgroundColor transparent"); + writer.writeLine("FontColor transparent"); + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + + Map<String,ElementStyle> elementStyles = new HashMap<>(); + List<Element> elements = view.getElements().stream().map(ElementView::getElement).collect(Collectors.toList()); + for (Element element : elements) { + ElementStyle elementStyle = findElementStyle(view, element); + + if (element instanceof DeploymentNode) { + // deployment node backgrounds are always white + elementStyle.setBackground("#ffffff"); + } + + if (!StringUtils.isNullOrEmpty(elementStyle.getTag()) ) { + elementStyles.put(elementStyle.getTag(), elementStyle); + }; + } + + List<ElementStyle> sortedElementStyles = elementStyles.values().stream().sorted(Comparator.comparing(ElementStyle::getTag)).collect(Collectors.toList());; + for (ElementStyle elementStyle : sortedElementStyles) { + id++; + Shape shape = elementStyle.getShape(); + String type = plantUMLShapeOf(elementStyle.getShape()); + if ("actor".equals(type)) { + type = "rectangle"; // the actor shape is not supported in this implementation + } + + String background = elementStyle.getBackground(); + String stroke = elementStyle.getStroke(); + String color = elementStyle.getColor(); + + if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + type = "sequenceParticipant"; + } + + writer.writeLine(format("skinparam %s<<%s>> {", type, id)); + writer.indent(); + writer.writeLine(String.format("BackgroundColor %s", background)); + writer.writeLine(String.format("FontColor %s", color)); + writer.writeLine(String.format("BorderColor %s", stroke)); + + if (shape == Shape.RoundedBox) { + writer.writeLine("roundCorner 20"); + } + writer.outdent(); + writer.writeLine("}"); + + String description = elementStyle.getTag(); + if (description.startsWith("Element,")) { + description = description.substring("Element,".length()); + } + description = description.replaceAll(",", ", "); + + String icon = ""; + if (elementStyleHasSupportedIcon(elementStyle)) { + double scale = calculateIconScale(elementStyle.getIcon()); + icon = "\\n\\n<img:" + elementStyle.getIcon() + "{scale=" + scale + "}>"; + } + + writer.writeLine(format("%s \"==%s%s\" <<%s>>", + type, + description, + icon, + id) + ); + writer.writeLine(); + } + + Map<String,RelationshipStyle> relationshipStyles = new HashMap<>(); + List<Relationship> relationships = view.getRelationships().stream().map(RelationshipView::getRelationship).collect(Collectors.toList()); + for (Relationship relationship : relationships) { + RelationshipStyle relationshipStyle = findRelationshipStyle(view, relationship); + + if (!StringUtils.isNullOrEmpty(relationshipStyle.getTag())) { + relationshipStyles.put(relationshipStyle.getTag(), relationshipStyle); + } + } + + List<RelationshipStyle> sortedRelationshipStyles = relationshipStyles.values().stream().sorted(Comparator.comparing(RelationshipStyle::getTag)).collect(Collectors.toList());; + for (RelationshipStyle relationshipStyle : sortedRelationshipStyles) { + id++; + + String description = relationshipStyle.getTag(); + if (description.startsWith("Relationship,")) { + description = description.substring("Relationship,".length()); + } + description = description.replaceAll(",", ", "); + + writer.writeLine(format("rectangle \".\" <<_transparent>> as %s", id)); + + boolean solid = relationshipStyle.getStyle() == LineStyle.Solid || false == relationshipStyle.getDashed(); + + String arrowStart = solid ? "-" : "."; + String arrowEnd = solid ? "->" : ".>"; + String buf = relationshipStyle.getColor(); + + if (relationshipStyle.getThickness() != null) { + buf += ",thickness=" + relationshipStyle.getThickness(); + } + + // 1 .[#rrggbb,thickness=n].> 2 : "..." + writer.writeLine(format("%s %s[%s]%s %s : \"<color:%s>%s\"", + id, + arrowStart, + buf, + arrowEnd, + id, + relationshipStyle.getColor(), + description) + ); + + writer.writeLine(); + } + + writer.writeLine(); + + writer.writeLine("@enduml"); + + return new Legend(writer.toString()); + } + + protected boolean renderAsSequenceDiagram(ModelView view) { + return view instanceof DynamicView && "true".equalsIgnoreCase(getViewOrViewSetProperty(view, PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "false")); + } + +} diff --git a/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/README.md b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/README.md new file mode 100644 index 000000000..f75f782df --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/README.md @@ -0,0 +1,23 @@ +# WebSequenceDiagrams + +The [WebSequenceDiagramExporter](WebSequenceDiagramsDiagram.java) class provides a way to export dynamic views to +diagram definitions that are compatible with [websequencediagrams.com](https://www.websequencediagrams.com). + +## Example usage + +You can either export all dynamic views in a workspace: + +``` +Workspace workspace = ... +WebSequenceDiagramsExporter exporter = new WebSequenceDiagramsExporter(); +Collection<Diagram> diagrams = exporter.export(workspace); +``` + +Or just a single dynamic view: + +``` +Workspace workspace = ... +DynamicView view = ... +WebSequenceDiagramsExporter exporter = new WebSequenceDiagramsExporter(); +Diagram diagram = exporter.export(view); +``` \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsDiagram.java b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsDiagram.java new file mode 100644 index 000000000..73445af84 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsDiagram.java @@ -0,0 +1,17 @@ +package com.structurizr.export.websequencediagrams; + +import com.structurizr.export.Diagram; +import com.structurizr.view.ModelView; + +public class WebSequenceDiagramsDiagram extends Diagram { + + public WebSequenceDiagramsDiagram(ModelView view, String definition) { + super(view, definition); + } + + @Override + public String getFileExtension() { + return "wsd"; + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java new file mode 100644 index 000000000..b731844a5 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java @@ -0,0 +1,176 @@ +package com.structurizr.export.websequencediagrams; + +import com.structurizr.export.AbstractDiagramExporter; +import com.structurizr.export.Diagram; +import com.structurizr.export.IndentingWriter; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Exports dynamic diagram definitions that can be copy-pasted + * into https://www.websequencediagrams.com + * + * This implementation only supports a basic sequence of interactions, + * both synchronous and asynchronous. It doesn't support return messages, + * parallel behaviour, etc. + */ +public class WebSequenceDiagramsExporter extends AbstractDiagramExporter { + + private static final String SYNCHRONOUS_INTERACTION = "->"; + private static final String ASYNCHRONOUS_INTERACTION = "->>"; + private static final String SYNCHRONOUS_INTERACTION_RETURN = "-->"; + private static final String ASYNCHRONOUS_INTERACTION_RETURN = "-->>"; + + @Override + public Diagram export(SystemLandscapeView view) { + return null; + } + + @Override + public Diagram export(SystemContextView view) { + return null; + } + + @Override + public Diagram export(ContainerView view) { + return null; + } + + @Override + public Diagram export(ComponentView view) { + return null; + } + + @Override + public Diagram export(DynamicView view) { + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + 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); + } + + writer.writeLine(); + + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + @Override + public Diagram export(DeploymentView view) { + return null; + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + writer.writeLine("title " + view.getName() + " - " + view.getKey()); + writer.writeLine(); + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + } + + @Override + protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { + } + + @Override + protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { + } + + @Override + protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + } + + @Override + protected void endGroupBoundary(ModelView view, IndentingWriter writer) { + } + + @Override + protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { + } + + @Override + protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) { + } + + @Override + protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { + } + + @Override + protected void endContainerBoundary(ModelView view, IndentingWriter writer) { + } + + @Override + protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { + } + + @Override + protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) { + } + + @Override + protected void writeElement(ModelView view, Element element, IndentingWriter writer) { + if (element instanceof Person) { + writer.writeLine(String.format("actor <<%s>>\\n%s as %s", + view.getViewSet().getConfiguration().getTerminology().findTerminology(element), + element.getName(), + element.getName()) + ); + } else { + writer.writeLine(String.format("participant <<%s>>\\n%s as %s", + view.getViewSet().getConfiguration().getTerminology().findTerminology(element), + element.getName(), + element.getName()) + ); + } + } + + @Override + protected void writeRelationship(ModelView view, RelationshipView relationshipView, IndentingWriter writer) { + Relationship r = relationshipView.getRelationship(); + + Element source = r.getSource(); + Element destination = r.getDestination(); + String description = relationshipView.getDescription(); + String arrow = r.getInteractionStyle() == InteractionStyle.Asynchronous ? ASYNCHRONOUS_INTERACTION : SYNCHRONOUS_INTERACTION; + + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + source = r.getDestination(); + destination = r.getSource(); + arrow = r.getInteractionStyle() == InteractionStyle.Asynchronous ? ASYNCHRONOUS_INTERACTION_RETURN : SYNCHRONOUS_INTERACTION_RETURN; + } + + if (StringUtils.isNullOrEmpty(description)) { + description = relationshipView.getRelationship().getDescription(); + } + + // Thing A->Thing B: Description + writer.writeLine(String.format("%s%s%s: %s", + source.getName(), + arrow, + destination.getName(), + description + )); + } + + @Override + protected Diagram createDiagram(ModelView view, String definition) { + return new WebSequenceDiagramsDiagram(view, definition); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/AbstractExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/AbstractExporterTests.java new file mode 100644 index 000000000..7782650d9 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/AbstractExporterTests.java @@ -0,0 +1,29 @@ +package com.structurizr.export; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +public abstract class AbstractExporterTests { + + protected String readFile(File file) throws Exception { + StringBuilder buf = new StringBuilder(); + + Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); + + List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); + + for (String line : lines) { + buf.append(line); + buf.append("\n"); + } + + if (buf.length() > 1) { + return buf.toString().substring(0, buf.length() - 1); + } else { + return buf.toString(); + } + } + +} diff --git a/structurizr-export/src/test/java/com/structurizr/export/IndentingWriterTests.java b/structurizr-export/src/test/java/com/structurizr/export/IndentingWriterTests.java new file mode 100644 index 000000000..7c050b98a --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/IndentingWriterTests.java @@ -0,0 +1,76 @@ +package com.structurizr.export; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class IndentingWriterTests { + + @Test + public void test_WithDefaultSettings() { + IndentingWriter writer = new IndentingWriter(); + + writer.writeLine("Line 1"); + writer.indent(); + writer.writeLine("Line 2"); + writer.indent(); + writer.writeLine("Line 3"); + writer.outdent(); + writer.writeLine("Line 4"); + writer.outdent(); + writer.writeLine("Line 4"); + + assertEquals("Line 1\n" + + " Line 2\n" + + " Line 3\n" + + " Line 4\n" + + "Line 4", writer.toString()); + } + + @Test + public void test_WithSpaces() { + IndentingWriter writer = new IndentingWriter(); + writer.setIndentType(IndentType.Spaces); + writer.setIndentQuantity(4); + + writer.writeLine("Line 1"); + writer.indent(); + writer.writeLine("Line 2"); + writer.indent(); + writer.writeLine("Line 3"); + writer.outdent(); + writer.writeLine("Line 4"); + writer.outdent(); + writer.writeLine("Line 4"); + + assertEquals("Line 1\n" + + " Line 2\n" + + " Line 3\n" + + " Line 4\n" + + "Line 4", writer.toString()); + } + + @Test + public void test_WithTabs() { + IndentingWriter writer = new IndentingWriter(); + writer.setIndentType(IndentType.Tabs); + writer.setIndentQuantity(1); + + writer.writeLine("Line 1"); + writer.indent(); + writer.writeLine("Line 2"); + writer.indent(); + writer.writeLine("Line 3"); + writer.outdent(); + writer.writeLine("Line 4"); + writer.outdent(); + writer.writeLine("Line 4"); + + assertEquals("Line 1\n" + + "\tLine 2\n" + + "\t\tLine 3\n" + + "\tLine 4\n" + + "Line 4", writer.toString()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot new file mode 100644 index 000000000..76c5ff5e9 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot @@ -0,0 +1,43 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">Internet Banking System - API Application - Components</font><br /><font point-size="24">The component diagram for the API Application.</font>> + + 4 [id=4,shape=rect, label=<<font point-size="34">Mainframe Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Stores all of the core banking<br />information about customers,<br />accounts, transactions, etc.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 17 [id=17,shape=rect, label=<<font point-size="34">Single-Page<br />Application</font><br /><font point-size="19">[Container: JavaScript and Angular]</font><br /><br /><font point-size="24">Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 6 [id=6,shape=rect, label=<<font point-size="34">E-mail System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">The internal Microsoft<br />Exchange e-mail system.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 18 [id=18,shape=rect, label=<<font point-size="34">Mobile App</font><br /><font point-size="19">[Container: Xamarin]</font><br /><br /><font point-size="24">Provides a limited subset of<br />the Internet banking<br />functionality to customers via<br />their mobile device.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 21 [id=21,shape=cylinder, label=<<font point-size="34">Database</font><br /><font point-size="19">[Container: Oracle Database Schema]</font><br /><br /><font point-size="24">Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + + subgraph cluster_20 { + margin=25 + label=<<font point-size="24"><br />API Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 29 [id=29,shape=rect, label=<<font point-size="34">Sign In Controller</font><br /><font point-size="19">[Component: Spring MVC Rest Controller]</font><br /><br /><font point-size="24">Allows users to sign in to the<br />Internet Banking System.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 30 [id=30,shape=rect, label=<<font point-size="34">Accounts Summary<br />Controller</font><br /><font point-size="19">[Component: Spring MVC Rest Controller]</font><br /><br /><font point-size="24">Provides customers with a<br />summary of their bank<br />accounts.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 31 [id=31,shape=rect, label=<<font point-size="34">Reset Password<br />Controller</font><br /><font point-size="19">[Component: Spring MVC Rest Controller]</font><br /><br /><font point-size="24">Allows users to reset their<br />passwords with a single use<br />URL.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 32 [id=32,shape=rect, label=<<font point-size="34">Security Component</font><br /><font point-size="19">[Component: Spring Bean]</font><br /><br /><font point-size="24">Provides functionality related<br />to signing in, changing<br />passwords, etc.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 33 [id=33,shape=rect, label=<<font point-size="34">Mainframe Banking<br />System Facade</font><br /><font point-size="19">[Component: Spring Bean]</font><br /><br /><font point-size="24">A facade onto the mainframe<br />banking system.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 34 [id=34,shape=rect, label=<<font point-size="34">E-mail Component</font><br /><font point-size="19">[Component: Spring Bean]</font><br /><br /><font point-size="24">Sends e-mails to users.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + } + + 17 -> 29 [id=35, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 17 -> 31 [id=37, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 17 -> 30 [id=38, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 18 -> 29 [id=39, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 18 -> 31 [id=41, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 18 -> 30 [id=42, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 29 -> 32 [id=43, label=<<font point-size="24">Uses</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 30 -> 33 [id=44, label=<<font point-size="24">Uses</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 31 -> 32 [id=45, label=<<font point-size="24">Uses</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 31 -> 34 [id=46, label=<<font point-size="24">Uses</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 32 -> 21 [id=47, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[JDBC]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 33 -> 4 [id=48, label=<<font point-size="24">Uses</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 34 -> 6 [id=49, label=<<font point-size="24">Sends e-mail using</font>>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot new file mode 100644 index 000000000..c4548fdc9 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot @@ -0,0 +1,37 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">Internet Banking System - Containers</font><br /><font point-size="24">The container diagram for the Internet Banking System.</font>> + + 1 [id=1,shape=rect, label=<<font point-size="32">Personal Banking<br />Customer</font><br /><font point-size="17">[Person]</font><br /><br /><font point-size="22">A customer of the bank, with<br />personal bank accounts.</font>>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] + 4 [id=4,shape=rect, label=<<font point-size="34">Mainframe Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Stores all of the core banking<br />information about customers,<br />accounts, transactions, etc.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 6 [id=6,shape=rect, label=<<font point-size="34">E-mail System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">The internal Microsoft<br />Exchange e-mail system.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + + subgraph cluster_2 { + margin=25 + label=<<font point-size="24"><br />Internet Banking System</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 17 [id=17,shape=rect, label=<<font point-size="34">Single-Page<br />Application</font><br /><font point-size="19">[Container: JavaScript and Angular]</font><br /><br /><font point-size="24">Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=rect, label=<<font point-size="34">Mobile App</font><br /><font point-size="19">[Container: Xamarin]</font><br /><br /><font point-size="24">Provides a limited subset of<br />the Internet banking<br />functionality to customers via<br />their mobile device.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 19 [id=19,shape=rect, label=<<font point-size="34">Web Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font><br /><br /><font point-size="24">Delivers the static content<br />and the Internet banking<br />single page application.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 20 [id=20,shape=rect, label=<<font point-size="34">API Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font><br /><br /><font point-size="24">Provides Internet banking<br />functionality via a JSON/HTTPS<br />API.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 21 [id=21,shape=cylinder, label=<<font point-size="34">Database</font><br /><font point-size="19">[Container: Oracle Database Schema]</font><br /><br /><font point-size="24">Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + 1 -> 19 [id=22, label=<<font point-size="24">Visits bigbank.com/ib<br />using</font><br /><font point-size="19">[HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 17 [id=23, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 18 [id=24, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 19 -> 17 [id=25, label=<<font point-size="24">Delivers to the customer's<br />web browser</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 20 -> 21 [id=26, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[JDBC]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 20 -> 4 [id=27, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 20 -> 6 [id=28, label=<<font point-size="24">Sends e-mail using</font><br /><font point-size="19">[SMTP]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 17 -> 20 [id=36, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 18 -> 20 [id=40, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 6 -> 1 [id=8, label=<<font point-size="24">Sends e-mails to</font>>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot new file mode 100644 index 000000000..0b86e2c80 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot @@ -0,0 +1,97 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">Internet Banking System - Deployment - Development</font><br /><font point-size="24">An example development deployment scenario for the Internet Banking System.</font>> + + subgraph cluster_50 { + margin=25 + label=<<font point-size="24">Developer Laptop</font><br /><font point-size="19">[Deployment Node: Microsoft Windows 10 or Apple macOS]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_51 { + margin=25 + label=<<font point-size="24">Docker Container - Web Server</font><br /><font point-size="19">[Deployment Node: Docker]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_52 { + margin=25 + label=<<font point-size="24">Apache Tomcat</font><br /><font point-size="19">[Deployment Node: Apache Tomcat 8.x]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 53 [id=53,shape=rect, label=<<font point-size="34">Web Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font><br /><br /><font point-size="24">Delivers the static content<br />and the Internet banking<br />single page application.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 54 [id=54,shape=rect, label=<<font point-size="34">API Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font><br /><br /><font point-size="24">Provides Internet banking<br />functionality via a JSON/HTTPS<br />API.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_59 { + margin=25 + label=<<font point-size="24">Docker Container - Database Server</font><br /><font point-size="19">[Deployment Node: Docker]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_60 { + margin=25 + label=<<font point-size="24">Database Server</font><br /><font point-size="19">[Deployment Node: Oracle 12c]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 61 [id=61,shape=cylinder, label=<<font point-size="34">Database</font><br /><font point-size="19">[Container: Oracle Database Schema]</font><br /><br /><font point-size="24">Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_63 { + margin=25 + label=<<font point-size="24">Web Browser</font><br /><font point-size="19">[Deployment Node: Chrome, Firefox, Safari, or Edge]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 64 [id=64,shape=rect, label=<<font point-size="34">Single-Page<br />Application</font><br /><font point-size="19">[Container: JavaScript and Angular]</font><br /><br /><font point-size="24">Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_55 { + margin=25 + label=<<font point-size="24">Big Bank plc</font><br /><font point-size="19">[Deployment Node: Big Bank plc data center]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_56 { + margin=25 + label=<<font point-size="24">bigbank-dev001</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 57 [id=57,shape=rect, label=<<font point-size="34">Mainframe Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Stores all of the core banking<br />information about customers,<br />accounts, transactions, etc.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + } + + } + + 54 -> 57 [id=58, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 54 -> 61 [id=62, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[JDBC]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 64 -> 54 [id=65, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 53 -> 64 [id=66, label=<<font point-size="24">Delivers to the customer's<br />web browser</font>>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot new file mode 100644 index 000000000..841b0dc42 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot @@ -0,0 +1,152 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">Internet Banking System - Deployment - Live</font><br /><font point-size="24">An example live deployment scenario for the Internet Banking System.</font>> + + subgraph cluster_67 { + margin=25 + label=<<font point-size="24">Customer's mobile device</font><br /><font point-size="19">[Deployment Node: Apple iOS or Android]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 68 [id=68,shape=rect, label=<<font point-size="34">Mobile App</font><br /><font point-size="19">[Container: Xamarin]</font><br /><br /><font point-size="24">Provides a limited subset of<br />the Internet banking<br />functionality to customers via<br />their mobile device.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + subgraph cluster_69 { + margin=25 + label=<<font point-size="24">Customer's computer</font><br /><font point-size="19">[Deployment Node: Microsoft Windows or Apple macOS]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_70 { + margin=25 + label=<<font point-size="24">Web Browser</font><br /><font point-size="19">[Deployment Node: Chrome, Firefox, Safari, or Edge]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 71 [id=71,shape=rect, label=<<font point-size="34">Single-Page<br />Application</font><br /><font point-size="19">[Container: JavaScript and Angular]</font><br /><br /><font point-size="24">Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_72 { + margin=25 + label=<<font point-size="24">Big Bank plc</font><br /><font point-size="19">[Deployment Node: Big Bank plc data center]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_73 { + margin=25 + label=<<font point-size="24">bigbank-prod001</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 74 [id=74,shape=rect, label=<<font point-size="34">Mainframe Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Stores all of the core banking<br />information about customers,<br />accounts, transactions, etc.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + } + + subgraph cluster_75 { + margin=25 + label=<<font point-size="24">bigbank-web***</font><br /><font point-size="19">[Deployment Node: Ubuntu 16.04 LTS]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_76 { + margin=25 + label=<<font point-size="24">Apache Tomcat</font><br /><font point-size="19">[Deployment Node: Apache Tomcat 8.x]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 77 [id=77,shape=rect, label=<<font point-size="34">Web Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font><br /><br /><font point-size="24">Delivers the static content<br />and the Internet banking<br />single page application.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_79 { + margin=25 + label=<<font point-size="24">bigbank-api***</font><br /><font point-size="19">[Deployment Node: Ubuntu 16.04 LTS]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_80 { + margin=25 + label=<<font point-size="24">Apache Tomcat</font><br /><font point-size="19">[Deployment Node: Apache Tomcat 8.x]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 81 [id=81,shape=rect, label=<<font point-size="34">API Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font><br /><br /><font point-size="24">Provides Internet banking<br />functionality via a JSON/HTTPS<br />API.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_85 { + margin=25 + label=<<font point-size="24">bigbank-db01</font><br /><font point-size="19">[Deployment Node: Ubuntu 16.04 LTS]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_86 { + margin=25 + label=<<font point-size="24">Oracle - Primary</font><br /><font point-size="19">[Deployment Node: Oracle 12c]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 87 [id=87,shape=cylinder, label=<<font point-size="34">Database</font><br /><font point-size="19">[Container: Oracle Database Schema]</font><br /><br /><font point-size="24">Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_89 { + margin=25 + label=<<font point-size="24">bigbank-db02</font><br /><font point-size="19">[Deployment Node: Ubuntu 16.04 LTS]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + subgraph cluster_90 { + margin=25 + label=<<font point-size="24">Oracle - Secondary</font><br /><font point-size="19">[Deployment Node: Oracle 12c]</font>> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 91 [id=91,shape=cylinder, label=<<font point-size="34">Database</font><br /><font point-size="19">[Container: Oracle Database Schema]</font><br /><br /><font point-size="24">Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + } + + 77 -> 71 [id=78, label=<<font point-size="24">Delivers to the customer's<br />web browser</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 68 -> 81 [id=82, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 71 -> 81 [id=83, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 81 -> 74 [id=84, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 81 -> 87 [id=88, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[JDBC]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 81 -> 91 [id=92, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[JDBC]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 87 -> 91 [id=93, label=<<font point-size="24">Replicates data to</font>>, style="dashed", color="#707070", fontcolor="#707070",ltail=cluster_86,lhead=cluster_90] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot new file mode 100644 index 000000000..bcf42801d --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot @@ -0,0 +1,29 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">API Application - Dynamic</font><br /><font point-size="24">Summarises how the sign in feature works in the single-page application.</font>> + + subgraph cluster_20 { + margin=25 + label=<<font point-size="24"><br />API Application</font><br /><font point-size="19">[Container: Java and Spring MVC]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 29 [id=29,shape=rect, label=<<font point-size="34">Sign In Controller</font><br /><font point-size="19">[Component: Spring MVC Rest Controller]</font><br /><br /><font point-size="24">Allows users to sign in to the<br />Internet Banking System.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 32 [id=32,shape=rect, label=<<font point-size="34">Security Component</font><br /><font point-size="19">[Component: Spring Bean]</font><br /><br /><font point-size="24">Provides functionality related<br />to signing in, changing<br />passwords, etc.</font>>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + } + + 17 [id=17,shape=rect, label=<<font point-size="34">Single-Page<br />Application</font><br /><font point-size="19">[Container: JavaScript and Angular]</font><br /><br /><font point-size="24">Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 21 [id=21,shape=cylinder, label=<<font point-size="34">Database</font><br /><font point-size="19">[Container: Oracle Database Schema]</font><br /><br /><font point-size="24">Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</font>>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + + 17 -> 29 [id=35, label=<<font point-size="24">1. Submits credentials to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 29 -> 32 [id=43, label=<<font point-size="24">2. Validates credentials<br />using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 32 -> 21 [id=47, label=<<font point-size="24">3. select * from users<br />where username = ?</font><br /><font point-size="19">[JDBC]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 21 -> 32 [id=47, label=<<font point-size="24">4. Returns user data to</font><br /><font point-size="19">[JDBC]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 32 -> 29 [id=43, label=<<font point-size="24">5. Returns true if the<br />hashed password matches</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 29 -> 17 [id=35, label=<<font point-size="24">6. Sends back an<br />authentication token to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot new file mode 100644 index 000000000..5f1280e06 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot @@ -0,0 +1,17 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">Internet Banking System - System Context</font><br /><font point-size="24">The system context diagram for the Internet Banking System.</font>> + + 1 [id=1,shape=rect, label=<<font point-size="32">Personal Banking<br />Customer</font><br /><font point-size="17">[Person]</font><br /><br /><font point-size="22">A customer of the bank, with<br />personal bank accounts.</font>>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] + 2 [id=2,shape=rect, label=<<font point-size="34">Internet Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Allows customers to view<br />information about their bank<br />accounts, and make payments.</font>>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] + 4 [id=4,shape=rect, label=<<font point-size="34">Mainframe Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Stores all of the core banking<br />information about customers,<br />accounts, transactions, etc.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 6 [id=6,shape=rect, label=<<font point-size="34">E-mail System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">The internal Microsoft<br />Exchange e-mail system.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + + 1 -> 2 [id=3, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 2 -> 4 [id=5, label=<<font point-size="24">Gets account information<br />from, and makes payments<br />using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 2 -> 6 [id=7, label=<<font point-size="24">Sends e-mail using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 6 -> 1 [id=8, label=<<font point-size="24">Sends e-mails to</font>>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot new file mode 100644 index 000000000..b82be15da --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot @@ -0,0 +1,35 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">System Landscape</font><br /><font point-size="24">The system landscape diagram for Big Bank plc.</font>> + + subgraph cluster_enterprise { + margin=25 + label=<<font point-size="24"><br />Big Bank plc</font><br /><font point-size="19">[Enterprise]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 12 [id=12,shape=rect, label=<<font point-size="32">Customer Service<br />Staff</font><br /><font point-size="17">[Person]</font><br /><br /><font point-size="22">Customer service staff within<br />the bank.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 15 [id=15,shape=rect, label=<<font point-size="32">Back Office Staff</font><br /><font point-size="17">[Person]</font><br /><br /><font point-size="22">Administration and support<br />staff within the bank.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 2 [id=2,shape=rect, label=<<font point-size="34">Internet Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Allows customers to view<br />information about their bank<br />accounts, and make payments.</font>>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] + 4 [id=4,shape=rect, label=<<font point-size="34">Mainframe Banking<br />System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Stores all of the core banking<br />information about customers,<br />accounts, transactions, etc.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 6 [id=6,shape=rect, label=<<font point-size="34">E-mail System</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">The internal Microsoft<br />Exchange e-mail system.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 9 [id=9,shape=rect, label=<<font point-size="34">ATM</font><br /><font point-size="19">[Software System]</font><br /><br /><font point-size="24">Allows customers to withdraw<br />cash.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + } + + 1 [id=1,shape=rect, label=<<font point-size="32">Personal Banking<br />Customer</font><br /><font point-size="17">[Person]</font><br /><br /><font point-size="22">A customer of the bank, with<br />personal bank accounts.</font>>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] + + 9 -> 4 [id=10, label=<<font point-size="24">Uses</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 9 [id=11, label=<<font point-size="24">Withdraws cash using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 12 -> 4 [id=13, label=<<font point-size="24">Uses</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 12 [id=14, label=<<font point-size="24">Asks questions to</font><br /><font point-size="19">[Telephone]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 15 -> 4 [id=16, label=<<font point-size="24">Uses</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 2 [id=3, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 2 -> 4 [id=5, label=<<font point-size="24">Gets account information<br />from, and makes payments<br />using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 2 -> 6 [id=7, label=<<font point-size="24">Sends e-mail using</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 6 -> 1 [id=8, label=<<font point-size="24">Sends e-mails to</font>>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot new file mode 100644 index 000000000..9c493cabe --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot @@ -0,0 +1,75 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=LR, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">Spring PetClinic - Deployment - Live</font>> + + subgraph cluster_5 { + margin=25 + label=<<font point-size="24">Amazon Web Services</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#232f3e" + fontcolor="#232f3e" + fillcolor="#ffffff" + + subgraph cluster_6 { + margin=25 + label=<<font point-size="24">US-East-1</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#147eba" + fontcolor="#147eba" + fillcolor="#ffffff" + + subgraph cluster_12 { + margin=25 + label=<<font point-size="24">Amazon RDS</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#3b48cc" + fontcolor="#3b48cc" + fillcolor="#ffffff" + + subgraph cluster_13 { + margin=25 + label=<<font point-size="24">MySQL</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#3b48cc" + fontcolor="#3b48cc" + fillcolor="#ffffff" + + 14 [id=14,shape=cylinder, label=<<font point-size="34">Database</font><br /><font point-size="19">[Container: Relational database schema]</font><br /><br /><font point-size="24">Stores information regarding<br />the veterinarians, the<br />clients, and their pets.</font>>, style=filled, color="#b2b2b2", fillcolor="#ffffff", fontcolor="#000000"] + } + + } + + 7 [id=7,shape=rect, label=<<font point-size="34">Route 53</font><br /><font point-size="19">[Infrastructure Node]</font><br /><br /><font point-size="24">Highly available and scalable<br />cloud DNS service.</font>>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] + 8 [id=8,shape=rect, label=<<font point-size="34">Elastic Load Balancer</font><br /><font point-size="19">[Infrastructure Node]</font><br /><br /><font point-size="24">Automatically distributes<br />incoming application traffic.</font>>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] + subgraph cluster_9 { + margin=25 + label=<<font point-size="24">Autoscaling group</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#cc2264" + fontcolor="#cc2264" + fillcolor="#ffffff" + + subgraph cluster_10 { + margin=25 + label=<<font point-size="24">Amazon EC2</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#d86613" + fontcolor="#d86613" + fillcolor="#ffffff" + + 11 [id=11,shape=rect, label=<<font point-size="34">Web Application</font><br /><font point-size="19">[Container: Java and Spring Boot]</font><br /><br /><font point-size="24">Allows employees to view and<br />manage information regarding<br />the veterinarians, the<br />clients, and their pets.</font>>, style=filled, color="#b2b2b2", fillcolor="#ffffff", fontcolor="#000000"] + } + + } + + } + + } + + 11 -> 14 [id=15, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[MySQL Protocol/SSL]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 7 -> 8 [id=16, label=<<font point-size="24">Forwards requests to</font><br /><font point-size="19">[HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] + 8 -> 11 [id=17, label=<<font point-size="24">Forwards requests to</font><br /><font point-size="19">[HTTPS]</font>>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java new file mode 100644 index 000000000..39f175254 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -0,0 +1,425 @@ +package com.structurizr.export.dot; + +import com.structurizr.Workspace; +import com.structurizr.export.AbstractExporterTests; +import com.structurizr.export.Diagram; +import com.structurizr.model.*; +import com.structurizr.util.WorkspaceUtils; +import com.structurizr.view.*; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DOTDiagramExporterTests extends AbstractExporterTests { + + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + DOTExporter dotWriter = new DOTExporter(); + + Collection<Diagram> diagrams = dotWriter.export(workspace); + assertEquals(7, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemContext")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-SystemContext.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-Containers.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-Components.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("SignIn")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-SignIn.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_AmazonWebServicesExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + ThemeUtils.loadThemes(workspace); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); + + DOTExporter exporter = new DOTExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_GroupsExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); + ThemeUtils.loadThemes(workspace); + + DOTExporter exporter = new DOTExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(3, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/groups-Containers.dot")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/dot/groups-Components.dot")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_NestedGroupsExample() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("Team 1"); + a.setGroup("Organisation 1/Department 1/Team 1"); + + SoftwareSystem b = workspace.getModel().addSoftwareSystem("Team 2"); + b.setGroup("Organisation 1/Department 1/Team 2"); + + SoftwareSystem c = workspace.getModel().addSoftwareSystem("Organisation 1"); + c.setGroup("Organisation 1"); + + SoftwareSystem d = workspace.getModel().addSoftwareSystem("Organisation 2"); + d.setGroup("Organisation 2"); + + SoftwareSystem e = workspace.getModel().addSoftwareSystem("Department 1"); + e.setGroup("Organisation 1/Department 1"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description"); + view.addAllElements(); + + DOTExporter exporter = new DOTExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/nested-groups.dot")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderContainerDiagramWithExternalContainers() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + + container1.uses(container2, "Uses"); + + ContainerView containerView = workspace.getViews().createContainerView(softwareSystem1, "Containers", ""); + containerView.add(container1); + containerView.add(container2); + + Diagram diagram = new DOTExporter().export(containerView); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + + " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + + " edge [fontname=\"Arial\"]\n" + + " label=<<br /><font point-size=\"34\">Software System 1 - Containers</font>>\n" + + "\n" + + " subgraph cluster_1 {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Software System 1</font><br /><font point-size=\"19\">[Software System]</font>>\n" + + " labelloc=b\n" + + " color=\"#444444\"\n" + + " fontcolor=\"#444444\"\n" + + " fillcolor=\"#444444\"\n" + + "\n" + + " 2 [id=2,shape=rect, label=<<font point-size=\"34\">Container 1</font><br /><font point-size=\"19\">[Container]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " subgraph cluster_3 {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Software System 2</font><br /><font point-size=\"19\">[Software System]</font>>\n" + + " labelloc=b\n" + + " color=\"#cccccc\"\n" + + " fontcolor=\"#cccccc\"\n" + + " fillcolor=\"#cccccc\"\n" + + "\n" + + " 4 [id=4,shape=rect, label=<<font point-size=\"34\">Container 2</font><br /><font point-size=\"19\">[Container]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " 2 -> 4 [id=5, label=<<font point-size=\"24\">Uses</font>>, style=\"dashed\", color=\"#707070\", fontcolor=\"#707070\"]\n" + + "}", diagram.getDefinition()); + } + + @Test + public void test_renderComponentDiagramWithExternalComponents() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component2 = container2.addComponent("Component 2"); + + component1.uses(component2, "Uses"); + + ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); + componentView.add(component1); + componentView.add(component2); + + Diagram diagram = new DOTExporter().export(componentView); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + + " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + + " edge [fontname=\"Arial\"]\n" + + " label=<<br /><font point-size=\"34\">Software System 1 - Container 1 - Components</font>>\n" + + "\n" + + " subgraph cluster_2 {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Container 1</font><br /><font point-size=\"19\">[Container]</font>>\n" + + " labelloc=b\n" + + " color=\"#444444\"\n" + + " fontcolor=\"#444444\"\n" + + " fillcolor=\"#444444\"\n" + + "\n" + + " 3 [id=3,shape=rect, label=<<font point-size=\"34\">Component 1</font><br /><font point-size=\"19\">[Component]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " subgraph cluster_5 {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Container 2</font><br /><font point-size=\"19\">[Container]</font>>\n" + + " labelloc=b\n" + + " color=\"#cccccc\"\n" + + " fontcolor=\"#cccccc\"\n" + + " fillcolor=\"#cccccc\"\n" + + "\n" + + " 6 [id=6,shape=rect, label=<<font point-size=\"34\">Component 2</font><br /><font point-size=\"19\">[Component]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " 3 -> 6 [id=7, label=<<font point-size=\"24\">Uses</font>>, style=\"dashed\", color=\"#707070\", fontcolor=\"#707070\"]\n" + + "}", diagram.getDefinition()); + } + + @Test + public void test_renderGroupStyles() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User 1").setGroup("Group 1"); + workspace.getModel().addPerson("User 2").setGroup("Group 2"); + workspace.getModel().addPerson("User 3").setGroup("Group 3"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", ""); + view.addDefaultElements(); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 1").color("#111111"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 2").color("#222222"); + + DOTExporter exporter = new DOTExporter(); + Diagram diagram = exporter.export(view); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + + " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + + " edge [fontname=\"Arial\"]\n" + + " label=<<br /><font point-size=\"34\">System Landscape</font>>\n" + + "\n" + + " subgraph \"cluster_group_Group 1\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 1</font>>\n" + + " labelloc=b\n" + + " color=\"#111111\"\n" + + " fontcolor=\"#111111\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 1 [id=1,shape=rect, label=<<font point-size=\"34\">User 1</font><br /><font point-size=\"19\">[Person]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_Group 2\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 2</font>>\n" + + " labelloc=b\n" + + " color=\"#222222\"\n" + + " fontcolor=\"#222222\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 2 [id=2,shape=rect, label=<<font point-size=\"34\">User 2</font><br /><font point-size=\"19\">[Person]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_Group 3\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 3</font>>\n" + + " labelloc=b\n" + + " color=\"#cccccc\"\n" + + " fontcolor=\"#cccccc\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 3 [id=3,shape=rect, label=<<font point-size=\"34\">User 3</font><br /><font point-size=\"19\">[Person]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + "\n" + + "}", diagram.getDefinition()); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); + + diagram = exporter.export(view); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + + " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + + " edge [fontname=\"Arial\"]\n" + + " label=<<br /><font point-size=\"34\">System Landscape</font>>\n" + + "\n" + + " subgraph \"cluster_group_Group 1\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 1</font>>\n" + + " labelloc=b\n" + + " color=\"#111111\"\n" + + " fontcolor=\"#111111\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 1 [id=1,shape=rect, label=<<font point-size=\"34\">User 1</font><br /><font point-size=\"19\">[Person]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_Group 2\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 2</font>>\n" + + " labelloc=b\n" + + " color=\"#222222\"\n" + + " fontcolor=\"#222222\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 2 [id=2,shape=rect, label=<<font point-size=\"34\">User 2</font><br /><font point-size=\"19\">[Person]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_Group 3\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 3</font>>\n" + + " labelloc=b\n" + + " color=\"#aabbcc\"\n" + + " fontcolor=\"#aabbcc\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 3 [id=3,shape=rect, label=<<font point-size=\"34\">User 3</font><br /><font point-size=\"19\">[Person]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + "\n" + + "}", diagram.getDefinition()); + } + + @Test + public void test_renderCustomView() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + + CustomElement a = model.addCustomElement("A"); + CustomElement b = model.addCustomElement("B", "Custom", "Description"); + a.uses(b, "Uses"); + + CustomView view = workspace.getViews().createCustomView("key", "Title", "Description"); + view.addDefaultElements(); + + Diagram diagram = new DOTExporter().export(view); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + + " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + + " edge [fontname=\"Arial\"]\n" + + " label=<<br /><font point-size=\"34\">Title</font><br /><font point-size=\"24\">Description</font>>\n" + + "\n" + + " 1 [id=1,shape=rect, label=<<font point-size=\"34\">A</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " 2 [id=2,shape=rect, label=<<font point-size=\"34\">B</font><br /><font point-size=\"19\">[Custom]</font><br /><br /><font point-size=\"24\">Description</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + "\n" + + " 1 -> 2 [id=3, label=<<font point-size=\"24\">Uses</font>>, style=\"dashed\", color=\"#707070\", fontcolor=\"#707070\"]\n" + + "}", diagram.getDefinition()); + } + + @Test + public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSeparator() { + Workspace workspace = new Workspace("Name", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + Container container1 = softwareSystem.addContainer("Container 1"); + container1.setGroup("Group 1"); + Container container2 = softwareSystem.addContainer("Container 2"); + container2.setGroup("Group 2"); + + ContainerView view = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); + view.addAllElements(); + + String expectedResult = "digraph {\n" + + " compound=true\n" + + " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + + " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + + " edge [fontname=\"Arial\"]\n" + + " label=<<br /><font point-size=\"34\">Software System - Containers</font>>\n" + + "\n" + + " subgraph cluster_1 {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Software System</font><br /><font point-size=\"19\">[Software System]</font>>\n" + + " labelloc=b\n" + + " color=\"#444444\"\n" + + " fontcolor=\"#444444\"\n" + + " fillcolor=\"#444444\"\n" + + "\n" + + " subgraph \"cluster_group_Group 1\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 1</font>>\n" + + " labelloc=b\n" + + " color=\"#cccccc\"\n" + + " fontcolor=\"#cccccc\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 2 [id=2,shape=rect, label=<<font point-size=\"34\">Container 1</font><br /><font point-size=\"19\">[Container]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_Group 2\" {\n" + + " margin=25\n" + + " label=<<font point-size=\"24\"><br />Group 2</font>>\n" + + " labelloc=b\n" + + " color=\"#cccccc\"\n" + + " fontcolor=\"#cccccc\"\n" + + " fillcolor=\"#ffffff\"\n" + + " style=\"dashed\"\n" + + "\n" + + " 3 [id=3,shape=rect, label=<<font point-size=\"34\">Container 2</font><br /><font point-size=\"19\">[Container]</font>>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + "}"; + + DOTExporter exporter = new DOTExporter(); + Diagram diagram = exporter.export(view); + assertEquals(expectedResult, diagram.getDefinition()); + + // this should be the same + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + exporter = new DOTExporter(); + diagram = exporter.export(view); + assertEquals(expectedResult, diagram.getDefinition()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot new file mode 100644 index 000000000..efa20b99e --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot @@ -0,0 +1,35 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">D - F - Components</font>> + + 3 [id=3,shape=rect, label=<<font point-size="34">C</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + + subgraph cluster_6 { + margin=25 + label=<<font point-size="24"><br />F</font><br /><font point-size="19">[Container]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + subgraph "cluster_group_Group 4" { + margin=25 + label=<<font point-size="24"><br />Group 4</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 8 [id=8,shape=rect, label=<<font point-size="34">H</font><br /><font point-size="19">[Component]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + 7 [id=7,shape=rect, label=<<font point-size="34">G</font><br /><font point-size="19">[Component]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + 3 -> 7 [id=13, label=<>, style="dashed", color="#707070", fontcolor="#707070"] + 3 -> 8 [id=15, label=<>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot new file mode 100644 index 000000000..fcddeeed4 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot @@ -0,0 +1,35 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">D - Containers</font>> + + 3 [id=3,shape=rect, label=<<font point-size="34">C</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + + subgraph cluster_4 { + margin=25 + label=<<font point-size="24"><br />D</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + subgraph "cluster_group_Group 3" { + margin=25 + label=<<font point-size="24"><br />Group 3</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 6 [id=6,shape=rect, label=<<font point-size="34">F</font><br /><font point-size="19">[Container]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + 5 [id=5,shape=rect, label=<<font point-size="34">E</font><br /><font point-size="19">[Container]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + 3 -> 5 [id=11, label=<>, style="dashed", color="#707070", fontcolor="#707070"] + 3 -> 6 [id=14, label=<>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot new file mode 100644 index 000000000..5daee1e84 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot @@ -0,0 +1,48 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">System Landscape</font>> + + subgraph cluster_enterprise { + margin=25 + label=<<font point-size="24"><br />Enterprise</font><br /><font point-size="19">[Enterprise]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph "cluster_group_Group 2" { + margin=25 + label=<<font point-size="24"><br />Group 2</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 4 [id=4,shape=rect, label=<<font point-size="34">D</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + 3 [id=3,shape=rect, label=<<font point-size="34">C</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + subgraph "cluster_group_Group 1" { + margin=25 + label=<<font point-size="24"><br />Group 1</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<<font point-size="34">B</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + 1 [id=1,shape=rect, label=<<font point-size="34">A</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + + 2 -> 3 [id=10, label=<>, style="dashed", color="#707070", fontcolor="#707070"] + 3 -> 4 [id=12, label=<>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 2 [id=9, label=<>, style="dashed", color="#707070", fontcolor="#707070"] +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot new file mode 100644 index 000000000..702f73d43 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot @@ -0,0 +1,69 @@ +digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<<br /><font point-size="34">System Landscape</font><br /><font point-size="24">Description</font>> + + subgraph "cluster_group_Organisation 1" { + margin=25 + label=<<font point-size="24"><br />Organisation 1</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 3 [id=3,shape=rect, label=<<font point-size="34">Organisation 1</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + subgraph "cluster_group_Department 1" { + margin=25 + label=<<font point-size="24"><br />Department 1</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 5 [id=5,shape=rect, label=<<font point-size="34">Department 1</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + subgraph "cluster_group_Team 1" { + margin=25 + label=<<font point-size="24"><br />Team 1</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 1 [id=1,shape=rect, label=<<font point-size="34">Team 1</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + subgraph "cluster_group_Team 2" { + margin=25 + label=<<font point-size="24"><br />Team 2</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<<font point-size="34">Team 2</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + } + + } + + subgraph "cluster_group_Organisation 2" { + margin=25 + label=<<font point-size="24"><br />Organisation 2</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 4 [id=4,shape=rect, label=<<font point-size="34">Organisation 2</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph new file mode 100644 index 000000000..2452b8dbc --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph @@ -0,0 +1,631 @@ +resources: + + - id: "1" + name: "Personal Banking Customer" + subtitle: "[Person]" + description: "A customer of the bank, with personal bank accounts." + backgroundColor: "#08427b" + color: "#ffffff" + + - id: "12" + name: "Customer Service Staff" + subtitle: "[Person]" + description: "Customer service staff within the bank." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "15" + name: "Back Office Staff" + subtitle: "[Person]" + description: "Administration and support staff within the bank." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "2" + name: "Internet Banking System" + subtitle: "[Software System]" + description: "Allows customers to view information about their bank accounts, and make payments." + backgroundColor: "#1168bd" + color: "#ffffff" + + children: + - id: "17" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "18" + name: "Mobile App" + subtitle: "[Container: Xamarin]" + description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "19" + name: "Web Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Delivers the static content and the Internet banking single page application." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "20" + name: "API Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Provides Internet banking functionality via a JSON/HTTPS API." + backgroundColor: "#438dd5" + color: "#ffffff" + + children: + - id: "29" + name: "Sign In Controller" + subtitle: "[Component: Spring MVC Rest Controller]" + description: "Allows users to sign in to the Internet Banking System." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "30" + name: "Accounts Summary Controller" + subtitle: "[Component: Spring MVC Rest Controller]" + description: "Provides customers with a summary of their bank accounts." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "31" + name: "Reset Password Controller" + subtitle: "[Component: Spring MVC Rest Controller]" + description: "Allows users to reset their passwords with a single use URL." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "32" + name: "Security Component" + subtitle: "[Component: Spring Bean]" + description: "Provides functionality related to signing in, changing passwords, etc." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "33" + name: "Mainframe Banking System Facade" + subtitle: "[Component: Spring Bean]" + description: "A facade onto the mainframe banking system." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "34" + name: "E-mail Component" + subtitle: "[Component: Spring Bean]" + description: "Sends e-mails to users." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "21" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "4" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "6" + name: "E-mail System" + subtitle: "[Software System]" + description: "The internal Microsoft Exchange e-mail system." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "9" + name: "ATM" + subtitle: "[Software System]" + description: "Allows customers to withdraw cash." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "50" + name: "Developer Laptop" + subtitle: "[Deployment Node: Microsoft Windows 10 or Apple macOS]" + description: "A developer laptop." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "51" + name: "Docker Container - Web Server" + subtitle: "[Deployment Node: Docker]" + description: "A Docker container." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "52" + name: "Apache Tomcat" + subtitle: "[Deployment Node: Apache Tomcat 8.x]" + description: "An open source Java EE web server." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "53" + name: "Web Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Delivers the static content and the Internet banking single page application." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "54" + name: "API Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Provides Internet banking functionality via a JSON/HTTPS API." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "59" + name: "Docker Container - Database Server" + subtitle: "[Deployment Node: Docker]" + description: "A Docker container." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "60" + name: "Database Server" + subtitle: "[Deployment Node: Oracle 12c]" + description: "A development database." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "61" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "63" + name: "Web Browser" + subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "64" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "55" + name: "Big Bank plc" + subtitle: "[Deployment Node: Big Bank plc data center]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "56" + name: "bigbank-dev001" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "57" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "67" + name: "Customer's mobile device" + subtitle: "[Deployment Node: Apple iOS or Android]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "68" + name: "Mobile App" + subtitle: "[Container: Xamarin]" + description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "69" + name: "Customer's computer" + subtitle: "[Deployment Node: Microsoft Windows or Apple macOS]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "70" + name: "Web Browser" + subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "71" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "72" + name: "Big Bank plc" + subtitle: "[Deployment Node: Big Bank plc data center]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "73" + name: "bigbank-prod001" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "74" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "75" + name: "bigbank-web***" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + description: "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "76" + name: "Apache Tomcat" + subtitle: "[Deployment Node: Apache Tomcat 8.x]" + description: "An open source Java EE web server." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "77" + name: "Web Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Delivers the static content and the Internet banking single page application." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "79" + name: "bigbank-api***" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + description: "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "80" + name: "Apache Tomcat" + subtitle: "[Deployment Node: Apache Tomcat 8.x]" + description: "An open source Java EE web server." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "81" + name: "API Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Provides Internet banking functionality via a JSON/HTTPS API." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "85" + name: "bigbank-db01" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + description: "The primary database server." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "86" + name: "Oracle - Primary" + subtitle: "[Deployment Node: Oracle 12c]" + description: "The primary, live database server." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "87" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "89" + name: "bigbank-db02" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + description: "The secondary database server." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "90" + name: "Oracle - Secondary" + subtitle: "[Deployment Node: Oracle 12c]" + description: "A secondary, standby database server, used for failover purposes only." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "91" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + +perspectives: + - name: Static Structure + relations: + - from: "1" + to: "9" + label: "Withdraws cash using" + color: "#707070" + + - from: "1" + to: "12" + label: "Asks questions to" + description: "Telephone" + color: "#707070" + + - from: "1" + to: "2" + label: "Views account balances, and makes payments using" + color: "#707070" + + - from: "12" + to: "4" + label: "Uses" + color: "#707070" + + - from: "15" + to: "4" + label: "Uses" + color: "#707070" + + - from: "2" + to: "4" + label: "Gets account information from, and makes payments using" + color: "#707070" + + - from: "2" + to: "6" + label: "Sends e-mail using" + color: "#707070" + + - from: "6" + to: "1" + label: "Sends e-mails to" + color: "#707070" + + - from: "9" + to: "4" + label: "Uses" + color: "#707070" + + - from: "1" + to: "19" + label: "Visits bigbank.com/ib using" + description: "HTTPS" + color: "#707070" + + - from: "1" + to: "17" + label: "Views account balances, and makes payments using" + color: "#707070" + + - from: "1" + to: "18" + label: "Views account balances, and makes payments using" + color: "#707070" + + - from: "17" + to: "20" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "18" + to: "20" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "19" + to: "17" + label: "Delivers to the customer's web browser" + color: "#707070" + + - from: "20" + to: "21" + label: "Reads from and writes to" + description: "JDBC" + color: "#707070" + + - from: "20" + to: "4" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#707070" + + - from: "20" + to: "6" + label: "Sends e-mail using" + description: "SMTP" + color: "#707070" + + - from: "17" + to: "29" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "17" + to: "31" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "17" + to: "30" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "18" + to: "29" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "18" + to: "31" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "18" + to: "30" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + + - from: "29" + to: "32" + label: "Uses" + color: "#707070" + + - from: "30" + to: "33" + label: "Uses" + color: "#707070" + + - from: "31" + to: "32" + label: "Uses" + color: "#707070" + + - from: "31" + to: "34" + label: "Uses" + color: "#707070" + + - from: "32" + to: "21" + label: "Reads from and writes to" + description: "JDBC" + color: "#707070" + + - from: "33" + to: "4" + label: "Uses" + description: "XML/HTTPS" + color: "#707070" + + - from: "34" + to: "6" + label: "Sends e-mail using" + color: "#707070" + + - name: Dynamic - API Application - Dynamic + sequence: + start: "17" + steps: + - to: "29" + label: "1. Submits credentials to" + description: "JSON/HTTPS" + color: "#707070" + + - to: "32" + label: "2. Validates credentials using" + color: "#707070" + + - to: "21" + label: "3. select * from users where username = ?" + description: "JDBC" + color: "#707070" + + - to: "32" + label: "4. Returns user data to" + description: "JDBC" + color: "#707070" + + - to: "29" + label: "5. Returns true if the hashed password matches" + color: "#707070" + + - to: "17" + label: "6. Sends back an authentication token to" + description: "JSON/HTTPS" + color: "#707070" + + - name: Deployment - Development + relations: + - from: "53" + to: "64" + label: "Delivers to the customer's web browser" + color: "#707070" + - from: "54" + to: "57" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#707070" + - from: "54" + to: "61" + label: "Reads from and writes to" + description: "JDBC" + color: "#707070" + - from: "64" + to: "54" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + - name: Deployment - Live + relations: + - from: "68" + to: "81" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + - from: "71" + to: "81" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#707070" + - from: "77" + to: "71" + label: "Delivers to the customer's web browser" + color: "#707070" + - from: "81" + to: "74" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#707070" + - from: "81" + to: "87" + label: "Reads from and writes to" + description: "JDBC" + color: "#707070" + - from: "81" + to: "91" + label: "Reads from and writes to" + description: "JDBC" + color: "#707070" diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph new file mode 100644 index 000000000..9689909c5 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph @@ -0,0 +1,120 @@ +resources: + + - id: "1" + name: "Spring PetClinic" + subtitle: "[Software System]" + description: "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "2" + name: "Web Application" + subtitle: "[Container: Java and Spring Boot]" + description: "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." + backgroundColor: "#ffffff" + color: "#000000" + + - id: "3" + name: "Database" + subtitle: "[Container: Relational database schema]" + description: "Stores information regarding the veterinarians, the clients, and their pets." + backgroundColor: "#ffffff" + color: "#000000" + + - id: "5" + name: "Amazon Web Services" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#232f3e" + + children: + - id: "6" + name: "US-East-1" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#147eba" + + children: + - id: "12" + name: "Amazon RDS" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#3b48cc" + + children: + - id: "13" + name: "MySQL" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#3b48cc" + + children: + - id: "14" + name: "Database" + subtitle: "[Container: Relational database schema]" + description: "Stores information regarding the veterinarians, the clients, and their pets." + backgroundColor: "#ffffff" + color: "#000000" + + - id: "9" + name: "Autoscaling group" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#cc2264" + + children: + - id: "10" + name: "Amazon EC2" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#d86613" + + children: + - id: "11" + name: "Web Application" + subtitle: "[Container: Java and Spring Boot]" + description: "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." + backgroundColor: "#ffffff" + color: "#000000" + + - id: "7" + name: "Route 53" + subtitle: "[Infrastructure Node]" + description: "Highly available and scalable cloud DNS service." + backgroundColor: "#ffffff" + color: "#693cc5" + + - id: "8" + name: "Elastic Load Balancer" + subtitle: "[Infrastructure Node]" + description: "Automatically distributes incoming application traffic." + backgroundColor: "#ffffff" + color: "#693cc5" + +perspectives: + - name: Static Structure + relations: + - from: "2" + to: "3" + label: "Reads from and writes to" + description: "MySQL Protocol/SSL" + color: "#707070" + + - name: Deployment - Live + relations: + - from: "11" + to: "14" + label: "Reads from and writes to" + description: "MySQL Protocol/SSL" + color: "#707070" + - from: "7" + to: "8" + label: "Forwards requests to" + description: "HTTPS" + color: "#707070" + - from: "8" + to: "11" + label: "Forwards requests to" + description: "HTTPS" + color: "#707070" \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographWriterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographWriterTests.java new file mode 100644 index 000000000..3cca4626d --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographWriterTests.java @@ -0,0 +1,76 @@ +package com.structurizr.export.ilograph; + +import com.structurizr.Workspace; +import com.structurizr.export.AbstractExporterTests; +import com.structurizr.export.Diagram; +import com.structurizr.export.WorkspaceExport; +import com.structurizr.export.dot.DOTExporter; +import com.structurizr.model.CustomElement; +import com.structurizr.model.Model; +import com.structurizr.util.WorkspaceUtils; +import com.structurizr.view.CustomView; +import com.structurizr.view.ThemeUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class IlographWriterTests extends AbstractExporterTests { + + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + IlographExporter ilographExporter = new IlographExporter(); + WorkspaceExport export = ilographExporter.export(workspace); + + String expected = readFile(new File("./src/test/java/com/structurizr/export/ilograph/36141.ilograph")); + assertEquals(expected, export.getDefinition()); + } + + @Test + public void test_AmazonWebServicesExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + ThemeUtils.loadThemes(workspace); + IlographExporter ilographExporter = new IlographExporter(); + WorkspaceExport export = ilographExporter.export(workspace); + + String expected = readFile(new File("./src/test/java/com/structurizr/export/ilograph/54915.ilograph")); + assertEquals(expected, export.getDefinition()); + } + + @Test + public void test_renderCustomElements() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + + CustomElement a = model.addCustomElement("A"); + CustomElement b = model.addCustomElement("B", "Custom", "Description"); + a.uses(b, "Uses"); + + WorkspaceExport export = new IlographExporter().export(workspace); + assertEquals("resources:\n" + + "\n" + + " - id: \"1\"\n" + + " name: \"A\"\n" + + " subtitle: \"\"\n" + + " backgroundColor: \"#dddddd\"\n" + + " color: \"#000000\"\n" + + "\n" + + " - id: \"2\"\n" + + " name: \"B\"\n" + + " subtitle: \"[Custom]\"\n" + + " description: \"Description\"\n" + + " backgroundColor: \"#dddddd\"\n" + + " color: \"#000000\"\n" + + "\n" + + "perspectives:\n" + + " - name: Static Structure\n" + + " relations:\n" + + " - from: \"1\"\n" + + " to: \"2\"\n" + + " label: \"Uses\"\n" + + " color: \"#707070\"\n", export.getDefinition()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd new file mode 100644 index 000000000..dcae0ac7f --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd @@ -0,0 +1,48 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Internet Banking System - API Application - Components"] + style diagram fill:#ffffff,stroke:#ffffff + + 4["<div style='font-weight: bold'>Mainframe Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Stores all of the core<br />banking information about<br />customers, accounts,<br />transactions, etc.</div>"] + style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff + 17["<div style='font-weight: bold'>Single-Page Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: JavaScript and Angular]</div><div style='font-size: 80%; margin-top:10px'>Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</div>"] + style 17 fill:#438dd5,stroke:#2e6295,color:#ffffff + 6["<div style='font-weight: bold'>E-mail System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>The internal Microsoft<br />Exchange e-mail system.</div>"] + style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff + 18["<div style='font-weight: bold'>Mobile App</div><div style='font-size: 70%; margin-top: 0px'>[Container: Xamarin]</div><div style='font-size: 80%; margin-top:10px'>Provides a limited subset of<br />the Internet banking<br />functionality to customers<br />via their mobile device.</div>"] + style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff + 21[("<div style='font-weight: bold'>Database</div><div style='font-size: 70%; margin-top: 0px'>[Container: Oracle Database Schema]</div><div style='font-size: 80%; margin-top:10px'>Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</div>")] + style 21 fill:#438dd5,stroke:#2e6295,color:#ffffff + + subgraph 20 [API Application] + style 20 fill:#ffffff,stroke:#2e6295,color:#2e6295 + + 29["<div style='font-weight: bold'>Sign In Controller</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring MVC Rest Controller]</div><div style='font-size: 80%; margin-top:10px'>Allows users to sign in to<br />the Internet Banking System.</div>"] + style 29 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 30["<div style='font-weight: bold'>Accounts Summary Controller</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring MVC Rest Controller]</div><div style='font-size: 80%; margin-top:10px'>Provides customers with a<br />summary of their bank<br />accounts.</div>"] + style 30 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 31["<div style='font-weight: bold'>Reset Password Controller</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring MVC Rest Controller]</div><div style='font-size: 80%; margin-top:10px'>Allows users to reset their<br />passwords with a single use<br />URL.</div>"] + style 31 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 32["<div style='font-weight: bold'>Security Component</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring Bean]</div><div style='font-size: 80%; margin-top:10px'>Provides functionality<br />related to signing in,<br />changing passwords, etc.</div>"] + style 32 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 33["<div style='font-weight: bold'>Mainframe Banking System Facade</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring Bean]</div><div style='font-size: 80%; margin-top:10px'>A facade onto the mainframe<br />banking system.</div>"] + style 33 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 34["<div style='font-weight: bold'>E-mail Component</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring Bean]</div><div style='font-size: 80%; margin-top:10px'>Sends e-mails to users.</div>"] + style 34 fill:#85bbf0,stroke:#5d82a8,color:#000000 + end + + 17-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->29 + 17-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->31 + 17-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->30 + 18-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->29 + 18-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->31 + 18-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->30 + 29-. "<div>Uses</div><div style='font-size: 70%'></div>" .->32 + 30-. "<div>Uses</div><div style='font-size: 70%'></div>" .->33 + 31-. "<div>Uses</div><div style='font-size: 70%'></div>" .->32 + 31-. "<div>Uses</div><div style='font-size: 70%'></div>" .->34 + 32-. "<div>Reads from and writes to</div><div style='font-size: 70%'>[JDBC]</div>" .->21 + 33-. "<div>Uses</div><div style='font-size: 70%'>[XML/HTTPS]</div>" .->4 + 34-. "<div>Sends e-mail using</div><div style='font-size: 70%'></div>" .->6 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd new file mode 100644 index 000000000..4cda955bd --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd @@ -0,0 +1,39 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Internet Banking System - Containers"] + style diagram fill:#ffffff,stroke:#ffffff + + 1["<div style='font-weight: bold'>Personal Banking Customer</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div><div style='font-size: 80%; margin-top:10px'>A customer of the bank, with<br />personal bank accounts.</div>"] + style 1 fill:#08427b,stroke:#052e56,color:#ffffff + 4["<div style='font-weight: bold'>Mainframe Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Stores all of the core<br />banking information about<br />customers, accounts,<br />transactions, etc.</div>"] + style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff + 6["<div style='font-weight: bold'>E-mail System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>The internal Microsoft<br />Exchange e-mail system.</div>"] + style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff + + subgraph 2 [Internet Banking System] + style 2 fill:#ffffff,stroke:#0b4884,color:#0b4884 + + 17["<div style='font-weight: bold'>Single-Page Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: JavaScript and Angular]</div><div style='font-size: 80%; margin-top:10px'>Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</div>"] + style 17 fill:#438dd5,stroke:#2e6295,color:#ffffff + 18["<div style='font-weight: bold'>Mobile App</div><div style='font-size: 70%; margin-top: 0px'>[Container: Xamarin]</div><div style='font-size: 80%; margin-top:10px'>Provides a limited subset of<br />the Internet banking<br />functionality to customers<br />via their mobile device.</div>"] + style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff + 19["<div style='font-weight: bold'>Web Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring MVC]</div><div style='font-size: 80%; margin-top:10px'>Delivers the static content<br />and the Internet banking<br />single page application.</div>"] + style 19 fill:#438dd5,stroke:#2e6295,color:#ffffff + 20["<div style='font-weight: bold'>API Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring MVC]</div><div style='font-size: 80%; margin-top:10px'>Provides Internet banking<br />functionality via a<br />JSON/HTTPS API.</div>"] + style 20 fill:#438dd5,stroke:#2e6295,color:#ffffff + 21[("<div style='font-weight: bold'>Database</div><div style='font-size: 70%; margin-top: 0px'>[Container: Oracle Database Schema]</div><div style='font-size: 80%; margin-top:10px'>Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</div>")] + style 21 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + 1-. "<div>Visits bigbank.com/ib using</div><div style='font-size: 70%'>[HTTPS]</div>" .->19 + 1-. "<div>Views account balances, and<br />makes payments using</div><div style='font-size: 70%'></div>" .->17 + 1-. "<div>Views account balances, and<br />makes payments using</div><div style='font-size: 70%'></div>" .->18 + 19-. "<div>Delivers to the customer's<br />web browser</div><div style='font-size: 70%'></div>" .->17 + 20-. "<div>Reads from and writes to</div><div style='font-size: 70%'>[JDBC]</div>" .->21 + 20-. "<div>Makes API calls to</div><div style='font-size: 70%'>[XML/HTTPS]</div>" .->4 + 20-. "<div>Sends e-mail using</div><div style='font-size: 70%'>[SMTP]</div>" .->6 + 17-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->20 + 18-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->20 + 6-. "<div>Sends e-mails to</div><div style='font-size: 70%'></div>" .->1 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd new file mode 100644 index 000000000..14b3194e6 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd @@ -0,0 +1,61 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Internet Banking System - Deployment - Development"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 50 [Developer Laptop] + style 50 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 51 [Docker Container - Web Server] + style 51 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 52 [Apache Tomcat] + style 52 fill:#ffffff,stroke:#888888,color:#000000 + + 53["<div style='font-weight: bold'>Web Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring MVC]</div><div style='font-size: 80%; margin-top:10px'>Delivers the static content<br />and the Internet banking<br />single page application.</div>"] + style 53 fill:#438dd5,stroke:#2e6295,color:#ffffff + 54["<div style='font-weight: bold'>API Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring MVC]</div><div style='font-size: 80%; margin-top:10px'>Provides Internet banking<br />functionality via a<br />JSON/HTTPS API.</div>"] + style 54 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + subgraph 59 [Docker Container - Database Server] + style 59 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 60 [Database Server] + style 60 fill:#ffffff,stroke:#888888,color:#000000 + + 61[("<div style='font-weight: bold'>Database</div><div style='font-size: 70%; margin-top: 0px'>[Container: Oracle Database Schema]</div><div style='font-size: 80%; margin-top:10px'>Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</div>")] + style 61 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + subgraph 63 [Web Browser] + style 63 fill:#ffffff,stroke:#888888,color:#000000 + + 64["<div style='font-weight: bold'>Single-Page Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: JavaScript and Angular]</div><div style='font-size: 80%; margin-top:10px'>Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</div>"] + style 64 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + subgraph 55 [Big Bank plc] + style 55 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 56 [bigbank-dev001] + style 56 fill:#ffffff,stroke:#888888,color:#000000 + + 57["<div style='font-weight: bold'>Mainframe Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Stores all of the core<br />banking information about<br />customers, accounts,<br />transactions, etc.</div>"] + style 57 fill:#999999,stroke:#6b6b6b,color:#ffffff + end + + end + + 54-. "<div>Makes API calls to</div><div style='font-size: 70%'>[XML/HTTPS]</div>" .->57 + 54-. "<div>Reads from and writes to</div><div style='font-size: 70%'>[JDBC]</div>" .->61 + 64-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->54 + 53-. "<div>Delivers to the customer's<br />web browser</div><div style='font-size: 70%'></div>" .->64 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd new file mode 100644 index 000000000..73b4fb23c --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd @@ -0,0 +1,92 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Internet Banking System - Deployment - Live"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 67 [Customer's mobile device] + style 67 fill:#ffffff,stroke:#888888,color:#000000 + + 68["<div style='font-weight: bold'>Mobile App</div><div style='font-size: 70%; margin-top: 0px'>[Container: Xamarin]</div><div style='font-size: 80%; margin-top:10px'>Provides a limited subset of<br />the Internet banking<br />functionality to customers<br />via their mobile device.</div>"] + style 68 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + subgraph 69 [Customer's computer] + style 69 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 70 [Web Browser] + style 70 fill:#ffffff,stroke:#888888,color:#000000 + + 71["<div style='font-weight: bold'>Single-Page Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: JavaScript and Angular]</div><div style='font-size: 80%; margin-top:10px'>Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</div>"] + style 71 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + subgraph 72 [Big Bank plc] + style 72 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 73 [bigbank-prod001] + style 73 fill:#ffffff,stroke:#888888,color:#000000 + + 74["<div style='font-weight: bold'>Mainframe Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Stores all of the core<br />banking information about<br />customers, accounts,<br />transactions, etc.</div>"] + style 74 fill:#999999,stroke:#6b6b6b,color:#ffffff + end + + subgraph 75 [bigbank-web***] + style 75 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 76 [Apache Tomcat] + style 76 fill:#ffffff,stroke:#888888,color:#000000 + + 77["<div style='font-weight: bold'>Web Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring MVC]</div><div style='font-size: 80%; margin-top:10px'>Delivers the static content<br />and the Internet banking<br />single page application.</div>"] + style 77 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + subgraph 79 [bigbank-api***] + style 79 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 80 [Apache Tomcat] + style 80 fill:#ffffff,stroke:#888888,color:#000000 + + 81["<div style='font-weight: bold'>API Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring MVC]</div><div style='font-size: 80%; margin-top:10px'>Provides Internet banking<br />functionality via a<br />JSON/HTTPS API.</div>"] + style 81 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + subgraph 85 [bigbank-db01] + style 85 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 86 [Oracle - Primary] + style 86 fill:#ffffff,stroke:#888888,color:#000000 + + 87[("<div style='font-weight: bold'>Database</div><div style='font-size: 70%; margin-top: 0px'>[Container: Oracle Database Schema]</div><div style='font-size: 80%; margin-top:10px'>Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</div>")] + style 87 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + subgraph 89 [bigbank-db02] + style 89 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 90 [Oracle - Secondary] + style 90 fill:#ffffff,stroke:#888888,color:#000000 + + 91[("<div style='font-weight: bold'>Database</div><div style='font-size: 70%; margin-top: 0px'>[Container: Oracle Database Schema]</div><div style='font-size: 80%; margin-top:10px'>Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</div>")] + style 91 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + end + + end + + 77-. "<div>Delivers to the customer's<br />web browser</div><div style='font-size: 70%'></div>" .->71 + 68-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->81 + 71-. "<div>Makes API calls to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->81 + 81-. "<div>Makes API calls to</div><div style='font-size: 70%'>[XML/HTTPS]</div>" .->74 + 81-. "<div>Reads from and writes to</div><div style='font-size: 70%'>[JDBC]</div>" .->87 + 81-. "<div>Reads from and writes to</div><div style='font-size: 70%'>[JDBC]</div>" .->91 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd new file mode 100644 index 000000000..655ff882c --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd @@ -0,0 +1,13 @@ +sequenceDiagram + + participant 17 as Single-Page Application<br />[Container: JavaScript and Angular] + participant 29 as Sign In Controller<br />[Component: Spring MVC Rest Controller] + participant 32 as Security Component<br />[Component: Spring Bean] + participant 21 as Database<br />[Container: Oracle Database Schema] + + 17->>29: Submits credentials to<br />[JSON/HTTPS] + 29->>32: Validates credentials using + 32->>21: select * from users where username = ?<br />[JDBC] + 21-->>32: Returns user data to<br />[JDBC] + 32-->>29: Returns true if the hashed password matches + 29-->>17: Sends back an authentication token to<br />[JSON/HTTPS] \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd new file mode 100644 index 000000000..ad523b076 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd @@ -0,0 +1,27 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["API Application - Dynamic"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 20 [API Application] + style 20 fill:#ffffff,stroke:#2e6295,color:#2e6295 + + 29["<div style='font-weight: bold'>Sign In Controller</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring MVC Rest Controller]</div><div style='font-size: 80%; margin-top:10px'>Allows users to sign in to<br />the Internet Banking System.</div>"] + style 29 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 32["<div style='font-weight: bold'>Security Component</div><div style='font-size: 70%; margin-top: 0px'>[Component: Spring Bean]</div><div style='font-size: 80%; margin-top:10px'>Provides functionality<br />related to signing in,<br />changing passwords, etc.</div>"] + style 32 fill:#85bbf0,stroke:#5d82a8,color:#000000 + end + + 17["<div style='font-weight: bold'>Single-Page Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: JavaScript and Angular]</div><div style='font-size: 80%; margin-top:10px'>Provides all of the Internet<br />banking functionality to<br />customers via their web<br />browser.</div>"] + style 17 fill:#438dd5,stroke:#2e6295,color:#ffffff + 21[("<div style='font-weight: bold'>Database</div><div style='font-size: 70%; margin-top: 0px'>[Container: Oracle Database Schema]</div><div style='font-size: 80%; margin-top:10px'>Stores user registration<br />information, hashed<br />authentication credentials,<br />access logs, etc.</div>")] + style 21 fill:#438dd5,stroke:#2e6295,color:#ffffff + + 17-. "<div>1. Submits credentials to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->29 + 29-. "<div>2. Validates credentials<br />using</div><div style='font-size: 70%'></div>" .->32 + 32-. "<div>3. select * from users where<br />username = ?</div><div style='font-size: 70%'>[JDBC]</div>" .->21 + 21-. "<div>4. Returns user data to</div><div style='font-size: 70%'>[JDBC]</div>" .->32 + 32-. "<div>5. Returns true if the hashed<br />password matches</div><div style='font-size: 70%'></div>" .->29 + 29-. "<div>6. Sends back an<br />authentication token to</div><div style='font-size: 70%'>[JSON/HTTPS]</div>" .->17 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd new file mode 100644 index 000000000..184335671 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd @@ -0,0 +1,20 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Internet Banking System - System Context"] + style diagram fill:#ffffff,stroke:#ffffff + + 1["<div style='font-weight: bold'>Personal Banking Customer</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div><div style='font-size: 80%; margin-top:10px'>A customer of the bank, with<br />personal bank accounts.</div>"] + style 1 fill:#08427b,stroke:#052e56,color:#ffffff + 2["<div style='font-weight: bold'>Internet Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Allows customers to view<br />information about their bank<br />accounts, and make payments.</div>"] + style 2 fill:#1168bd,stroke:#0b4884,color:#ffffff + 4["<div style='font-weight: bold'>Mainframe Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Stores all of the core<br />banking information about<br />customers, accounts,<br />transactions, etc.</div>"] + style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff + 6["<div style='font-weight: bold'>E-mail System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>The internal Microsoft<br />Exchange e-mail system.</div>"] + style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff + + 1-. "<div>Views account balances, and<br />makes payments using</div><div style='font-size: 70%'></div>" .->2 + 2-. "<div>Gets account information<br />from, and makes payments<br />using</div><div style='font-size: 70%'></div>" .->4 + 2-. "<div>Sends e-mail using</div><div style='font-size: 70%'></div>" .->6 + 6-. "<div>Sends e-mails to</div><div style='font-size: 70%'></div>" .->1 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd new file mode 100644 index 000000000..9e382989d --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd @@ -0,0 +1,36 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph enterprise [Big Bank plc] + style enterprise fill:#ffffff,stroke:#444444,color:#444444 + + 12["<div style='font-weight: bold'>Customer Service Staff</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div><div style='font-size: 80%; margin-top:10px'>Customer service staff within<br />the bank.</div>"] + style 12 fill:#999999,stroke:#6b6b6b,color:#ffffff + 15["<div style='font-weight: bold'>Back Office Staff</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div><div style='font-size: 80%; margin-top:10px'>Administration and support<br />staff within the bank.</div>"] + style 15 fill:#999999,stroke:#6b6b6b,color:#ffffff + 2["<div style='font-weight: bold'>Internet Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Allows customers to view<br />information about their bank<br />accounts, and make payments.</div>"] + style 2 fill:#1168bd,stroke:#0b4884,color:#ffffff + 4["<div style='font-weight: bold'>Mainframe Banking System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Stores all of the core<br />banking information about<br />customers, accounts,<br />transactions, etc.</div>"] + style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff + 6["<div style='font-weight: bold'>E-mail System</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>The internal Microsoft<br />Exchange e-mail system.</div>"] + style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff + 9["<div style='font-weight: bold'>ATM</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div><div style='font-size: 80%; margin-top:10px'>Allows customers to withdraw<br />cash.</div>"] + style 9 fill:#999999,stroke:#6b6b6b,color:#ffffff + end + + 1["<div style='font-weight: bold'>Personal Banking Customer</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div><div style='font-size: 80%; margin-top:10px'>A customer of the bank, with<br />personal bank accounts.</div>"] + style 1 fill:#08427b,stroke:#052e56,color:#ffffff + + 9-. "<div>Uses</div><div style='font-size: 70%'></div>" .->4 + 1-. "<div>Withdraws cash using</div><div style='font-size: 70%'></div>" .->9 + 12-. "<div>Uses</div><div style='font-size: 70%'></div>" .->4 + 1-. "<div>Asks questions to</div><div style='font-size: 70%'>[Telephone]</div>" .->12 + 15-. "<div>Uses</div><div style='font-size: 70%'></div>" .->4 + 1-. "<div>Views account balances, and<br />makes payments using</div><div style='font-size: 70%'></div>" .->2 + 2-. "<div>Gets account information<br />from, and makes payments<br />using</div><div style='font-size: 70%'></div>" .->4 + 2-. "<div>Sends e-mail using</div><div style='font-size: 70%'></div>" .->6 + 6-. "<div>Sends e-mails to</div><div style='font-size: 70%'></div>" .->1 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd new file mode 100644 index 000000000..dac7a2f2e --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd @@ -0,0 +1,48 @@ +graph LR + linkStyle default fill:#ffffff + + subgraph diagram ["Spring PetClinic - Deployment - Live"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 5 [Amazon Web Services] + style 5 fill:#ffffff,stroke:#232f3e,color:#232f3e + + subgraph 6 [US-East-1] + style 6 fill:#ffffff,stroke:#147eba,color:#147eba + + subgraph 12 [Amazon RDS] + style 12 fill:#ffffff,stroke:#3b48cc,color:#3b48cc + + subgraph 13 [MySQL] + style 13 fill:#ffffff,stroke:#3b48cc,color:#3b48cc + + 14[("<div style='font-weight: bold'>Database</div><div style='font-size: 70%; margin-top: 0px'>[Container: Relational database schema]</div><div style='font-size: 80%; margin-top:10px'>Stores information regarding<br />the veterinarians, the<br />clients, and their pets.</div>")] + style 14 fill:#ffffff,stroke:#b2b2b2,color:#000000 + end + + end + + 7("<div style='font-weight: bold'>Route 53</div><div style='font-size: 70%; margin-top: 0px'>[Infrastructure Node]</div><div style='font-size: 80%; margin-top:10px'>Highly available and scalable<br />cloud DNS service.</div>") + style 7 fill:#ffffff,stroke:#693cc5,color:#693cc5 + 8("<div style='font-weight: bold'>Elastic Load Balancer</div><div style='font-size: 70%; margin-top: 0px'>[Infrastructure Node]</div><div style='font-size: 80%; margin-top:10px'>Automatically distributes<br />incoming application traffic.</div>") + style 8 fill:#ffffff,stroke:#693cc5,color:#693cc5 + subgraph 9 [Autoscaling group] + style 9 fill:#ffffff,stroke:#cc2264,color:#cc2264 + + subgraph 10 [Amazon EC2] + style 10 fill:#ffffff,stroke:#d86613,color:#d86613 + + 11("<div style='font-weight: bold'>Web Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring Boot]</div><div style='font-size: 80%; margin-top:10px'>Allows employees to view and<br />manage information regarding<br />the veterinarians, the<br />clients, and their pets.</div>") + style 11 fill:#ffffff,stroke:#b2b2b2,color:#000000 + end + + end + + end + + end + + 11-. "<div>Reads from and writes to</div><div style='font-size: 70%'>[MySQL Protocol/SSL]</div>" .->14 + 7-. "<div>Forwards requests to</div><div style='font-size: 70%'>[HTTPS]</div>" .->8 + 8-. "<div>Forwards requests to</div><div style='font-size: 70%'>[HTTPS]</div>" .->11 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java new file mode 100644 index 000000000..e01c6ca53 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -0,0 +1,317 @@ +package com.structurizr.export.mermaid; + +import com.structurizr.Workspace; +import com.structurizr.export.AbstractExporterTests; +import com.structurizr.export.Diagram; +import com.structurizr.model.*; +import com.structurizr.util.WorkspaceUtils; +import com.structurizr.view.*; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MermaidDiagramExporterTests extends AbstractExporterTests { + + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(7, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-Components.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd")); + assertEquals(expected, diagram.getDefinition()); + + // and the sequence diagram version + workspace.getViews().getConfiguration().addProperty(exporter.MERMAID_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/mermaid/36141-SignIn-sequence.mmd")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_AmazonWebServicesExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + ThemeUtils.loadThemes(workspace); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); + + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_GroupsExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); + ThemeUtils.loadThemes(workspace); + + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(3, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/groups-Components.mmd")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_NestedGroupsExample() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("Team 1"); + a.setGroup("Organisation 1/Department 1/Team 1"); + + SoftwareSystem b = workspace.getModel().addSoftwareSystem("Team 2"); + b.setGroup("Organisation 1/Department 1/Team 2"); + + SoftwareSystem c = workspace.getModel().addSoftwareSystem("Organisation 1"); + c.setGroup("Organisation 1"); + + SoftwareSystem d = workspace.getModel().addSoftwareSystem("Organisation 2"); + d.setGroup("Organisation 2"); + + SoftwareSystem e = workspace.getModel().addSoftwareSystem("Department 1"); + e.setGroup("Organisation 1/Department 1"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description"); + view.addAllElements(); + + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/nested-groups.mmd")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderContainerDiagramWithExternalContainers() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + + container1.uses(container2, "Uses"); + + ContainerView containerView = workspace.getViews().createContainerView(softwareSystem1, "Containers", ""); + containerView.add(container1); + containerView.add(container2); + + Diagram diagram = new MermaidDiagramExporter().export(containerView); + assertEquals("graph TB\n" + + " linkStyle default fill:#ffffff\n" + + "\n" + + " subgraph diagram [\"Software System 1 - Containers\"]\n" + + " style diagram fill:#ffffff,stroke:#ffffff\n" + + "\n" + + " subgraph 1 [Software System 1]\n" + + " style 1 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + + "\n" + + " 2[\"<div style='font-weight: bold'>Container 1</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>\"]\n" + + " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " subgraph 3 [Software System 2]\n" + + " style 3 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + + "\n" + + " 4[\"<div style='font-weight: bold'>Container 2</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>\"]\n" + + " style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " 2-. \"<div>Uses</div><div style='font-size: 70%'></div>\" .->4\n" + + " end", diagram.getDefinition()); + } + + @Test + public void test_renderComponentDiagramWithExternalComponents() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component2 = container2.addComponent("Component 2"); + + component1.uses(component2, "Uses"); + + ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); + componentView.add(component1); + componentView.add(component2); + + Diagram diagram = new MermaidDiagramExporter().export(componentView); + assertEquals("graph TB\n" + + " linkStyle default fill:#ffffff\n" + + "\n" + + " subgraph diagram [\"Software System 1 - Container 1 - Components\"]\n" + + " style diagram fill:#ffffff,stroke:#ffffff\n" + + "\n" + + " subgraph 2 [Container 1]\n" + + " style 2 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + + "\n" + + " 3[\"<div style='font-weight: bold'>Component 1</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>\"]\n" + + " style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " subgraph 5 [Container 2]\n" + + " style 5 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + + "\n" + + " 6[\"<div style='font-weight: bold'>Component 2</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>\"]\n" + + " style 6 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " 3-. \"<div>Uses</div><div style='font-size: 70%'></div>\" .->6\n" + + " end", diagram.getDefinition()); + } + + @Test + public void test_renderGroupStyles() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User 1").setGroup("Group 1"); + workspace.getModel().addPerson("User 2").setGroup("Group 2"); + workspace.getModel().addPerson("User 3").setGroup("Group 3"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", ""); + view.addDefaultElements(); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 1").color("#111111"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 2").color("#222222"); + + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + Diagram diagram = exporter.export(view); + assertEquals("graph TB\n" + + " linkStyle default fill:#ffffff\n" + + "\n" + + " subgraph diagram [\"System Landscape\"]\n" + + " style diagram fill:#ffffff,stroke:#ffffff\n" + + "\n" + + " subgraph group1 [Group 1]\n" + + " style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5\n" + + "\n" + + " 1[\"<div style='font-weight: bold'>User 1</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>\"]\n" + + " style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " subgraph group2 [Group 2]\n" + + " style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5\n" + + "\n" + + " 2[\"<div style='font-weight: bold'>User 2</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>\"]\n" + + " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " subgraph group3 [Group 3]\n" + + " style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5\n" + + "\n" + + " 3[\"<div style='font-weight: bold'>User 3</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>\"]\n" + + " style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + "\n" + + " end", diagram.getDefinition()); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); + + diagram = exporter.export(view); + assertEquals("graph TB\n" + + " linkStyle default fill:#ffffff\n" + + "\n" + + " subgraph diagram [\"System Landscape\"]\n" + + " style diagram fill:#ffffff,stroke:#ffffff\n" + + "\n" + + " subgraph group1 [Group 1]\n" + + " style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5\n" + + "\n" + + " 1[\"<div style='font-weight: bold'>User 1</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>\"]\n" + + " style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " subgraph group2 [Group 2]\n" + + " style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5\n" + + "\n" + + " 2[\"<div style='font-weight: bold'>User 2</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>\"]\n" + + " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + " subgraph group3 [Group 3]\n" + + " style group3 fill:#ffffff,stroke:#aabbcc,color:#aabbcc,stroke-dasharray:5\n" + + "\n" + + " 3[\"<div style='font-weight: bold'>User 3</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>\"]\n" + + " style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " end\n" + + "\n" + + "\n" + + " end", diagram.getDefinition()); + } + + @Test + public void test_renderCustomView() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + + CustomElement a = model.addCustomElement("A"); + CustomElement b = model.addCustomElement("B", "Custom", "Description"); + a.uses(b, "Uses"); + + CustomView view = workspace.getViews().createCustomView("key", "Title", "Description"); + view.addDefaultElements(); + + Diagram diagram = new MermaidDiagramExporter().export(view); + assertEquals("graph TB\n" + + " linkStyle default fill:#ffffff\n" + + "\n" + + " subgraph diagram [\"Title\"]\n" + + " style diagram fill:#ffffff,stroke:#ffffff\n" + + "\n" + + " 1[\"<div style='font-weight: bold'>A</div><div style='font-size: 70%; margin-top: 0px'></div>\"]\n" + + " style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + " 2[\"<div style='font-weight: bold'>B</div><div style='font-size: 70%; margin-top: 0px'>[Custom]</div><div style='font-size: 80%; margin-top:10px'>Description</div>\"]\n" + + " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + + "\n" + + " 1-. \"<div>Uses</div><div style='font-size: 70%'></div>\" .->2\n" + + " end", diagram.getDefinition()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd new file mode 100644 index 000000000..79c389eca --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd @@ -0,0 +1,26 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["D - F - Components"] + style diagram fill:#ffffff,stroke:#ffffff + + 3["<div style='font-weight: bold'>C</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 + + subgraph 6 [F] + style 6 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a + + subgraph group1 [Group 4] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 8["<div style='font-weight: bold'>H</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>"] + style 8 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 7["<div style='font-weight: bold'>G</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>"] + style 7 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 3-. "<div></div><div style='font-size: 70%'></div>" .->7 + 3-. "<div></div><div style='font-size: 70%'></div>" .->8 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd new file mode 100644 index 000000000..3c5abba9d --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd @@ -0,0 +1,26 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["D - Containers"] + style diagram fill:#ffffff,stroke:#ffffff + + 3["<div style='font-weight: bold'>C</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 + + subgraph 4 [D] + style 4 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a + + subgraph group1 [Group 3] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 6["<div style='font-weight: bold'>F</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>"] + style 6 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 5["<div style='font-weight: bold'>E</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>"] + style 5 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 3-. "<div></div><div style='font-size: 70%'></div>" .->5 + 3-. "<div></div><div style='font-size: 70%'></div>" .->6 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd new file mode 100644 index 000000000..5ca147d80 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd @@ -0,0 +1,34 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph enterprise [Enterprise] + style enterprise fill:#ffffff,stroke:#444444,color:#444444 + + subgraph group1 [Group 2] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 4["<div style='font-weight: bold'>D</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 3["<div style='font-weight: bold'>C</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph group2 [Group 1] + style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 2["<div style='font-weight: bold'>B</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 1["<div style='font-weight: bold'>A</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 + + 2-. "<div></div><div style='font-size: 70%'></div>" .->3 + 3-. "<div></div><div style='font-size: 70%'></div>" .->4 + 1-. "<div></div><div style='font-size: 70%'></div>" .->2 + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd new file mode 100644 index 000000000..e48b4e3ec --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd @@ -0,0 +1,43 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 [Organisation 1] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 3["<div style='font-weight: bold'>Organisation 1</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 + subgraph group2 [Department 1] + style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 5["<div style='font-weight: bold'>Department 1</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 5 fill:#dddddd,stroke:#9a9a9a,color:#000000 + subgraph group3 [Team 1] + style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 1["<div style='font-weight: bold'>Team 1</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph group4 [Team 2] + style group4 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 2["<div style='font-weight: bold'>Team 2</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + end + + end + + subgraph group5 [Organisation 2] + style group5 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 4["<div style='font-weight: bold'>Organisation 2</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + + end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java new file mode 100644 index 000000000..8066396c2 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -0,0 +1,755 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.Workspace; +import com.structurizr.export.AbstractExporterTests; +import com.structurizr.export.Diagram; +import com.structurizr.model.*; +import com.structurizr.util.WorkspaceUtils; +import com.structurizr.view.*; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class C4PlantUMLDiagramExporterTests extends AbstractExporterTests { + + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "true"); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(7, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml")); + assertEquals(expected, diagram.getDefinition()); + + 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.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml")); + assertEquals(expected, diagram.getDefinition()); + + 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.PLANTUML_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 + public void test_AmazonWebServicesExampleWithoutTags() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + ThemeUtils.loadThemes(workspace); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); + workspace.getViews().getViews().forEach(v -> v.addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "false")); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_AmazonWebServicesExampleWithTags() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + ThemeUtils.loadThemes(workspace); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); + workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "true"); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_GroupsExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); + ThemeUtils.loadThemes(workspace); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(3, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_NestedGroupsExample() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("Team 1"); + a.setGroup("Organisation 1/Department 1/Team 1"); + + SoftwareSystem b = workspace.getModel().addSoftwareSystem("Team 2"); + b.setGroup("Organisation 1/Department 1/Team 2"); + + SoftwareSystem c = workspace.getModel().addSoftwareSystem("Organisation 1"); + c.setGroup("Organisation 1"); + + SoftwareSystem d = workspace.getModel().addSoftwareSystem("Organisation 2"); + d.setGroup("Organisation 2"); + + SoftwareSystem e = workspace.getModel().addSoftwareSystem("Department 1"); + e.setGroup("Organisation 1/Department 1"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description"); + view.addAllElements(); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_NestedGroupsExample_WithDotAsGroupSeparator() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addProperty("structurizr.groupSeparator", "."); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("Team 1"); + a.setGroup("Organisation 1.Department 1.Team 1"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description"); + view.addAllElements(); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderGroupStyles() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User 1").setGroup("Group 1"); + workspace.getModel().addPerson("User 2").setGroup("Group 2"); + workspace.getModel().addPerson("User 3").setGroup("Group 3"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", ""); + view.addDefaultElements(); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 1").color("#111111").icon("https://example.com/icon1.png"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 2").color("#222222").icon("https://example.com/icon2.png"); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter() { + @Override + protected double calculateIconScale(String iconUrl) { + return 1.0; + } + }; + + Diagram diagram = exporter.export(view); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml")); + assertEquals(expected, diagram.getDefinition()); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); + + diagram = exporter.export(view); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderContainerDiagramWithExternalContainers() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + + container1.uses(container2, "Uses"); + + ContainerView containerView = workspace.getViews().createContainerView(softwareSystem1, "Containers", ""); + containerView.add(container1); + containerView.add(container2); + + Diagram diagram = new C4PlantUMLExporter().export(containerView); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Software System 1 - Containers\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "!include <C4/C4_Container>\n" + + "\n" + + "System_Boundary(\"SoftwareSystem1_boundary\", \"Software System 1\", $tags=\"\") {\n" + + " Container(SoftwareSystem1.Container1, \"Container 1\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "System_Boundary(\"SoftwareSystem2_boundary\", \"Software System 2\", $tags=\"\") {\n" + + " Container(SoftwareSystem2.Container2, \"Container 2\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "Rel(SoftwareSystem1.Container1, SoftwareSystem2.Container2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderComponentDiagramWithExternalComponents() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component2 = container2.addComponent("Component 2"); + + component1.uses(component2, "Uses"); + + ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); + componentView.add(component1); + componentView.add(component2); + + Diagram diagram = new C4PlantUMLExporter().export(componentView); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Software System 1 - Container 1 - Components\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "!include <C4/C4_Component>\n" + + "\n" + + "Container_Boundary(\"SoftwareSystem1.Container1_boundary\", \"Container 1\", $tags=\"\") {\n" + + " Component(SoftwareSystem1.Container1.Component1, \"Component 1\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "Container_Boundary(\"SoftwareSystem2.Container2_boundary\", \"Container 2\", $tags=\"\") {\n" + + " Component(SoftwareSystem2.Container2.Component2, \"Component 2\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "Rel(SoftwareSystem1.Container1.Component1, SoftwareSystem2.Container2.Component2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderDiagramWithElementUrls() { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + softwareSystem.setUrl("https://structurizr.com"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "System(SoftwareSystem, \"Software System\", $descr=\"\", $tags=\"\", $link=\"https://structurizr.com\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderDiagramWithIncludes() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Software System"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + view.addProperty(C4PlantUMLExporter.PLANTUML_INCLUDES_PROPERTY, "styles.puml"); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "!include styles.puml\n" + + "\n" + + "System(SoftwareSystem, \"Software System\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderDiagramWithNewLineCharacterInElementName() { + Workspace workspace = new Workspace("Name", "Description"); + + workspace.getModel().addSoftwareSystem("Software\nSystem"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "System(SoftwareSystem, \"Software\\nSystem\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderInfrastructureNodeWithTechnology() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment node"); + deploymentNode.addInfrastructureNode("Infrastructure node", "description", "technology"); + + DeploymentView view = workspace.getViews().createDeploymentView("key", "view description"); + view.addDefaultElements(); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Deployment - Default\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "!include <C4/C4_Deployment>\n" + + "\n" + + "Deployment_Node(Default.Deploymentnode, \"Deployment node\", $type=\"\", $descr=\"\", $tags=\"\", $link=\"\") {\n" + + " Deployment_Node(Default.Deploymentnode.Infrastructurenode, \"Infrastructure node\", $type=\"technology\", $descr=\"description\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_printProperties() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("SoftwareSystem"); + Container container1 = softwareSystem.addContainer("Container 1"); + container1.addProperty("structurizr.dsl.identifier", "container1"); + container1.addProperty("IP", "127.0.0.1"); + container1.addProperty("Region", "East"); + Container container2 = softwareSystem.addContainer("Container 2"); + container1.addProperty("structurizr.dsl.identifier", "container2"); + container2.addProperty("Region", "West"); + container2.addProperty("IP", "127.0.0.2"); + Relationship relationship = container1.uses(container2, ""); + relationship.addProperty("Prop1", "Value1"); + relationship.addProperty("Prop2", "Value2"); + + workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_ELEMENT_PROPERTIES_PROPERTY, Boolean.TRUE.toString()); + workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_RELATIONSHIP_PROPERTIES_PROPERTY, Boolean.TRUE.toString()); + ContainerView view = workspace.getViews().createContainerView(softwareSystem, "containerView", ""); + view.addDefaultElements(); + + Diagram diagram = new C4PlantUMLExporter().export(view); + + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_deploymentViewPrintProperties() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment node"); + deploymentNode.addProperty("Prop1", "Value1"); + + InfrastructureNode infraNode = deploymentNode.addInfrastructureNode("Infrastructure node", "description", "technology"); + infraNode.addProperty("Prop2", "Value2"); + + workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_ELEMENT_PROPERTIES_PROPERTY, Boolean.TRUE.toString()); + DeploymentView deploymentView = workspace.getViews().createDeploymentView("deploymentView", ""); + deploymentView.addDefaultElements(); + + Diagram diagram = new C4PlantUMLExporter().export(deploymentView); + + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_legendAndStereotypes() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Name"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + // legend (true) and stereotypes (false) + view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "true"); + view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "false"); + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + + // legend (true) and stereotypes (true) + view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "true"); + view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "true"); + diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(false)\n" + + "@enduml", diagram.getDefinition()); + + // legend (false) and stereotypes (false) + view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "false"); + view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "false"); + diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "hide stereotypes\n" + + "@enduml", diagram.getDefinition()); + + // legend (false) and stereotypes (true) + view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "false"); + view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "true"); + diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "show stereotypes\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderContainerShapes() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Container container1 = softwareSystem.addContainer("Default Container"); + Container container2 = softwareSystem.addContainer("Cylinder Container"); + Container container3 = softwareSystem.addContainer("Pipe Container"); + Container container4 = softwareSystem.addContainer("Robot Container"); + container2.addTags("Cylinder"); + container3.addTags("Pipe"); + container4.addTags("Robot"); // Just an example of a shape that has no C4-PlantUML mapping. + + ContainerView containerView = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); + containerView.add(container1); + containerView.add(container2); + containerView.add(container3); + containerView.add(container4); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Cylinder").shape(Shape.Cylinder); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Pipe").shape(Shape.Pipe); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Robot").shape(Shape.Robot); + + Diagram diagram = new C4PlantUMLExporter().export(containerView); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Software System - Containers\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "!include <C4/C4_Container>\n" + + "\n" + + "System_Boundary(\"SoftwareSystem_boundary\", \"Software System\", $tags=\"\") {\n" + + " Container(SoftwareSystem.DefaultContainer, \"Default Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + " ContainerDb(SoftwareSystem.CylinderContainer, \"Cylinder Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + " ContainerQueue(SoftwareSystem.PipeContainer, \"Pipe Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + " Container(SoftwareSystem.RobotContainer, \"Robot Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderComponentShapes() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + + Component component1 = container.addComponent("Default Component"); + Component component2 = container.addComponent("Cylinder Component"); + Component component3 = container.addComponent("Pipe Component"); + Component component4 = container.addComponent("Robot Component"); + + component2.addTags("Cylinder"); + component3.addTags("Pipe"); + component4.addTags("Robot"); // Just an example of a shape that has no C4-PlantUML mapping. + + ContainerView containerView = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); + ComponentView componentView = workspace.getViews().createComponentView(container, "Components", ""); + componentView.add(component1); + componentView.add(component2); + componentView.add(component3); + componentView.add(component4); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Cylinder").shape(Shape.Cylinder); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Pipe").shape(Shape.Pipe); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Robot").shape(Shape.Robot); + + Diagram diagram = new C4PlantUMLExporter().export(componentView); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Software System - Container - Components\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "!include <C4/C4_Component>\n" + + "\n" + + "Container_Boundary(\"SoftwareSystem.Container_boundary\", \"Container\", $tags=\"\") {\n" + + " Component(SoftwareSystem.Container.DefaultComponent, \"Default Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + " ComponentDb(SoftwareSystem.Container.CylinderComponent, \"Cylinder Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + " ComponentQueue(SoftwareSystem.Container.PipeComponent, \"Pipe Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + " Component(SoftwareSystem.Container.RobotComponent, \"Robot Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void testFont() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + workspace.getViews().getConfiguration().getBranding().setFont(new Font("Courier")); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "skinparam {\n" + + " defaultFontName \"Courier\"\n" + + "}\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "Person(User, \"User\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition().toString()); + + } + + @Test + public void stdlib_false() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + view.addProperty(C4PlantUMLExporter.C4PLANTUML_STANDARD_LIBRARY_PROPERTY, "false"); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml\n" + + "!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml\n" + + "\n" + + "Person(User, \"User\", $descr=\"\", $tags=\"\", $link=\"\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition().toString()); + + } + + @Test + public void componentWithoutTechnology() { + Workspace workspace = new Workspace("Name", "Description"); + Container container = workspace.getModel().addSoftwareSystem("Name").addContainer("Name"); + container.addComponent("Name").setDescription("Description"); + + ComponentView view = workspace.getViews().createComponentView(container, "key", "Description"); + view.addAllElements(); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Name - Name - Components\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "!include <C4/C4_Component>\n" + + "\n" + + "Container_Boundary(\"Name.Name_boundary\", \"Name\", $tags=\"\") {\n" + + " Component(Name.Name.Name, \"Name\", $techn=\"\", $descr=\"Description\", $tags=\"\", $link=\"\")\n" + + "}\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void borderStyling() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Name"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "true"); + workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.ELEMENT).stroke("green").border(Border.Dashed).strokeWidth(2); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals("@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "!include <C4/C4>\n" + + "!include <C4/C4_Context>\n" + + "\n" + + "AddElementTag(\"Element\", $bgColor=\"#dddddd\", $borderColor=\"#008000\", $fontColor=\"#000000\", $sprite=\"\", $shadowing=\"\", $borderStyle=\"Dashed\", $borderThickness=\"2\")\n" + + "\n" + + "System(Name, \"Name\", $descr=\"\", $tags=\"Element\", $link=\"\")\n" + + "\n" + + "\n" + + "SHOW_LEGEND(true)\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void elementWithUrl() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Name").setUrl("https://example.com"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + + Diagram diagram = new C4PlantUMLExporter().export(view); + assertEquals(""" + @startuml + set separator none + title System Landscape + + top to bottom direction + + !include <C4/C4> + !include <C4/C4_Context> + + System(Name, "Name", $descr="", $tags="", $link="https://example.com") + + + SHOW_LEGEND(true) + @enduml""", diagram.getDefinition()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java new file mode 100644 index 000000000..0acf637cb --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -0,0 +1,1031 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.Workspace; +import com.structurizr.export.AbstractExporterTests; +import com.structurizr.export.Diagram; +import com.structurizr.model.*; +import com.structurizr.util.WorkspaceUtils; +import com.structurizr.view.*; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +public class StructurizrPlantUMLDiagramExporterTests extends AbstractExporterTests { + + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + workspace.getViews().getConfiguration().addProperty(StructurizrPlantUMLExporter.PLANTUML_ANIMATION_PROPERTY, "true"); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(7, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml")); + assertEquals(expected, diagram.getDefinition()); + assertEquals(3, diagram.getFrames().size()); + + //assertEquals("", diagram.getLegend().getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml")); + assertEquals(expected, diagram.getDefinition()); + assertEquals(4, diagram.getFrames().size()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml")); + assertEquals(expected, diagram.getDefinition()); + assertEquals(6, diagram.getFrames().size()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml")); + assertEquals(expected, diagram.getDefinition()); + assertEquals(4, diagram.getFrames().size()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml")); + assertEquals(expected, diagram.getDefinition()); + assertEquals(6, diagram.getFrames().size()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml")); + assertEquals(expected, diagram.getDefinition()); + assertEquals(4, diagram.getFrames().size()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml")); + assertEquals(expected, diagram.getDefinition()); + assertEquals(6, diagram.getFrames().size()); + + // and the sequence diagram version + workspace.getViews().getConfiguration().addProperty(exporter.PLANTUML_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/structurizr/36141-SignIn-sequence.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_AmazonWebServicesExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + ThemeUtils.loadThemes(workspace); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml")); + assertEquals(expected, diagram.getDefinition()); + + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml")); + assertEquals(expected, diagram.getLegend().getDefinition()); + } + + @Test + public void test_GroupsExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); + ThemeUtils.loadThemes(workspace); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(3, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml")); + assertEquals(expected, diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_NestedGroupsExample() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("Team 1"); + a.setGroup("Organisation 1/Department 1/Team 1"); + + SoftwareSystem b = workspace.getModel().addSoftwareSystem("Team 2"); + b.setGroup("Organisation 1/Department 1/Team 2"); + + SoftwareSystem c = workspace.getModel().addSoftwareSystem("Organisation 1"); + c.setGroup("Organisation 1"); + + SoftwareSystem d = workspace.getModel().addSoftwareSystem("Organisation 2"); + d.setGroup("Organisation 2"); + + SoftwareSystem e = workspace.getModel().addSoftwareSystem("Department 1"); + e.setGroup("Organisation 1/Department 1"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description"); + view.addAllElements(); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Organisation 1/Department 1/Team 1").color("#ff0000"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Organisation 1/Department 1/Team 2").color("#0000ff"); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + + Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderGroupStyles() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User 1").setGroup("Group 1"); + workspace.getModel().addPerson("User 2").setGroup("Group 2"); + workspace.getModel().addPerson("User 3").setGroup("Group 3"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", ""); + view.addDefaultElements(); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 1").color("#111111").icon("https://example.com/icon1.png"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 2").color("#222222").icon("https://example.com/icon2.png"); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter() { + @Override + protected double calculateIconScale(String iconUrl) { + return 1.0; + } + }; + + Diagram diagram = exporter.export(view); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml")); + assertEquals(expected, diagram.getDefinition()); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); + + diagram = exporter.export(view); + expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderContainerDiagramWithExternalContainers() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + + container1.uses(container2, "Uses"); + + ContainerView containerView = workspace.getViews().createContainerView(softwareSystem1, "Containers", ""); + containerView.add(container1); + containerView.add(container2); + + Diagram diagram = new StructurizrPlantUMLExporter().export(containerView); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Software System 1 - Containers\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "skinparam {\n" + + " arrowFontSize 10\n" + + " defaultTextAlignment center\n" + + " wrapWidth 200\n" + + " maxMessageSize 100\n" + + "}\n" + + "\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<SoftwareSystem1.Container1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem2.Container2>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem1>> {\n" + + " BorderColor #9a9a9a\n" + + " FontColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem2>> {\n" + + " BorderColor #9a9a9a\n" + + " FontColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "\n" + + "rectangle \"Software System 1\\n<size:10>[Software System]</size>\" <<SoftwareSystem1>> {\n" + + " rectangle \"==Container 1\\n<size:10>[Container]</size>\" <<SoftwareSystem1.Container1>> as SoftwareSystem1.Container1\n" + + "}\n" + + "\n" + + "rectangle \"Software System 2\\n<size:10>[Software System]</size>\" <<SoftwareSystem2>> {\n" + + " rectangle \"==Container 2\\n<size:10>[Container]</size>\" <<SoftwareSystem2.Container2>> as SoftwareSystem2.Container2\n" + + "}\n" + + "\n" + + "SoftwareSystem1.Container1 .[#707070,thickness=2].> SoftwareSystem2.Container2 : \"<color:#707070>Uses\"\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderComponentDiagramWithExternalComponents() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + Component component2 = container1.addComponent("Component 2"); + + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component3 = container2.addComponent("Component 3"); + + component1.uses(component2, "Uses"); + component2.uses(component3, "Uses"); + + ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); + componentView.add(component1); + componentView.add(component2); + componentView.add(component3); + + Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderComponentDiagramWithExternalComponentsAndSoftwareSystemBoundariesIncluded() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + Component component2 = container1.addComponent("Component 2"); + + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component3 = container2.addComponent("Component 3"); + + component1.uses(component2, "Uses"); + component2.uses(component3, "Uses"); + + ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); + componentView.add(component1); + componentView.add(component2); + componentView.add(component3); + componentView.addProperty("structurizr.softwareSystemBoundaries", "true"); + + Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderDynamicDiagramWithExternalContainers() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + + container1.uses(container2, "Uses"); + + DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystem1, "Dynamic", ""); + dynamicView.add(container1, container2); + + Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); + assertEquals("@startuml\n" + + "set separator none\n" + + "title Software System 1 - Dynamic\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "skinparam {\n" + + " arrowFontSize 10\n" + + " defaultTextAlignment center\n" + + " wrapWidth 200\n" + + " maxMessageSize 100\n" + + "}\n" + + "\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<SoftwareSystem1.Container1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem2.Container2>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem1>> {\n" + + " BorderColor #9a9a9a\n" + + " FontColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem2>> {\n" + + " BorderColor #9a9a9a\n" + + " FontColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "\n" + + "rectangle \"Software System 1\\n<size:10>[Software System]</size>\" <<SoftwareSystem1>> {\n" + + " rectangle \"==Container 1\\n<size:10>[Container]</size>\" <<SoftwareSystem1.Container1>> as SoftwareSystem1.Container1\n" + + "}\n" + + "\n" + + "rectangle \"Software System 2\\n<size:10>[Software System]</size>\" <<SoftwareSystem2>> {\n" + + " rectangle \"==Container 2\\n<size:10>[Container]</size>\" <<SoftwareSystem2.Container2>> as SoftwareSystem2.Container2\n" + + "}\n" + + "\n" + + "SoftwareSystem1.Container1 .[#707070,thickness=2].> SoftwareSystem2.Container2 : \"<color:#707070>1. Uses\"\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + public void test_renderDynamicDiagramWithExternalComponents() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + Component component2 = container1.addComponent("Component 2"); + + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component3 = container2.addComponent("Component 3"); + + component1.uses(component2, "Uses"); + component2.uses(component3, "Uses"); + + DynamicView dynamicView = workspace.getViews().createDynamicView(container1, "Dynamic", ""); + dynamicView.add(component1, component2); + dynamicView.add(component2, component3); + + Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderDynamicDiagramWithExternalComponentsAndSoftwareSystemBoundariesIncluded() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + Component component2 = container1.addComponent("Component 2"); + + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + Component component3 = container2.addComponent("Component 3"); + + component1.uses(component2, "Uses"); + component2.uses(component3, "Uses"); + + DynamicView dynamicView = workspace.getViews().createDynamicView(container1, "Dynamic", ""); + dynamicView.add(component1, component2); + dynamicView.add(component2, component3); + dynamicView.addProperty("structurizr.softwareSystemBoundaries", "true"); + + Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_renderDiagramWithElementUrls() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + softwareSystem.setUrl("https://structurizr.com"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertTrue(diagram.getDefinition().contains("rectangle \"==Software System\\n<size:10>[Software System]</size>\" <<SoftwareSystem>> as SoftwareSystem [[https://structurizr.com]]\n")); + } + + @Test + public void test_renderDiagramWithIncludes() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Software System"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + view.getViewSet().getConfiguration().addProperty(StructurizrPlantUMLExporter.PLANTUML_INCLUDES_PROPERTY, "styles.puml"); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertTrue(diagram.getDefinition().contains("!include styles.puml\n")); + } + + @Test + public void test_renderDiagramWithNewLineCharacterInElementName() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Software\nSystem"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertTrue(diagram.getDefinition().contains("rectangle \"==Software\\nSystem\\n<size:10>[Software System]</size>\" <<SoftwareSystem>> as SoftwareSystem")); + } + + @Test + public void test_renderCustomView() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + + CustomElement a = model.addCustomElement("A"); + CustomElement b = model.addCustomElement("B", "Custom", "Description"); + a.uses(b, "Uses"); + + CustomView view = workspace.getViews().createCustomView("key", "Title", "Description"); + view.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertEquals("@startuml\nset separator none\n" + + "title Title\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "skinparam {\n" + + " arrowFontSize 10\n" + + " defaultTextAlignment center\n" + + " wrapWidth 200\n" + + " maxMessageSize 100\n" + + "}\n" + + "\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<2>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "\n" + + "rectangle \"==A\" <<1>> as 1\n" + + "rectangle \"==B\\n<size:10>[Custom]</size>\\n\\nDescription\" <<2>> as 2\n" + + "\n" + + "1 .[#707070,thickness=2].> 2 : \"<color:#707070>Uses\"\n" + + "@enduml", diagram.getDefinition()); + } + + @Test + void renderWorkspaceWithUnicodeElementName() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("Пользователь"); + workspace.getViews().createSystemLandscapeView("key", "Description").addDefaultElements(); + + String diagramDefinition = new StructurizrPlantUMLExporter().export(workspace).stream().findFirst().get().getDefinition(); + + assertTrue(diagramDefinition.contains("skinparam rectangle<<Пользователь>> {")); + assertTrue(diagramDefinition.contains("rectangle \"==Пользователь\\n<size:10>[Person]</size>\" <<Пользователь>> as Пользователь")); + } + + @Test + public void testLegend() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + + CustomElement a = model.addCustomElement("A"); + a.addTags("Tag 1"); + CustomElement b = model.addCustomElement("B"); + b.addTags("Tag 2"); + a.uses(b, "...").addTags("Tag 3"); + b.uses(a, "...").addTags("Tag 4"); + + CustomView view = workspace.getViews().createCustomView("key", "Title", "Description"); + view.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertEquals("@startuml\nset separator none\n" + + "\n" + + "skinparam {\n" + + " shadowing false\n" + + " arrowFontSize 15\n" + + " defaultTextAlignment center\n" + + " wrapWidth 100\n" + + " maxMessageSize 100\n" + + "}\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<_transparent>> {\n" + + " BorderColor transparent\n" + + " BackgroundColor transparent\n" + + " FontColor transparent\n" + + "}\n" + + "\n" + + "skinparam rectangle<<1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + "}\n" + + "rectangle \"==Element\" <<1>>\n" + + "\n" + + "rectangle \".\" <<_transparent>> as 2\n" + + "2 .[#707070,thickness=2].> 2 : \"<color:#707070>Relationship\"\n" + + "\n" + + "\n" + + "@enduml", diagram.getLegend().getDefinition()); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag 1").background("#ff0000").color("#ffffff").shape(Shape.RoundedBox); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag 2").background("#00ff00").color("#ffffff").shape(Shape.Hexagon); + workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag 3").color("#0000ff"); + workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag 4").color("#ff00ff").thickness(3).style(LineStyle.Solid); + + diagram = new StructurizrPlantUMLExporter().export(view); + assertEquals("@startuml\nset separator none\n" + + "\n" + + "skinparam {\n" + + " shadowing false\n" + + " arrowFontSize 15\n" + + " defaultTextAlignment center\n" + + " wrapWidth 100\n" + + " maxMessageSize 100\n" + + "}\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<_transparent>> {\n" + + " BorderColor transparent\n" + + " BackgroundColor transparent\n" + + " FontColor transparent\n" + + "}\n" + + "\n" + + "skinparam rectangle<<1>> {\n" + + " BackgroundColor #ff0000\n" + + " FontColor #ffffff\n" + + " BorderColor #b20000\n" + + " roundCorner 20\n" + + "}\n" + + "rectangle \"==Tag 1\" <<1>>\n" + + "\n" + + "skinparam hexagon<<2>> {\n" + + " BackgroundColor #00ff00\n" + + " FontColor #ffffff\n" + + " BorderColor #00b200\n" + + "}\n" + + "hexagon \"==Tag 2\" <<2>>\n" + + "\n" + + "rectangle \".\" <<_transparent>> as 3\n" + + "3 .[#0000ff,thickness=2].> 3 : \"<color:#0000ff>Tag 3\"\n" + + "\n" + + "rectangle \".\" <<_transparent>> as 4\n" + + "4 -[#ff00ff,thickness=3]-> 4 : \"<color:#ff00ff>Tag 4\"\n" + + "\n" + + "\n" + + "@enduml", diagram.getLegend().getDefinition()); + } + + @Test + public void staticDiagramsAreUnchangedWhenSequenceDiagramsAreEnabled() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + + model.addSoftwareSystem("Software System").setGroup("Group"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram; + String expected = "@startuml\n" + + "set separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "skinparam {\n" + + " arrowFontSize 10\n" + + " defaultTextAlignment center\n" + + " wrapWidth 200\n" + + " maxMessageSize 100\n" + + "}\n" + + "\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<SoftwareSystem>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "\n" + + "rectangle \"Group\" <<group1>> {\n" + + " skinparam RectangleBorderColor<<group1>> #cccccc\n" + + " skinparam RectangleFontColor<<group1>> #cccccc\n" + + " skinparam RectangleBorderStyle<<group1>> dashed\n" + + "\n" + + " rectangle \"==Software System\\n<size:10>[Software System]</size>\" <<SoftwareSystem>> as SoftwareSystem\n" + + "}\n" + + "\n" + + "\n" + + "@enduml"; + + diagram = exporter.export(view); + assertEquals(expected, diagram.getDefinition()); + + workspace.getViews().getConfiguration().addProperty(StructurizrPlantUMLExporter.PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "true"); + + diagram = exporter.export(view); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void testFont() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + workspace.getViews().getConfiguration().getBranding().setFont(new Font("Courier")); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertEquals("@startuml\nset separator none\n" + + "title System Landscape\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "skinparam {\n" + + " arrowFontSize 10\n" + + " defaultTextAlignment center\n" + + " wrapWidth 200\n" + + " maxMessageSize 100\n" + + " defaultFontName \"Courier\"\n" + + "}\n" + + "\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<User>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "\n" + + "rectangle \"==User\\n<size:10>[Person]</size>\" <<User>> as User\n" + + "\n" + + "@enduml", diagram.getDefinition().toString()); + + assertEquals("@startuml\nset separator none\n" + + "\n" + + "skinparam {\n" + + " shadowing false\n" + + " arrowFontSize 15\n" + + " defaultTextAlignment center\n" + + " wrapWidth 100\n" + + " maxMessageSize 100\n" + + " defaultFontName \"Courier\"\n" + + "}\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<_transparent>> {\n" + + " BorderColor transparent\n" + + " BackgroundColor transparent\n" + + " FontColor transparent\n" + + "}\n" + + "\n" + + "skinparam rectangle<<1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + "}\n" + + "rectangle \"==Element\" <<1>>\n" + + "\n" + + "\n" + + "@enduml", diagram.getLegend().getDefinition()); + } + + @Test + public void dynamicView_UnscopedWithGroups() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystemA = workspace.getModel().addSoftwareSystem("A"); + softwareSystemA.setGroup("Group 1"); + SoftwareSystem softwareSystemB = workspace.getModel().addSoftwareSystem("B"); + softwareSystemB.setGroup("Group 2"); + softwareSystemA.uses(softwareSystemB, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(softwareSystemA, softwareSystemB); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void dynamicView_SoftwareSystemScopedWithGroups() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystemA = workspace.getModel().addSoftwareSystem("A"); + Container containerA = softwareSystemA.addContainer("A"); + containerA.setGroup("Group 1"); + SoftwareSystem softwareSystemB = workspace.getModel().addSoftwareSystem("B"); + Container containerB = softwareSystemB.addContainer("B"); + containerB.setGroup("Group 2"); + containerA.uses(containerB, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + view.add(containerA, containerB); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void dynamicView_ContainerScopedWithGroups() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystemA = workspace.getModel().addSoftwareSystem("A"); + Container containerA = softwareSystemA.addContainer("A"); + Component componentA = containerA.addComponent("A"); + componentA.setGroup("Group 1"); + SoftwareSystem softwareSystemB = workspace.getModel().addSoftwareSystem("B"); + Container containerB = softwareSystemB.addContainer("B"); + Component componentB = containerB.addComponent("B"); + componentB.setGroup("Group 2"); + componentA.uses(componentB, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView(containerA, "key", "Description"); + view.add(componentA, componentB); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSeparator() { + Workspace workspace = new Workspace("Name", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + Container container1 = softwareSystem.addContainer("Container 1"); + container1.setGroup("Group 1"); + Container container2 = softwareSystem.addContainer("Container 2"); + container2.setGroup("Group 2"); + + ContainerView view = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); + view.addAllElements(); + + String expectedResult = "@startuml\n" + + "set separator none\n" + + "title Software System - Containers\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "skinparam {\n" + + " arrowFontSize 10\n" + + " defaultTextAlignment center\n" + + " wrapWidth 200\n" + + " maxMessageSize 100\n" + + "}\n" + + "\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<SoftwareSystem.Container1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem.Container2>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<SoftwareSystem>> {\n" + + " BorderColor #9a9a9a\n" + + " FontColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "\n" + + "rectangle \"Software System\\n<size:10>[Software System]</size>\" <<SoftwareSystem>> {\n" + + " rectangle \"Group 1\" <<group1>> {\n" + + " skinparam RectangleBorderColor<<group1>> #cccccc\n" + + " skinparam RectangleFontColor<<group1>> #cccccc\n" + + " skinparam RectangleBorderStyle<<group1>> dashed\n" + + "\n" + + " rectangle \"==Container 1\\n<size:10>[Container]</size>\" <<SoftwareSystem.Container1>> as SoftwareSystem.Container1\n" + + " }\n" + + "\n" + + " rectangle \"Group 2\" <<group2>> {\n" + + " skinparam RectangleBorderColor<<group2>> #cccccc\n" + + " skinparam RectangleFontColor<<group2>> #cccccc\n" + + " skinparam RectangleBorderStyle<<group2>> dashed\n" + + "\n" + + " rectangle \"==Container 2\\n<size:10>[Container]</size>\" <<SoftwareSystem.Container2>> as SoftwareSystem.Container2\n" + + " }\n" + + "\n" + + "}\n" + + "\n" + + "@enduml"; + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(expectedResult, diagram.getDefinition()); + + // this should be the same + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + exporter = new StructurizrPlantUMLExporter(); + diagram = exporter.export(view); + assertEquals(expectedResult, diagram.getDefinition()); + } + + @Test + public void deploymentView_WithGroups() { + Workspace workspace = new Workspace("Name", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + DeploymentNode server1 = workspace.getModel().addDeploymentNode("Server 1"); + server1.setGroup("Group 1"); + + InfrastructureNode infrastructureNode1 = server1.addInfrastructureNode("Infrastructure Node 1"); + InfrastructureNode infrastructureNode2 = server1.addInfrastructureNode("Infrastructure Node 2"); + + SoftwareSystemInstance softwareSystemInstance = server1.add(softwareSystem); + softwareSystemInstance.setGroup("Group 2"); + infrastructureNode2.setGroup("Group 2"); + + DeploymentView view = workspace.getViews().createDeploymentView("key", "Description"); + view.add(infrastructureNode1); + view.add(infrastructureNode2); + view.add(softwareSystemInstance); + + String expectedResult = "@startuml\n" + + "set separator none\n" + + "title Deployment - Default\n" + + "\n" + + "top to bottom direction\n" + + "\n" + + "skinparam {\n" + + " arrowFontSize 10\n" + + " defaultTextAlignment center\n" + + " wrapWidth 200\n" + + " maxMessageSize 100\n" + + "}\n" + + "\n" + + "hide stereotype\n" + + "\n" + + "skinparam rectangle<<Default.Server1.InfrastructureNode1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<Default.Server1.InfrastructureNode2>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<Default.Server1>> {\n" + + " BackgroundColor #ffffff\n" + + " FontColor #000000\n" + + " BorderColor #888888\n" + + " shadowing false\n" + + "}\n" + + "skinparam rectangle<<Default.Server1.SoftwareSystem_1>> {\n" + + " BackgroundColor #dddddd\n" + + " FontColor #000000\n" + + " BorderColor #9a9a9a\n" + + " shadowing false\n" + + "}\n" + + "\n" + + "rectangle \"Group 1\" <<group1>> {\n" + + " skinparam RectangleBorderColor<<group1>> #cccccc\n" + + " skinparam RectangleFontColor<<group1>> #cccccc\n" + + " skinparam RectangleBorderStyle<<group1>> dashed\n" + + "\n" + + " rectangle \"Server 1\\n<size:10>[Deployment Node]</size>\" <<Default.Server1>> as Default.Server1 {\n" + + " rectangle \"Group 2\" <<group2>> {\n" + + " skinparam RectangleBorderColor<<group2>> #cccccc\n" + + " skinparam RectangleFontColor<<group2>> #cccccc\n" + + " skinparam RectangleBorderStyle<<group2>> dashed\n" + + "\n" + + " rectangle \"==Infrastructure Node 2\\n<size:10>[Infrastructure Node]</size>\" <<Default.Server1.InfrastructureNode2>> as Default.Server1.InfrastructureNode2\n" + + " rectangle \"==Software System\\n<size:10>[Software System]</size>\" <<Default.Server1.SoftwareSystem_1>> as Default.Server1.SoftwareSystem_1\n" + + " }\n" + + "\n" + + " rectangle \"==Infrastructure Node 1\\n<size:10>[Infrastructure Node]</size>\" <<Default.Server1.InfrastructureNode1>> as Default.Server1.InfrastructureNode1\n" + + " }\n" + + "\n" + + "}\n" + + "\n" + + "@enduml"; + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(expectedResult, diagram.getDefinition()); + + // this should be the same + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + exporter = new StructurizrPlantUMLExporter(); + diagram = exporter.export(view); + assertEquals(expectedResult, diagram.getDefinition()); + } + + @Test + public void test_ElementInstanceUrl() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + a.setUrl("https://example.com/url1"); + SoftwareSystemInstance aInstance = workspace.getModel().addDeploymentNode("Node").add(a); + + DeploymentView view = workspace.getViews().createDeploymentView("deployment", "Default"); + view.add(aInstance); + + assertEquals(""" +@startuml +set separator none +title Deployment - Default + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<Default.Node.A_1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<Default.Node>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} + +rectangle "Node\\n<size:10>[Deployment Node]</size>" <<Default.Node>> as Default.Node { + rectangle "==A\\n<size:10>[Software System]</size>" <<Default.Node.A_1>> as Default.Node.A_1 [[https://example.com/url1]] +} + +@enduml""", new StructurizrPlantUMLExporter().export(view).getDefinition()); + + aInstance.setUrl("https://example.com/url2"); + assertEquals(""" +@startuml +set separator none +title Deployment - Default + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<Default.Node.A_1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<Default.Node>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} + +rectangle "Node\\n<size:10>[Deployment Node]</size>" <<Default.Node>> as Default.Node { + rectangle "==A\\n<size:10>[Software System]</size>" <<Default.Node.A_1>> as Default.Node.A_1 [[https://example.com/url2]] +} + +@enduml""", new StructurizrPlantUMLExporter().export(view).getDefinition()); } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml new file mode 100644 index 000000000..47509b6c7 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml @@ -0,0 +1,52 @@ +@startuml +set separator none +title Internet Banking System - API Application - Components + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> +!include <C4/C4_Component> + +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") +Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") +System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") +Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") +ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + +Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { + Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.AccountsSummaryController, "Accounts Summary Controller", $techn="Spring MVC Rest Controller", $descr="Provides customers with a summary of their bank accounts.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.ResetPasswordController, "Reset Password Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to reset their passwords with a single use URL.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, "Mainframe Banking System Facade", $techn="Spring Bean", $descr="A facade onto the mainframe banking system.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.EmailComponent, "E-mail Component", $techn="Spring Bean", $descr="Sends e-mails to users.", $tags="Component", $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 \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml new file mode 100644 index 000000000..9badc190c --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml @@ -0,0 +1,46 @@ +@startuml +set separator none +title Internet Banking System - Containers + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> + +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Person", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +Person_Ext(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person", $link="") +System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") +System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + +System_Boundary("InternetBankingSystem_boundary", "Internet Banking System", $tags="Software System") { + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") + Container(InternetBankingSystem.WebApplication, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") + Container(InternetBankingSystem.APIApplication, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") + ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") +} + +Rel(PersonalBankingCustomer, InternetBankingSystem.WebApplication, "Visits bigbank.com/ib using", $techn="HTTPS", $tags="Relationship", $link="") +Rel(PersonalBankingCustomer, InternetBankingSystem.SinglePageApplication, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") +Rel(PersonalBankingCustomer, InternetBankingSystem.MobileApp, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem.WebApplication, InternetBankingSystem.SinglePageApplication, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication, InternetBankingSystem.Database, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication, EmailSystem, "Sends e-mail using", $techn="SMTP", $tags="Relationship", $link="") +Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml new file mode 100644 index 000000000..d3e9a8d69 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml @@ -0,0 +1,55 @@ +@startuml +set separator none +title Internet Banking System - Deployment - Development + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> +!include <C4/C4_Deployment> + +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +Deployment_Node(Development.DeveloperLaptop, "Developer Laptop", $type="Microsoft Windows 10 or Apple macOS", $descr="A developer laptop.", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer, "Docker Container - Web Server", $type="Docker", $descr="A Docker container.", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="An open source Java EE web server.", $tags="Element", $link="") { + Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") + Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") + } + + } + + Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer, "Docker Container - Database Server", $type="Docker", $descr="A Docker container.", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer, "Database Server", $type="Oracle 12c", $descr="A development database.", $tags="Element", $link="") { + ContainerDb(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + } + + } + + Deployment_Node(Development.DeveloperLaptop.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { + Container(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + } + +} + +Deployment_Node(Development.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.BigBankplc.bigbankdev001, "bigbank-dev001", $type="", $descr="", $tags="Element", $link="") { + System(Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + } + +} + +Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") +Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="") +Rel(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml new file mode 100644 index 000000000..6a33ec429 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml @@ -0,0 +1,79 @@ +@startuml +set separator none +title Internet Banking System - Deployment - Live + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> +!include <C4/C4_Deployment> + +AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database,Failover", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Failover", $textColor="#707070", $lineColor="#707070", $lineStyle = "") +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +Deployment_Node(Live.Customersmobiledevice, "Customer's mobile device", $type="Apple iOS or Android", $descr="", $tags="Element", $link="") { + Container(Live.Customersmobiledevice.MobileApp_1, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") +} + +Deployment_Node(Live.Customerscomputer, "Customer's computer", $type="Microsoft Windows or Apple macOS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.Customerscomputer.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { + Container(Live.Customerscomputer.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + } + +} + +Deployment_Node(Live.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankprod001, "bigbank-prod001", $type="", $descr="", $tags="Element", $link="") { + System(Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + } + + Deployment_Node(Live.BigBankplc.bigbankweb, "bigbank-web*** (x4)", $type="Ubuntu 16.04 LTS", $descr="A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankweb.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="An open source Java EE web server.", $tags="Element", $link="") { + Container(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") + } + + } + + Deployment_Node(Live.BigBankplc.bigbankapi, "bigbank-api*** (x8)", $type="Ubuntu 16.04 LTS", $descr="A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankapi.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="An open source Java EE web server.", $tags="Element", $link="") { + Container(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") + } + + } + + Deployment_Node(Live.BigBankplc.bigbankdb01, "bigbank-db01", $type="Ubuntu 16.04 LTS", $descr="The primary database server.", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankdb01.OraclePrimary, "Oracle - Primary", $type="Oracle 12c", $descr="The primary, live database server.", $tags="Element", $link="") { + ContainerDb(Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + } + + } + + Deployment_Node(Live.BigBankplc.bigbankdb02, "bigbank-db02", $type="Ubuntu 16.04 LTS", $descr="The secondary database server.", $tags="Failover", $link="") { + Deployment_Node(Live.BigBankplc.bigbankdb02.OracleSecondary, "Oracle - Secondary", $type="Oracle 12c", $descr="A secondary, standby database server, used for failover purposes only.", $tags="Failover", $link="") { + ContainerDb(Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database,Failover", $link="") + } + + } + +} + +Rel(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, Live.Customerscomputer.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") +Rel(Live.Customersmobiledevice.MobileApp_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(Live.Customerscomputer.WebBrowser.SinglePageApplication_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") +Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="") +Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2, "Reads from and writes to", $techn="JDBC", $tags="Failover", $link="") +Rel(Live.BigBankplc.bigbankdb01.OraclePrimary, Live.BigBankplc.bigbankdb02.OracleSecondary, "Replicates data to", $techn="", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml new file mode 100644 index 000000000..57f8c67a1 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml @@ -0,0 +1,26 @@ +@startuml +set separator none +title API Application - Dynamic + +!include <C4/C4_Sequence> + +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") +Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") +Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") +ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + +Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "Validates credentials using", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "select * from users where username = ?", $techn="JDBC", $tags="Relationship", $link="") +Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "Returns user data to", $techn="JDBC", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml new file mode 100644 index 000000000..ee3f343b4 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml @@ -0,0 +1,36 @@ +@startuml +set separator none +title API Application - Dynamic + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> +!include <C4/C4_Component> + +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { + Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") +} + +Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") +ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + +Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1. Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2. Validates credentials using", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3. select * from users where username = ?", $techn="JDBC", $tags="Relationship", $link="") +Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4. Returns user data to", $techn="JDBC", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "5. Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "6. Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml new file mode 100644 index 000000000..e4b9cce22 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml @@ -0,0 +1,27 @@ +@startuml +set separator none +title Internet Banking System - System Context + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> + +AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Person", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +Person_Ext(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person", $link="") +System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") +System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") +System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + +Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") +Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml new file mode 100644 index 000000000..8f4464816 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml @@ -0,0 +1,39 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> + +AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Person", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +Enterprise_Boundary(enterprise, "Big Bank plc") { + Person(CustomerServiceStaff, "Customer Service Staff", $descr="Customer service staff within the bank.", $tags="Person,Bank Staff", $link="") + Person(BackOfficeStaff, "Back Office Staff", $descr="Administration and support staff within the bank.", $tags="Person,Bank Staff", $link="") + System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") + System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + System(ATM, "ATM", $descr="Allows customers to withdraw cash.", $tags="Software System,Existing System", $link="") +} + +Person_Ext(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person", $link="") + +Rel(ATM, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") +Rel(PersonalBankingCustomer, ATM, "Withdraws cash using", $techn="", $tags="Relationship", $link="") +Rel(CustomerServiceStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") +Rel(PersonalBankingCustomer, CustomerServiceStaff, "Asks questions to", $techn="Telephone", $tags="Relationship", $link="") +Rel(BackOfficeStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") +Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") +Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml new file mode 100644 index 000000000..db81378bc --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml @@ -0,0 +1,52 @@ +@startuml +set separator none +title Spring PetClinic - Deployment - Live + +left to right direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> +!include <C4/C4_Deployment> + +AddElementTag("Container,Application", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="Solid", $borderThickness="1") + +AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") + +Deployment_Node(Live.AmazonWebServices, "Amazon Web Services", $type="", $descr="", $tags="Amazon Web Services - Cloud", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1, "US-East-1", $type="", $descr="", $tags="Amazon Web Services - Region", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS, "Amazon RDS", $type="", $descr="", $tags="Amazon Web Services - RDS", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL, "MySQL", $type="", $descr="", $tags="Amazon Web Services - RDS MySQL instance", $link="") { + ContainerDb(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Database", $techn="Relational database schema", $descr="Stores information regarding the veterinarians, the clients, and their pets.", $tags="Container,Database", $link="") + } + + } + + Deployment_Node(Live.AmazonWebServices.USEast1.Route53, "Route 53", $type="", $descr="Highly available and scalable cloud DNS service.", $tags="Amazon Web Services - Route 53", $link="") + Deployment_Node(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Elastic Load Balancer", $type="", $descr="Automatically distributes incoming application traffic.", $tags="Amazon Web Services - Elastic Load Balancing", $link="") + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup, "Autoscaling group", $type="", $descr="", $tags="Amazon Web Services - Auto Scaling", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2, "Amazon EC2", $type="", $descr="", $tags="Amazon Web Services - EC2", $link="") { + Container(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Web Application", $techn="Java and Spring Boot", $descr="Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", $tags="Container,Application", $link="") + } + + } + + } + +} + +Rel(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Reads from and writes to", $techn="MySQL Protocol/SSL", $tags="Relationship", $link="") +Rel(Live.AmazonWebServices.USEast1.Route53, Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Forwards requests to", $techn="HTTPS", $tags="Relationship", $link="") +Rel(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Forwards requests to", $techn="HTTPS", $tags="Relationship", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml new file mode 100644 index 000000000..f6a6505d3 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml @@ -0,0 +1,39 @@ +@startuml +set separator none +title Spring PetClinic - Deployment - Live + +left to right direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> +!include <C4/C4_Deployment> + +Deployment_Node(Live.AmazonWebServices, "Amazon Web Services", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1, "US-East-1", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS, "Amazon RDS", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL, "MySQL", $type="", $descr="", $tags="", $link="") { + ContainerDb(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Database", $techn="Relational database schema", $descr="Stores information regarding the veterinarians, the clients, and their pets.", $tags="", $link="") + } + + } + + Deployment_Node(Live.AmazonWebServices.USEast1.Route53, "Route 53", $type="", $descr="Highly available and scalable cloud DNS service.", $tags="", $link="") + Deployment_Node(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Elastic Load Balancer", $type="", $descr="Automatically distributes incoming application traffic.", $tags="", $link="") + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup, "Autoscaling group", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2, "Amazon EC2", $type="", $descr="", $tags="", $link="") { + Container(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Web Application", $techn="Java and Spring Boot", $descr="Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", $tags="", $link="") + } + + } + + } + +} + +Rel(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Reads from and writes to", $techn="MySQL Protocol/SSL", $tags="", $link="") +Rel(Live.AmazonWebServices.USEast1.Route53, Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Forwards requests to", $techn="HTTPS", $tags="", $link="") +Rel(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Forwards requests to", $techn="HTTPS", $tags="", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml new file mode 100644 index 000000000..94c6a07ac --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml @@ -0,0 +1,28 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> + +AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_1, "Group 1", $tags="Group 1") { + Person(User1, "User 1", $descr="", $tags="", $link="") +} + +AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_2, "Group 2", $tags="Group 2") { + Person(User2, "User 2", $descr="", $tags="", $link="") +} + +AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_3, "Group 3", $tags="Group 3") { + Person(User3, "User 3", $descr="", $tags="", $link="") +} + + + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml new file mode 100644 index 000000000..085b61b85 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml @@ -0,0 +1,28 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> + +AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_1, "Group 1", $tags="Group 1") { + Person(User1, "User 1", $descr="", $tags="", $link="") +} + +AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_2, "Group 2", $tags="Group 2") { + Person(User2, "User 2", $descr="", $tags="", $link="") +} + +AddBoundaryTag("Group 3", $borderColor="#aabbcc", $fontColor="#aabbcc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_3, "Group 3", $tags="Group 3") { + Person(User3, "User 3", $descr="", $tags="", $link="") +} + + + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml new file mode 100644 index 000000000..5707e4f47 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml @@ -0,0 +1,26 @@ +@startuml +set separator none +title D - F - Components + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Component> + +System(C, "C", $descr="", $tags="", $link="") + +Container_Boundary("D.F_boundary", "F", $tags="") { + AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_1, "Group 4", $tags="Group 4") { + Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") + } + + Component(D.F.G, "G", $techn="", $descr="", $tags="", $link="") +} + +Rel(C, D.F.G, "", $techn="", $tags="", $link="") +Rel(C, D.F.H, "", $techn="", $tags="", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml new file mode 100644 index 000000000..42af97606 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml @@ -0,0 +1,26 @@ +@startuml +set separator none +title D - Containers + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> + +System(C, "C", $descr="", $tags="", $link="") + +System_Boundary("D_boundary", "D", $tags="") { + AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_1, "Group 3", $tags="Group 3") { + Container(D.F, "F", $techn="", $descr="", $tags="", $link="") + } + + Container(D.E, "E", $techn="", $descr="", $tags="", $link="") +} + +Rel(C, D.E, "", $techn="", $tags="", $link="") +Rel(C, D.F, "", $techn="", $tags="", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml new file mode 100644 index 000000000..add8abb27 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml @@ -0,0 +1,31 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> + +Enterprise_Boundary(enterprise, "Enterprise") { + AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_1, "Group 2", $tags="Group 2") { + System(D, "D", $descr="", $tags="", $link="") + } + + System(C, "C", $descr="", $tags="", $link="") +} + +AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_2, "Group 1", $tags="Group 1") { + System_Ext(B, "B", $descr="", $tags="", $link="") +} + +System_Ext(A, "A", $descr="", $tags="", $link="") + +Rel(B, C, "", $techn="", $tags="", $link="") +Rel(C, D, "", $techn="", $tags="", $link="") +Rel(A, B, "", $techn="", $tags="", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml new file mode 100644 index 000000000..cda61f1b3 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml @@ -0,0 +1,26 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> + +AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_1, "Organisation 1", $tags="Organisation 1") { + AddBoundaryTag("Organisation 1.Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_2, "Department 1", $tags="Organisation 1.Department 1") { + AddBoundaryTag("Organisation 1.Department 1.Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_3, "Team 1", $tags="Organisation 1.Department 1.Team 1") { + System(Team1, "Team 1", $descr="", $tags="", $link="") + } + + } + +} + + + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml new file mode 100644 index 000000000..783dc9fb8 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml @@ -0,0 +1,38 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> + +AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_1, "Organisation 1", $tags="Organisation 1") { + System(Organisation1, "Organisation 1", $descr="", $tags="", $link="") + AddBoundaryTag("Organisation 1/Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_2, "Department 1", $tags="Organisation 1/Department 1") { + System(Department1, "Department 1", $descr="", $tags="", $link="") + AddBoundaryTag("Organisation 1/Department 1/Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_3, "Team 1", $tags="Organisation 1/Department 1/Team 1") { + System(Team1, "Team 1", $descr="", $tags="", $link="") + } + + AddBoundaryTag("Organisation 1/Department 1/Team 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_4, "Team 2", $tags="Organisation 1/Department 1/Team 2") { + System(Team2, "Team 2", $descr="", $tags="", $link="") + } + + } + +} + +AddBoundaryTag("Organisation 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_5, "Organisation 2", $tags="Organisation 2") { + System(Organisation2, "Organisation 2", $descr="", $tags="", $link="") +} + + + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml new file mode 100644 index 000000000..3a505ee33 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml @@ -0,0 +1,28 @@ +@startuml +set separator none +title SoftwareSystem - Containers + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Container> + +System_Boundary("SoftwareSystem_boundary", "SoftwareSystem", $tags="") { + WithoutPropertyHeader() + AddProperty("IP","127.0.0.1") + AddProperty("Region","East") + Container(SoftwareSystem.Container1, "Container 1", $techn="", $descr="", $tags="", $link="") + WithoutPropertyHeader() + AddProperty("IP","127.0.0.2") + AddProperty("Region","West") + Container(SoftwareSystem.Container2, "Container 2", $techn="", $descr="", $tags="", $link="") +} + +WithoutPropertyHeader() +AddProperty("Prop1","Value1") +AddProperty("Prop2","Value2") +Rel(SoftwareSystem.Container1, SoftwareSystem.Container2, "", $techn="", $tags="", $link="") + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml new file mode 100644 index 000000000..f96e7c97a --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml @@ -0,0 +1,21 @@ +@startuml +set separator none +title Deployment - Default + +top to bottom direction + +!include <C4/C4> +!include <C4/C4_Context> +!include <C4/C4_Deployment> + +WithoutPropertyHeader() +AddProperty("Prop1","Value1") +Deployment_Node(Default.Deploymentnode, "Deployment node", $type="", $descr="", $tags="", $link="") { + WithoutPropertyHeader() + AddProperty("Prop2","Value2") + Deployment_Node(Default.Deploymentnode.Infrastructurenode, "Infrastructure node", $type="technology", $descr="description", $tags="", $link="") +} + + +SHOW_LEGEND(true) +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml new file mode 100644 index 000000000..edc664115 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml @@ -0,0 +1,116 @@ +@startuml +set separator none +title Internet Banking System - API Application - Components + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<InternetBankingSystem.APIApplication.AccountsSummaryController>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam database<<InternetBankingSystem.Database>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication.EmailComponent>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam rectangle<<EmailSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<MainframeBankingSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication.MainframeBankingSystemFacade>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.MobileApp>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication.ResetPasswordController>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication.SecurityComponent>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication.SignInController>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.SinglePageApplication>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication>> { + BorderColor #2e6295 + FontColor #2e6295 + shadowing false +} + +rectangle "==Mainframe Banking System\n<size:10>[Software System]</size>\n\nStores all of the core banking information about customers, accounts, transactions, etc." <<MainframeBankingSystem>> as MainframeBankingSystem +rectangle "==Single-Page Application\n<size:10>[Container: JavaScript and Angular]</size>\n\nProvides all of the Internet banking functionality to customers via their web browser." <<InternetBankingSystem.SinglePageApplication>> as InternetBankingSystem.SinglePageApplication +rectangle "==E-mail System\n<size:10>[Software System]</size>\n\nThe internal Microsoft Exchange e-mail system." <<EmailSystem>> as EmailSystem +rectangle "==Mobile App\n<size:10>[Container: Xamarin]</size>\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <<InternetBankingSystem.MobileApp>> as InternetBankingSystem.MobileApp +database "==Database\n<size:10>[Container: Oracle Database Schema]</size>\n\nStores user registration information, hashed authentication credentials, access logs, etc." <<InternetBankingSystem.Database>> as InternetBankingSystem.Database + +rectangle "API Application\n<size:10>[Container: Java and Spring MVC]</size>" <<InternetBankingSystem.APIApplication>> { + rectangle "==Sign In Controller\n<size:10>[Component: Spring MVC Rest Controller]</size>\n\nAllows users to sign in to the Internet Banking System." <<InternetBankingSystem.APIApplication.SignInController>> as InternetBankingSystem.APIApplication.SignInController + rectangle "==Accounts Summary Controller\n<size:10>[Component: Spring MVC Rest Controller]</size>\n\nProvides customers with a summary of their bank accounts." <<InternetBankingSystem.APIApplication.AccountsSummaryController>> as InternetBankingSystem.APIApplication.AccountsSummaryController + rectangle "==Reset Password Controller\n<size:10>[Component: Spring MVC Rest Controller]</size>\n\nAllows users to reset their passwords with a single use URL." <<InternetBankingSystem.APIApplication.ResetPasswordController>> as InternetBankingSystem.APIApplication.ResetPasswordController + rectangle "==Security Component\n<size:10>[Component: Spring Bean]</size>\n\nProvides functionality related to signing in, changing passwords, etc." <<InternetBankingSystem.APIApplication.SecurityComponent>> as InternetBankingSystem.APIApplication.SecurityComponent + rectangle "==Mainframe Banking System Facade\n<size:10>[Component: Spring Bean]</size>\n\nA facade onto the mainframe banking system." <<InternetBankingSystem.APIApplication.MainframeBankingSystemFacade>> as InternetBankingSystem.APIApplication.MainframeBankingSystemFacade + rectangle "==E-mail Component\n<size:10>[Component: Spring Bean]</size>\n\nSends e-mails to users." <<InternetBankingSystem.APIApplication.EmailComponent>> as InternetBankingSystem.APIApplication.EmailComponent +} + +InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.AccountsSummaryController : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.AccountsSummaryController : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.APIApplication.SignInController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "<color:#707070>Uses" +InternetBankingSystem.APIApplication.AccountsSummaryController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.MainframeBankingSystemFacade : "<color:#707070>Uses" +InternetBankingSystem.APIApplication.ResetPasswordController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "<color:#707070>Uses" +InternetBankingSystem.APIApplication.ResetPasswordController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.EmailComponent : "<color:#707070>Uses" +InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "<color:#707070>Reads from and writes to\n<color:#707070><size:8>[JDBC]</size>" +InternetBankingSystem.APIApplication.MainframeBankingSystemFacade .[#707070,thickness=2].> MainframeBankingSystem : "<color:#707070>Uses\n<color:#707070><size:8>[XML/HTTPS]</size>" +InternetBankingSystem.APIApplication.EmailComponent .[#707070,thickness=2].> EmailSystem : "<color:#707070>Sends e-mail using" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml new file mode 100644 index 000000000..300b0d9e8 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml @@ -0,0 +1,92 @@ +@startuml +set separator none +title Internet Banking System - Containers + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<InternetBankingSystem.APIApplication>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam database<<InternetBankingSystem.Database>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<EmailSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<MainframeBankingSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<InternetBankingSystem.MobileApp>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam person<<PersonalBankingCustomer>> { + BackgroundColor #08427b + FontColor #ffffff + BorderColor #052e56 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.SinglePageApplication>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.WebApplication>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<InternetBankingSystem>> { + BorderColor #0b4884 + FontColor #0b4884 + shadowing false +} + +person "==Personal Banking Customer\n<size:10>[Person]</size>\n\nA customer of the bank, with personal bank accounts." <<PersonalBankingCustomer>> as PersonalBankingCustomer +rectangle "==Mainframe Banking System\n<size:10>[Software System]</size>\n\nStores all of the core banking information about customers, accounts, transactions, etc." <<MainframeBankingSystem>> as MainframeBankingSystem +rectangle "==E-mail System\n<size:10>[Software System]</size>\n\nThe internal Microsoft Exchange e-mail system." <<EmailSystem>> as EmailSystem + +rectangle "Internet Banking System\n<size:10>[Software System]</size>" <<InternetBankingSystem>> { + rectangle "==Single-Page Application\n<size:10>[Container: JavaScript and Angular]</size>\n\nProvides all of the Internet banking functionality to customers via their web browser." <<InternetBankingSystem.SinglePageApplication>> as InternetBankingSystem.SinglePageApplication + rectangle "==Mobile App\n<size:10>[Container: Xamarin]</size>\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <<InternetBankingSystem.MobileApp>> as InternetBankingSystem.MobileApp + rectangle "==Web Application\n<size:10>[Container: Java and Spring MVC]</size>\n\nDelivers the static content and the Internet banking single page application." <<InternetBankingSystem.WebApplication>> as InternetBankingSystem.WebApplication + rectangle "==API Application\n<size:10>[Container: Java and Spring MVC]</size>\n\nProvides Internet banking functionality via a JSON/HTTPS API." <<InternetBankingSystem.APIApplication>> as InternetBankingSystem.APIApplication + database "==Database\n<size:10>[Container: Oracle Database Schema]</size>\n\nStores user registration information, hashed authentication credentials, access logs, etc." <<InternetBankingSystem.Database>> as InternetBankingSystem.Database +} + +PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.WebApplication : "<color:#707070>Visits bigbank.com/ib using\n<color:#707070><size:8>[HTTPS]</size>" +PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.SinglePageApplication : "<color:#707070>Views account balances, and makes payments using" +PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.MobileApp : "<color:#707070>Views account balances, and makes payments using" +InternetBankingSystem.WebApplication .[#707070,thickness=2].> InternetBankingSystem.SinglePageApplication : "<color:#707070>Delivers to the customer's web browser" +InternetBankingSystem.APIApplication .[#707070,thickness=2].> InternetBankingSystem.Database : "<color:#707070>Reads from and writes to\n<color:#707070><size:8>[JDBC]</size>" +InternetBankingSystem.APIApplication .[#707070,thickness=2].> MainframeBankingSystem : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[XML/HTTPS]</size>" +InternetBankingSystem.APIApplication .[#707070,thickness=2].> EmailSystem : "<color:#707070>Sends e-mail using\n<color:#707070><size:8>[SMTP]</size>" +InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "<color:#707070>Sends e-mails to" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml new file mode 100644 index 000000000..e35d6fa8c --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml @@ -0,0 +1,128 @@ +@startuml +set separator none +title Internet Banking System - Deployment - Development + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Development.BigBankplc>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam database<<Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop.DockerContainerDatabaseServer>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop.DockerContainerWebServer>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Development.DeveloperLaptop.WebBrowser>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Development.BigBankplc.bigbankdev001>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} + +rectangle "Developer Laptop\n<size:10>[Deployment Node: Microsoft Windows 10 or Apple macOS]</size>" <<Development.DeveloperLaptop>> as Development.DeveloperLaptop { + rectangle "Docker Container - Web Server\n<size:10>[Deployment Node: Docker]</size>" <<Development.DeveloperLaptop.DockerContainerWebServer>> as Development.DeveloperLaptop.DockerContainerWebServer { + rectangle "Apache Tomcat\n<size:10>[Deployment Node: Apache Tomcat 8.x]</size>" <<Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat>> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat { + rectangle "==Web Application\n<size:10>[Container: Java and Spring MVC]</size>\n\nDelivers the static content and the Internet banking single page application." <<Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1>> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 + rectangle "==API Application\n<size:10>[Container: Java and Spring MVC]</size>\n\nProvides Internet banking functionality via a JSON/HTTPS API." <<Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1>> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 + } + + } + + rectangle "Docker Container - Database Server\n<size:10>[Deployment Node: Docker]</size>" <<Development.DeveloperLaptop.DockerContainerDatabaseServer>> as Development.DeveloperLaptop.DockerContainerDatabaseServer { + rectangle "Database Server\n<size:10>[Deployment Node: Oracle 12c]</size>" <<Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer>> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer { + database "==Database\n<size:10>[Container: Oracle Database Schema]</size>\n\nStores user registration information, hashed authentication credentials, access logs, etc." <<Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1>> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 + } + + } + + rectangle "Web Browser\n<size:10>[Deployment Node: Chrome, Firefox, Safari, or Edge]</size>" <<Development.DeveloperLaptop.WebBrowser>> as Development.DeveloperLaptop.WebBrowser { + rectangle "==Single-Page Application\n<size:10>[Container: JavaScript and Angular]</size>\n\nProvides all of the Internet banking functionality to customers via their web browser." <<Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1>> as Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 + } + +} + +rectangle "Big Bank plc\n<size:10>[Deployment Node: Big Bank plc data center]</size>" <<Development.BigBankplc>> as Development.BigBankplc { + rectangle "bigbank-dev001\n<size:10>[Deployment Node]</size>" <<Development.BigBankplc.bigbankdev001>> as Development.BigBankplc.bigbankdev001 { + rectangle "==Mainframe Banking System\n<size:10>[Software System]</size>\n\nStores all of the core banking information about customers, accounts, transactions, etc." <<Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1>> as Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 + } + +} + +Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[XML/HTTPS]</size>" +Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 : "<color:#707070>Reads from and writes to\n<color:#707070><size:8>[JDBC]</size>" +Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 : "<color:#707070>Delivers to the customer's web browser" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml new file mode 100644 index 000000000..72427ad33 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml @@ -0,0 +1,190 @@ +@startuml +set separator none +title Internet Banking System - Deployment - Live + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankapi.ApacheTomcat>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankweb.ApacheTomcat>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.Customerscomputer>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.Customersmobiledevice>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam database<<Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam database<<Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<Live.Customersmobiledevice.MobileApp_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankdb01.OraclePrimary>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankdb02.OracleSecondary>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.Customerscomputer.WebBrowser.SinglePageApplication_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<Live.Customerscomputer.WebBrowser>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankapi>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankdb01>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankdb02>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankprod001>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} +skinparam rectangle<<Live.BigBankplc.bigbankweb>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false +} + +rectangle "Customer's mobile device\n<size:10>[Deployment Node: Apple iOS or Android]</size>" <<Live.Customersmobiledevice>> as Live.Customersmobiledevice { + rectangle "==Mobile App\n<size:10>[Container: Xamarin]</size>\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <<Live.Customersmobiledevice.MobileApp_1>> as Live.Customersmobiledevice.MobileApp_1 +} + +rectangle "Customer's computer\n<size:10>[Deployment Node: Microsoft Windows or Apple macOS]</size>" <<Live.Customerscomputer>> as Live.Customerscomputer { + rectangle "Web Browser\n<size:10>[Deployment Node: Chrome, Firefox, Safari, or Edge]</size>" <<Live.Customerscomputer.WebBrowser>> as Live.Customerscomputer.WebBrowser { + rectangle "==Single-Page Application\n<size:10>[Container: JavaScript and Angular]</size>\n\nProvides all of the Internet banking functionality to customers via their web browser." <<Live.Customerscomputer.WebBrowser.SinglePageApplication_1>> as Live.Customerscomputer.WebBrowser.SinglePageApplication_1 + } + +} + +rectangle "Big Bank plc\n<size:10>[Deployment Node: Big Bank plc data center]</size>" <<Live.BigBankplc>> as Live.BigBankplc { + rectangle "bigbank-prod001\n<size:10>[Deployment Node]</size>" <<Live.BigBankplc.bigbankprod001>> as Live.BigBankplc.bigbankprod001 { + rectangle "==Mainframe Banking System\n<size:10>[Software System]</size>\n\nStores all of the core banking information about customers, accounts, transactions, etc." <<Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1>> as Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 + } + + rectangle "bigbank-web*** (x4)\n<size:10>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<Live.BigBankplc.bigbankweb>> as Live.BigBankplc.bigbankweb { + rectangle "Apache Tomcat\n<size:10>[Deployment Node: Apache Tomcat 8.x]</size>" <<Live.BigBankplc.bigbankweb.ApacheTomcat>> as Live.BigBankplc.bigbankweb.ApacheTomcat { + rectangle "==Web Application\n<size:10>[Container: Java and Spring MVC]</size>\n\nDelivers the static content and the Internet banking single page application." <<Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1>> as Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 + } + + } + + rectangle "bigbank-api*** (x8)\n<size:10>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<Live.BigBankplc.bigbankapi>> as Live.BigBankplc.bigbankapi { + rectangle "Apache Tomcat\n<size:10>[Deployment Node: Apache Tomcat 8.x]</size>" <<Live.BigBankplc.bigbankapi.ApacheTomcat>> as Live.BigBankplc.bigbankapi.ApacheTomcat { + rectangle "==API Application\n<size:10>[Container: Java and Spring MVC]</size>\n\nProvides Internet banking functionality via a JSON/HTTPS API." <<Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1>> as Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 + } + + } + + rectangle "bigbank-db01\n<size:10>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<Live.BigBankplc.bigbankdb01>> as Live.BigBankplc.bigbankdb01 { + rectangle "Oracle - Primary\n<size:10>[Deployment Node: Oracle 12c]</size>" <<Live.BigBankplc.bigbankdb01.OraclePrimary>> as Live.BigBankplc.bigbankdb01.OraclePrimary { + database "==Database\n<size:10>[Container: Oracle Database Schema]</size>\n\nStores user registration information, hashed authentication credentials, access logs, etc." <<Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1>> as Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 + } + + } + + rectangle "bigbank-db02\n<size:10>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<Live.BigBankplc.bigbankdb02>> as Live.BigBankplc.bigbankdb02 { + rectangle "Oracle - Secondary\n<size:10>[Deployment Node: Oracle 12c]</size>" <<Live.BigBankplc.bigbankdb02.OracleSecondary>> as Live.BigBankplc.bigbankdb02.OracleSecondary { + database "==Database\n<size:10>[Container: Oracle Database Schema]</size>\n\nStores user registration information, hashed authentication credentials, access logs, etc." <<Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2>> as Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2 + } + + } + +} + +Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 .[#707070,thickness=2].> Live.Customerscomputer.WebBrowser.SinglePageApplication_1 : "<color:#707070>Delivers to the customer's web browser" +Live.Customersmobiledevice.MobileApp_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +Live.Customerscomputer.WebBrowser.SinglePageApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 : "<color:#707070>Makes API calls to\n<color:#707070><size:8>[XML/HTTPS]</size>" +Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 : "<color:#707070>Reads from and writes to\n<color:#707070><size:8>[JDBC]</size>" +Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2 : "<color:#707070>Reads from and writes to\n<color:#707070><size:8>[JDBC]</size>" +Live.BigBankplc.bigbankdb01.OraclePrimary .[#707070,thickness=2].> Live.BigBankplc.bigbankdb02.OracleSecondary : "<color:#707070>Replicates data to" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml new file mode 100644 index 000000000..eb84e8096 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml @@ -0,0 +1,49 @@ +@startuml +set separator none +title API Application - Dynamic + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam sequenceParticipant<<InternetBankingSystem.Database>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam sequenceParticipant<<InternetBankingSystem.APIApplication.SecurityComponent>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam sequenceParticipant<<InternetBankingSystem.APIApplication.SignInController>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam sequenceParticipant<<InternetBankingSystem.SinglePageApplication>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} + +participant "Single-Page Application\n<size:10>[Container: JavaScript and Angular]</size>" as InternetBankingSystem.SinglePageApplication <<InternetBankingSystem.SinglePageApplication>> #438dd5 +participant "Sign In Controller\n<size:10>[Component: Spring MVC Rest Controller]</size>" as InternetBankingSystem.APIApplication.SignInController <<InternetBankingSystem.APIApplication.SignInController>> #85bbf0 +participant "Security Component\n<size:10>[Component: Spring Bean]</size>" as InternetBankingSystem.APIApplication.SecurityComponent <<InternetBankingSystem.APIApplication.SecurityComponent>> #85bbf0 +database "Database\n<size:10>[Container: Oracle Database Schema]</size>" as InternetBankingSystem.Database <<InternetBankingSystem.Database>> #438dd5 +InternetBankingSystem.SinglePageApplication -[#707070]> InternetBankingSystem.APIApplication.SignInController : Submits credentials to +InternetBankingSystem.APIApplication.SignInController -[#707070]> InternetBankingSystem.APIApplication.SecurityComponent : Validates credentials using +InternetBankingSystem.APIApplication.SecurityComponent -[#707070]> InternetBankingSystem.Database : select * from users where username = ? +InternetBankingSystem.APIApplication.SecurityComponent <-[#707070]- InternetBankingSystem.Database : Returns user data to +InternetBankingSystem.APIApplication.SignInController <-[#707070]- InternetBankingSystem.APIApplication.SecurityComponent : Returns true if the hashed password matches +InternetBankingSystem.SinglePageApplication <-[#707070]- InternetBankingSystem.APIApplication.SignInController : Sends back an authentication token to +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml new file mode 100644 index 000000000..687ce9d7e --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml @@ -0,0 +1,60 @@ +@startuml +set separator none +title API Application - Dynamic + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam database<<InternetBankingSystem.Database>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication.SecurityComponent>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication.SignInController>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.SinglePageApplication>> { + BackgroundColor #438dd5 + FontColor #ffffff + BorderColor #2e6295 + shadowing false +} +skinparam rectangle<<InternetBankingSystem.APIApplication>> { + BorderColor #2e6295 + FontColor #2e6295 + shadowing false +} + +rectangle "API Application\n<size:10>[Container: Java and Spring MVC]</size>" <<InternetBankingSystem.APIApplication>> { + rectangle "==Sign In Controller\n<size:10>[Component: Spring MVC Rest Controller]</size>\n\nAllows users to sign in to the Internet Banking System." <<InternetBankingSystem.APIApplication.SignInController>> as InternetBankingSystem.APIApplication.SignInController + rectangle "==Security Component\n<size:10>[Component: Spring Bean]</size>\n\nProvides functionality related to signing in, changing passwords, etc." <<InternetBankingSystem.APIApplication.SecurityComponent>> as InternetBankingSystem.APIApplication.SecurityComponent +} + +rectangle "==Single-Page Application\n<size:10>[Container: JavaScript and Angular]</size>\n\nProvides all of the Internet banking functionality to customers via their web browser." <<InternetBankingSystem.SinglePageApplication>> as InternetBankingSystem.SinglePageApplication +database "==Database\n<size:10>[Container: Oracle Database Schema]</size>\n\nStores user registration information, hashed authentication credentials, access logs, etc." <<InternetBankingSystem.Database>> as InternetBankingSystem.Database + +InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "<color:#707070>1. Submits credentials to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +InternetBankingSystem.APIApplication.SignInController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "<color:#707070>2. Validates credentials using" +InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "<color:#707070>3. select * from users where username = ?\n<color:#707070><size:8>[JDBC]</size>" +InternetBankingSystem.APIApplication.SecurityComponent <.[#707070,thickness=2]. InternetBankingSystem.Database : "<color:#707070>4. Returns user data to\n<color:#707070><size:8>[JDBC]</size>" +InternetBankingSystem.APIApplication.SignInController <.[#707070,thickness=2]. InternetBankingSystem.APIApplication.SecurityComponent : "<color:#707070>5. Returns true if the hashed password matches" +InternetBankingSystem.SinglePageApplication <.[#707070,thickness=2]. InternetBankingSystem.APIApplication.SignInController : "<color:#707070>6. Sends back an authentication token to\n<color:#707070><size:8>[JSON/HTTPS]</size>" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml new file mode 100644 index 000000000..36302bdb3 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml @@ -0,0 +1,50 @@ +@startuml +set separator none +title Internet Banking System - System Context + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<EmailSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<InternetBankingSystem>> { + BackgroundColor #1168bd + FontColor #ffffff + BorderColor #0b4884 + shadowing false +} +skinparam rectangle<<MainframeBankingSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam person<<PersonalBankingCustomer>> { + BackgroundColor #08427b + FontColor #ffffff + BorderColor #052e56 + shadowing false +} + +person "==Personal Banking Customer\n<size:10>[Person]</size>\n\nA customer of the bank, with personal bank accounts." <<PersonalBankingCustomer>> as PersonalBankingCustomer +rectangle "==Internet Banking System\n<size:10>[Software System]</size>\n\nAllows customers to view information about their bank accounts, and make payments." <<InternetBankingSystem>> as InternetBankingSystem +rectangle "==Mainframe Banking System\n<size:10>[Software System]</size>\n\nStores all of the core banking information about customers, accounts, transactions, etc." <<MainframeBankingSystem>> as MainframeBankingSystem +rectangle "==E-mail System\n<size:10>[Software System]</size>\n\nThe internal Microsoft Exchange e-mail system." <<EmailSystem>> as EmailSystem + +PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem : "<color:#707070>Views account balances, and makes payments using" +InternetBankingSystem .[#707070,thickness=2].> MainframeBankingSystem : "<color:#707070>Gets account information from, and makes payments using" +InternetBankingSystem .[#707070,thickness=2].> EmailSystem : "<color:#707070>Sends e-mail using" +EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "<color:#707070>Sends e-mails to" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml new file mode 100644 index 000000000..511b1a16c --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml @@ -0,0 +1,82 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<ATM>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam person<<BackOfficeStaff>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam person<<CustomerServiceStaff>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<EmailSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam rectangle<<InternetBankingSystem>> { + BackgroundColor #1168bd + FontColor #ffffff + BorderColor #0b4884 + shadowing false +} +skinparam rectangle<<MainframeBankingSystem>> { + BackgroundColor #999999 + FontColor #ffffff + BorderColor #6b6b6b + shadowing false +} +skinparam person<<PersonalBankingCustomer>> { + BackgroundColor #08427b + FontColor #ffffff + BorderColor #052e56 + shadowing false +} + +rectangle "Big Bank plc" <<enterprise>> { + skinparam RectangleBorderColor<<enterprise>> #444444 + skinparam RectangleFontColor<<enterprise>> #444444 + + person "==Customer Service Staff\n<size:10>[Person]</size>\n\nCustomer service staff within the bank." <<CustomerServiceStaff>> as CustomerServiceStaff + person "==Back Office Staff\n<size:10>[Person]</size>\n\nAdministration and support staff within the bank." <<BackOfficeStaff>> as BackOfficeStaff + rectangle "==Internet Banking System\n<size:10>[Software System]</size>\n\nAllows customers to view information about their bank accounts, and make payments." <<InternetBankingSystem>> as InternetBankingSystem + rectangle "==Mainframe Banking System\n<size:10>[Software System]</size>\n\nStores all of the core banking information about customers, accounts, transactions, etc." <<MainframeBankingSystem>> as MainframeBankingSystem + rectangle "==E-mail System\n<size:10>[Software System]</size>\n\nThe internal Microsoft Exchange e-mail system." <<EmailSystem>> as EmailSystem + rectangle "==ATM\n<size:10>[Software System]</size>\n\nAllows customers to withdraw cash." <<ATM>> as ATM +} + +person "==Personal Banking Customer\n<size:10>[Person]</size>\n\nA customer of the bank, with personal bank accounts." <<PersonalBankingCustomer>> as PersonalBankingCustomer + +ATM .[#707070,thickness=2].> MainframeBankingSystem : "<color:#707070>Uses" +PersonalBankingCustomer .[#707070,thickness=2].> ATM : "<color:#707070>Withdraws cash using" +CustomerServiceStaff .[#707070,thickness=2].> MainframeBankingSystem : "<color:#707070>Uses" +PersonalBankingCustomer .[#707070,thickness=2].> CustomerServiceStaff : "<color:#707070>Asks questions to\n<color:#707070><size:8>[Telephone]</size>" +BackOfficeStaff .[#707070,thickness=2].> MainframeBankingSystem : "<color:#707070>Uses" +PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem : "<color:#707070>Views account balances, and makes payments using" +InternetBankingSystem .[#707070,thickness=2].> MainframeBankingSystem : "<color:#707070>Gets account information from, and makes payments using" +InternetBankingSystem .[#707070,thickness=2].> EmailSystem : "<color:#707070>Sends e-mail using" +EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "<color:#707070>Sends e-mails to" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml new file mode 100644 index 000000000..3532dad2d --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml @@ -0,0 +1,102 @@ +@startuml +set separator none + +skinparam { + shadowing false + arrowFontSize 15 + defaultTextAlignment center + wrapWidth 100 + maxMessageSize 100 +} +hide stereotype + +skinparam rectangle<<_transparent>> { + BorderColor transparent + BackgroundColor transparent + FontColor transparent +} + +skinparam rectangle<<1>> { + BackgroundColor #ffffff + FontColor #cc2264 + BorderColor #cc2264 + roundCorner 20 +} +rectangle "==Amazon Web Services - Auto Scaling\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}>" <<1>> + +skinparam rectangle<<2>> { + BackgroundColor #ffffff + FontColor #232f3e + BorderColor #232f3e + roundCorner 20 +} +rectangle "==Amazon Web Services - Cloud\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}>" <<2>> + +skinparam rectangle<<3>> { + BackgroundColor #ffffff + FontColor #d86613 + BorderColor #d86613 + roundCorner 20 +} +rectangle "==Amazon Web Services - EC2\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}>" <<3>> + +skinparam rectangle<<4>> { + BackgroundColor #ffffff + FontColor #693cc5 + BorderColor #693cc5 + roundCorner 20 +} +rectangle "==Amazon Web Services - Elastic Load Balancing\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}>" <<4>> + +skinparam rectangle<<5>> { + BackgroundColor #ffffff + FontColor #3b48cc + BorderColor #3b48cc + roundCorner 20 +} +rectangle "==Amazon Web Services - RDS\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}>" <<5>> + +skinparam rectangle<<6>> { + BackgroundColor #ffffff + FontColor #3b48cc + BorderColor #3b48cc + roundCorner 20 +} +rectangle "==Amazon Web Services - RDS MySQL instance\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}>" <<6>> + +skinparam rectangle<<7>> { + BackgroundColor #ffffff + FontColor #147eba + BorderColor #147eba + roundCorner 20 +} +rectangle "==Amazon Web Services - Region\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}>" <<7>> + +skinparam rectangle<<8>> { + BackgroundColor #ffffff + FontColor #693cc5 + BorderColor #693cc5 + roundCorner 20 +} +rectangle "==Amazon Web Services - Route 53\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}>" <<8>> + +skinparam rectangle<<9>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #b2b2b2 + roundCorner 20 +} +rectangle "==Container, Application" <<9>> + +skinparam database<<10>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #b2b2b2 +} +database "==Container, Database" <<10>> + +rectangle "." <<_transparent>> as 11 +11 .[#707070,thickness=2].> 11 : "<color:#707070>Relationship" + + +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml new file mode 100644 index 000000000..b0799c732 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml @@ -0,0 +1,111 @@ +@startuml +set separator none +title Spring PetClinic - Deployment - Live + +left to right direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2>> { + BackgroundColor #ffffff + FontColor #d86613 + BorderColor #d86613 + roundCorner 20 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices.USEast1.AmazonRDS>> { + BackgroundColor #ffffff + FontColor #3b48cc + BorderColor #3b48cc + roundCorner 20 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices>> { + BackgroundColor #ffffff + FontColor #232f3e + BorderColor #232f3e + roundCorner 20 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices.USEast1.Autoscalinggroup>> { + BackgroundColor #ffffff + FontColor #cc2264 + BorderColor #cc2264 + roundCorner 20 + shadowing false +} +skinparam database<<Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #b2b2b2 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices.USEast1.ElasticLoadBalancer>> { + BackgroundColor #ffffff + FontColor #693cc5 + BorderColor #693cc5 + roundCorner 20 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices.USEast1.AmazonRDS.MySQL>> { + BackgroundColor #ffffff + FontColor #3b48cc + BorderColor #3b48cc + roundCorner 20 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices.USEast1.Route53>> { + BackgroundColor #ffffff + FontColor #693cc5 + BorderColor #693cc5 + roundCorner 20 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices.USEast1>> { + BackgroundColor #ffffff + FontColor #147eba + BorderColor #147eba + roundCorner 20 + shadowing false +} +skinparam rectangle<<Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1>> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #b2b2b2 + roundCorner 20 + shadowing false +} + +rectangle "Amazon Web Services\n<size:10>[Deployment Node]</size>\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}>" <<Live.AmazonWebServices>> as Live.AmazonWebServices { + rectangle "US-East-1\n<size:10>[Deployment Node]</size>\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}>" <<Live.AmazonWebServices.USEast1>> as Live.AmazonWebServices.USEast1 { + rectangle "Amazon RDS\n<size:10>[Deployment Node]</size>\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}>" <<Live.AmazonWebServices.USEast1.AmazonRDS>> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\n<size:10>[Deployment Node]</size>\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}>" <<Live.AmazonWebServices.USEast1.AmazonRDS.MySQL>> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { + database "==Database\n<size:10>[Container: Relational database schema]</size>\n\nStores information regarding the veterinarians, the clients, and their pets." <<Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1>> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1 + } + + } + + rectangle "==Route 53\n<size:10>[Infrastructure Node]</size>\n\nHighly available and scalable cloud DNS service.\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}>" <<Live.AmazonWebServices.USEast1.Route53>> as Live.AmazonWebServices.USEast1.Route53 + rectangle "==Elastic Load Balancer\n<size:10>[Infrastructure Node]</size>\n\nAutomatically distributes incoming application traffic.\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}>" <<Live.AmazonWebServices.USEast1.ElasticLoadBalancer>> as Live.AmazonWebServices.USEast1.ElasticLoadBalancer + rectangle "Autoscaling group\n<size:10>[Deployment Node]</size>\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}>" <<Live.AmazonWebServices.USEast1.Autoscalinggroup>> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2\n<size:10>[Deployment Node]</size>\n\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}>" <<Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2>> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2 { + rectangle "==Web Application\n<size:10>[Container: Java and Spring Boot]</size>\n\nAllows employees to view and manage information regarding the veterinarians, the clients, and their pets." <<Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1>> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1 + } + + } + + } + +} + +Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1 .[#707070,thickness=2].> Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1 : "<color:#707070>Reads from and writes to\n<color:#707070><size:8>[MySQL Protocol/SSL]</size>" +Live.AmazonWebServices.USEast1.Route53 .[#707070,thickness=2].> Live.AmazonWebServices.USEast1.ElasticLoadBalancer : "<color:#707070>Forwards requests to\n<color:#707070><size:8>[HTTPS]</size>" +Live.AmazonWebServices.USEast1.ElasticLoadBalancer .[#707070,thickness=2].> Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1 : "<color:#707070>Forwards requests to\n<color:#707070><size:8>[HTTPS]</size>" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml new file mode 100644 index 000000000..524f39cc9 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml @@ -0,0 +1,56 @@ +@startuml +set separator none +title Software System 1 - Container 1 - Components + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<SoftwareSystem1.Container1.Component1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1.Component2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2.Component3>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "Container 1\n<size:10>[Container]</size>" <<SoftwareSystem1.Container1>> { + rectangle "==Component 1\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component1>> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component2>> as SoftwareSystem1.Container1.Component2 +} + +rectangle "Container 2\n<size:10>[Container]</size>" <<SoftwareSystem2.Container2>> { + rectangle "==Component 3\n<size:10>[Component]</size>" <<SoftwareSystem2.Container2.Component3>> as SoftwareSystem2.Container2.Component3 +} + +SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "<color:#707070>Uses" +SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "<color:#707070>Uses" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml new file mode 100644 index 000000000..4888c4988 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml @@ -0,0 +1,62 @@ +@startuml +set separator none +title Software System 1 - Container 1 - Components + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<SoftwareSystem1.Container1.Component1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1.Component2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2.Component3>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "Software System 1\n<size:10>[Software System]</size>" <<SoftwareSystem1>> { + rectangle "Container 1\n<size:10>[Container]</size>" <<SoftwareSystem1.Container1>> { + rectangle "==Component 1\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component1>> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component2>> as SoftwareSystem1.Container1.Component2 + } + + } + +rectangle "Software System 2\n<size:10>[Software System]</size>" <<SoftwareSystem2>> { + rectangle "Container 2\n<size:10>[Container]</size>" <<SoftwareSystem2.Container2>> { + rectangle "==Component 3\n<size:10>[Component]</size>" <<SoftwareSystem2.Container2.Component3>> as SoftwareSystem2.Container2.Component3 + } + + } + +SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "<color:#707070>Uses" +SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "<color:#707070>Uses" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml new file mode 100644 index 000000000..bd0fade3d --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml @@ -0,0 +1,62 @@ +@startuml +set separator none +title A - Dynamic + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<A.A.A>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<B.B.B>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<A.A>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} +skinparam rectangle<<B.B>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "A\n<size:10>[Container]</size>" <<A.A>> { + rectangle "Group 1" <<group1>> { + skinparam RectangleBorderColor<<group1>> #cccccc + skinparam RectangleFontColor<<group1>> #cccccc + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==A\n<size:10>[Component]</size>" <<A.A.A>> as A.A.A + } + +} + +rectangle "B\n<size:10>[Container]</size>" <<B.B>> { + rectangle "Group 2" <<group2>> { + skinparam RectangleBorderColor<<group2>> #cccccc + skinparam RectangleFontColor<<group2>> #cccccc + skinparam RectangleBorderStyle<<group2>> dashed + + rectangle "==B\n<size:10>[Component]</size>" <<B.B.B>> as B.B.B + } + +} + +A.A.A .[#707070,thickness=2].> B.B.B : "<color:#707070>1. Uses" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml new file mode 100644 index 000000000..13cc2141b --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml @@ -0,0 +1,62 @@ +@startuml +set separator none +title A - Dynamic + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<A.A>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<B.B>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<A>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} +skinparam rectangle<<B>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "A\n<size:10>[Software System]</size>" <<A>> { + rectangle "Group 1" <<group1>> { + skinparam RectangleBorderColor<<group1>> #cccccc + skinparam RectangleFontColor<<group1>> #cccccc + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==A\n<size:10>[Container]</size>" <<A.A>> as A.A + } + +} + +rectangle "B\n<size:10>[Software System]</size>" <<B>> { + rectangle "Group 2" <<group2>> { + skinparam RectangleBorderColor<<group2>> #cccccc + skinparam RectangleFontColor<<group2>> #cccccc + skinparam RectangleBorderStyle<<group2>> dashed + + rectangle "==B\n<size:10>[Container]</size>" <<B.B>> as B.B + } + +} + +A.A .[#707070,thickness=2].> B.B : "<color:#707070>1. Uses" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml new file mode 100644 index 000000000..5b7656db8 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml @@ -0,0 +1,46 @@ +@startuml +set separator none +title Dynamic + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<A>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<B>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} + +rectangle "Group 1" <<group1>> { + skinparam RectangleBorderColor<<group1>> #cccccc + skinparam RectangleFontColor<<group1>> #cccccc + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==A\n<size:10>[Software System]</size>" <<A>> as A +} + +rectangle "Group 2" <<group2>> { + skinparam RectangleBorderColor<<group2>> #cccccc + skinparam RectangleFontColor<<group2>> #cccccc + skinparam RectangleBorderStyle<<group2>> dashed + + rectangle "==B\n<size:10>[Software System]</size>" <<B>> as B +} + +A .[#707070,thickness=2].> B : "<color:#707070>1. Uses" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml new file mode 100644 index 000000000..ba9a23a8a --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml @@ -0,0 +1,56 @@ +@startuml +set separator none +title Container 1 - Dynamic + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<SoftwareSystem1.Container1.Component1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1.Component2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2.Component3>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "Container 1\n<size:10>[Container]</size>" <<SoftwareSystem1.Container1>> { + rectangle "==Component 1\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component1>> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component2>> as SoftwareSystem1.Container1.Component2 +} + +rectangle "Container 2\n<size:10>[Container]</size>" <<SoftwareSystem2.Container2>> { + rectangle "==Component 3\n<size:10>[Component]</size>" <<SoftwareSystem2.Container2.Component3>> as SoftwareSystem2.Container2.Component3 +} + +SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "<color:#707070>1. Uses" +SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "<color:#707070>2. Uses" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml new file mode 100644 index 000000000..588a26fb6 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml @@ -0,0 +1,62 @@ +@startuml +set separator none +title Container 1 - Dynamic + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<SoftwareSystem1.Container1.Component1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1.Component2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2.Component3>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem1.Container1>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} +skinparam rectangle<<SoftwareSystem2.Container2>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "Software System 1\n<size:10>[Software System]</size>" <<SoftwareSystem1>> { + rectangle "Container 1\n<size:10>[Container]</size>" <<SoftwareSystem1.Container1>> { + rectangle "==Component 1\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component1>> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\n<size:10>[Component]</size>" <<SoftwareSystem1.Container1.Component2>> as SoftwareSystem1.Container1.Component2 + } + + } + +rectangle "Software System 2\n<size:10>[Software System]</size>" <<SoftwareSystem2>> { + rectangle "Container 2\n<size:10>[Container]</size>" <<SoftwareSystem2.Container2>> { + rectangle "==Component 3\n<size:10>[Component]</size>" <<SoftwareSystem2.Container2.Component3>> as SoftwareSystem2.Container2.Component3 + } + + } + +SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "<color:#707070>1. Uses" +SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "<color:#707070>2. Uses" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml new file mode 100644 index 000000000..8406d0994 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml @@ -0,0 +1,60 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<User1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<User2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<User3>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} + +rectangle "Group 1\n\n<img:https://example.com/icon1.png{scale=1.0}>" <<group1>> { + skinparam RectangleBorderColor<<group1>> #111111 + skinparam RectangleFontColor<<group1>> #111111 + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==User 1\n<size:10>[Person]</size>" <<User1>> as User1 +} + +rectangle "Group 2\n\n<img:https://example.com/icon2.png{scale=1.0}>" <<group2>> { + skinparam RectangleBorderColor<<group2>> #222222 + skinparam RectangleFontColor<<group2>> #222222 + skinparam RectangleBorderStyle<<group2>> dashed + + rectangle "==User 2\n<size:10>[Person]</size>" <<User2>> as User2 +} + +rectangle "Group 3" <<group3>> { + skinparam RectangleBorderColor<<group3>> #cccccc + skinparam RectangleFontColor<<group3>> #cccccc + skinparam RectangleBorderStyle<<group3>> dashed + + rectangle "==User 3\n<size:10>[Person]</size>" <<User3>> as User3 +} + + +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml new file mode 100644 index 000000000..5c51fe4d3 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml @@ -0,0 +1,60 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<User1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<User2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<User3>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} + +rectangle "Group 1\n\n<img:https://example.com/icon1.png{scale=1.0}>" <<group1>> { + skinparam RectangleBorderColor<<group1>> #111111 + skinparam RectangleFontColor<<group1>> #111111 + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==User 1\n<size:10>[Person]</size>" <<User1>> as User1 +} + +rectangle "Group 2\n\n<img:https://example.com/icon2.png{scale=1.0}>" <<group2>> { + skinparam RectangleBorderColor<<group2>> #222222 + skinparam RectangleFontColor<<group2>> #222222 + skinparam RectangleBorderStyle<<group2>> dashed + + rectangle "==User 2\n<size:10>[Person]</size>" <<User2>> as User2 +} + +rectangle "Group 3" <<group3>> { + skinparam RectangleBorderColor<<group3>> #aabbcc + skinparam RectangleFontColor<<group3>> #aabbcc + skinparam RectangleBorderStyle<<group3>> dashed + + rectangle "==User 3\n<size:10>[Person]</size>" <<User3>> as User3 +} + + +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml new file mode 100644 index 000000000..0cc6d9bd4 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml @@ -0,0 +1,56 @@ +@startuml +set separator none +title D - F - Components + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<C>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<D.F.G>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<D.F.H>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<D.F>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "==C\n<size:10>[Software System]</size>" <<C>> as C + +rectangle "F\n<size:10>[Container]</size>" <<D.F>> { + rectangle "Group 4" <<group1>> { + skinparam RectangleBorderColor<<group1>> #cccccc + skinparam RectangleFontColor<<group1>> #cccccc + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==H\n<size:10>[Component]</size>" <<D.F.H>> as D.F.H + } + + rectangle "==G\n<size:10>[Component]</size>" <<D.F.G>> as D.F.G +} + +C .[#707070,thickness=2].> D.F.G : "<color:#707070>" +C .[#707070,thickness=2].> D.F.H : "<color:#707070>" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml new file mode 100644 index 000000000..fd5a74bf5 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml @@ -0,0 +1,56 @@ +@startuml +set separator none +title D - Containers + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<C>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<D.E>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<D.F>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<D>> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false +} + +rectangle "==C\n<size:10>[Software System]</size>" <<C>> as C + +rectangle "D\n<size:10>[Software System]</size>" <<D>> { + rectangle "Group 3" <<group1>> { + skinparam RectangleBorderColor<<group1>> #cccccc + skinparam RectangleFontColor<<group1>> #cccccc + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==F\n<size:10>[Container]</size>" <<D.F>> as D.F + } + + rectangle "==E\n<size:10>[Container]</size>" <<D.E>> as D.E +} + +C .[#707070,thickness=2].> D.E : "<color:#707070>" +C .[#707070,thickness=2].> D.F : "<color:#707070>" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml new file mode 100644 index 000000000..094f24a31 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml @@ -0,0 +1,69 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<A>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<B>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<C>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<D>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} + +rectangle "Enterprise" <<enterprise>> { + skinparam RectangleBorderColor<<enterprise>> #444444 + skinparam RectangleFontColor<<enterprise>> #444444 + + rectangle "Group 2" <<group1>> { + skinparam RectangleBorderColor<<group1>> #cccccc + skinparam RectangleFontColor<<group1>> #cccccc + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==D\n<size:10>[Software System]</size>" <<D>> as D + } + + rectangle "==C\n<size:10>[Software System]</size>" <<C>> as C +} + +rectangle "Group 1" <<group2>> { + skinparam RectangleBorderColor<<group2>> #cccccc + skinparam RectangleFontColor<<group2>> #cccccc + skinparam RectangleBorderStyle<<group2>> dashed + + rectangle "==B\n<size:10>[Software System]</size>" <<B>> as B +} + +rectangle "==A\n<size:10>[Software System]</size>" <<A>> as A + +B .[#707070,thickness=2].> C : "<color:#707070>" +C .[#707070,thickness=2].> D : "<color:#707070>" +A .[#707070,thickness=2].> B : "<color:#707070>" +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml new file mode 100644 index 000000000..3c3d12a53 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml @@ -0,0 +1,88 @@ +@startuml +set separator none +title System Landscape + +top to bottom direction + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 +} + +hide stereotype + +skinparam rectangle<<Department1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<Organisation1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<Organisation2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<Team1>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} +skinparam rectangle<<Team2>> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false +} + +rectangle "Organisation 1" <<group1>> { + skinparam RectangleBorderColor<<group1>> #cccccc + skinparam RectangleFontColor<<group1>> #cccccc + skinparam RectangleBorderStyle<<group1>> dashed + + rectangle "==Organisation 1\n<size:10>[Software System]</size>" <<Organisation1>> as Organisation1 + rectangle "Department 1" <<group2>> { + skinparam RectangleBorderColor<<group2>> #cccccc + skinparam RectangleFontColor<<group2>> #cccccc + skinparam RectangleBorderStyle<<group2>> dashed + + rectangle "==Department 1\n<size:10>[Software System]</size>" <<Department1>> as Department1 + rectangle "Team 1" <<group3>> { + skinparam RectangleBorderColor<<group3>> #ff0000 + skinparam RectangleFontColor<<group3>> #ff0000 + skinparam RectangleBorderStyle<<group3>> dashed + + rectangle "==Team 1\n<size:10>[Software System]</size>" <<Team1>> as Team1 + } + + rectangle "Team 2" <<group4>> { + skinparam RectangleBorderColor<<group4>> #0000ff + skinparam RectangleFontColor<<group4>> #0000ff + skinparam RectangleBorderStyle<<group4>> dashed + + rectangle "==Team 2\n<size:10>[Software System]</size>" <<Team2>> as Team2 + } + + } + +} + +rectangle "Organisation 2" <<group5>> { + skinparam RectangleBorderColor<<group5>> #cccccc + skinparam RectangleFontColor<<group5>> #cccccc + skinparam RectangleBorderStyle<<group5>> dashed + + rectangle "==Organisation 2\n<size:10>[Software System]</size>" <<Organisation2>> as Organisation2 +} + + +@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd new file mode 100644 index 000000000..240ee181c --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd @@ -0,0 +1,13 @@ +title API Application - Dynamic - SignIn + +participant <<Container>>\nSingle-Page Application as Single-Page Application +participant <<Component>>\nSign In Controller as Sign In Controller +participant <<Component>>\nSecurity Component as Security Component +participant <<Container>>\nDatabase as Database + +Single-Page Application->Sign In Controller: Submits credentials to +Sign In Controller->Security Component: Validates credentials using +Security Component->Database: select * from users where username = ? +Database-->Security Component: Returns user data to +Security Component-->Sign In Controller: Returns true if the hashed password matches +Sign In Controller-->Single-Page Application: Sends back an authentication token to \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java new file mode 100644 index 000000000..e0a4c5229 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java @@ -0,0 +1,53 @@ +package com.structurizr.export.websequencediagrams; + +import com.structurizr.Workspace; +import com.structurizr.export.AbstractExporterTests; +import com.structurizr.export.Diagram; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.util.WorkspaceUtils; +import com.structurizr.view.DynamicView; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +public class WebSequenceDiagramsExporterTests extends AbstractExporterTests { + + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + WebSequenceDiagramsExporter exporter = new WebSequenceDiagramsExporter(); + + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); + String expected = readFile(new File("./src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd")); + assertEquals(expected, diagram.getDefinition()); + } + + @Test + public void test_dynamicViewThatDoeNotOverrideRelationshipDescriptions() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + a.uses(b, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(a, b); + + WebSequenceDiagramsExporter exporter = new WebSequenceDiagramsExporter(); + + Collection<Diagram> diagrams = exporter.export(workspace); + Diagram diagram = diagrams.iterator().next(); + assertEquals("title Dynamic - key\n" + + "\n" + + "participant <<Software System>>\\nA as A\n" + + "participant <<Software System>>\\nB as B\n" + + "\n" + + "A->B: Uses", diagram.getDefinition()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/resources/groups.json b/structurizr-export/src/test/resources/groups.json new file mode 100644 index 000000000..62c3303b8 --- /dev/null +++ b/structurizr-export/src/test/resources/groups.json @@ -0,0 +1,203 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "properties" : { + "structurizr.dsl" : "d29ya3NwYWNlIHsKCiAgICBtb2RlbCB7CiAgICAgICAgYSA9IHNvZnR3YXJlU3lzdGVtICJBIgogICAgICAgIAogICAgICAgIGdyb3VwICJHcm91cCAxIiB7CiAgICAgICAgICAgIGIgPSBzb2Z0d2FyZVN5c3RlbSAiQiIKICAgICAgICB9CgogICAgICAgIGVudGVycHJpc2UgIkVudGVycHJpc2UiIHsKICAgICAgICAgICAgYyA9IHNvZnR3YXJlU3lzdGVtICJDIgoKICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDIiIHsKICAgICAgICAgICAgICAgIGQgPSBzb2Z0d2FyZVN5c3RlbSAiRCIgewogICAgICAgICAgICAgICAgICAgIGUgPSBjb250YWluZXIgIkUiCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDMiIHsKICAgICAgICAgICAgICAgICAgICAgICAgZiA9IGNvbnRhaW5lciAiRiIgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgZyA9IGNvbXBvbmVudCAiRyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDQiIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoID0gY29tcG9uZW50ICJIIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIAogICAgICAgIGEgLT4gYgogICAgICAgIGIgLT4gYwogICAgICAgIGMgLT4gZQogICAgICAgIGMgLT4gZwogICAgICAgIGMgLT4gaAoKICAgIH0KICAgIAogICAgdmlld3MgewogICAgICAgIHN5c3RlbWxhbmRzY2FwZSAiU3lzdGVtTGFuZHNjYXBlIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhdXRvbGF5b3V0CiAgICAgICAgfQogICAgICAgIAogICAgICAgIGNvbnRhaW5lciBkICJDb250YWluZXJzIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhdXRvbGF5b3V0CiAgICAgICAgfQoKICAgICAgICBjb21wb25lbnQgZiAiQ29tcG9uZW50cyIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYXV0b2xheW91dAogICAgICAgIH0KICAgIH0KCn0K" + }, + "configuration" : { }, + "model" : { + "enterprise" : { + "name" : "Enterprise" + }, + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "A", + "relationships" : [ { + "id" : "9", + "tags" : "Relationship", + "sourceId" : "1", + "destinationId" : "2" + } ], + "location" : "External" + }, { + "id" : "2", + "tags" : "Element,Software System", + "name" : "B", + "relationships" : [ { + "id" : "10", + "tags" : "Relationship", + "sourceId" : "2", + "destinationId" : "3" + } ], + "group" : "Group 1", + "location" : "External" + }, { + "id" : "3", + "tags" : "Element,Software System", + "name" : "C", + "relationships" : [ { + "id" : "11", + "tags" : "Relationship", + "sourceId" : "3", + "destinationId" : "5" + }, { + "id" : "12", + "tags" : "Relationship", + "sourceId" : "3", + "destinationId" : "4" + }, { + "id" : "13", + "tags" : "Relationship", + "sourceId" : "3", + "destinationId" : "7" + }, { + "id" : "14", + "tags" : "Relationship", + "sourceId" : "3", + "destinationId" : "6" + }, { + "id" : "15", + "tags" : "Relationship", + "sourceId" : "3", + "destinationId" : "8" + } ], + "location" : "Internal" + }, { + "id" : "4", + "tags" : "Element,Software System", + "name" : "D", + "group" : "Group 2", + "location" : "Internal", + "containers" : [ { + "id" : "5", + "tags" : "Element,Container", + "name" : "E" + }, { + "id" : "6", + "tags" : "Element,Container", + "name" : "F", + "group" : "Group 3", + "components" : [ { + "id" : "8", + "tags" : "Element,Component", + "name" : "H", + "group" : "Group 4", + "size" : 0 + }, { + "id" : "7", + "tags" : "Element,Component", + "name" : "G", + "size" : 0 + } ] + } ] + } ] + }, + "documentation" : { }, + "views" : { + "systemLandscapeViews" : [ { + "key" : "SystemLandscape", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false + }, + "enterpriseBoundaryVisible" : true, + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "2", + "x" : 0, + "y" : 0 + }, { + "id" : "3", + "x" : 0, + "y" : 0 + }, { + "id" : "4", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "12" + }, { + "id" : "9" + }, { + "id" : "10" + } ] + } ], + "containerViews" : [ { + "softwareSystemId" : "4", + "key" : "Containers", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false + }, + "externalSoftwareSystemBoundariesVisible" : true, + "elements" : [ { + "id" : "3", + "x" : 0, + "y" : 0 + }, { + "id" : "5", + "x" : 0, + "y" : 0 + }, { + "id" : "6", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "14" + }, { + "id" : "11" + } ] + } ], + "componentViews" : [ { + "key" : "Components", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false + }, + "containerId" : "6", + "externalContainerBoundariesVisible" : true, + "elements" : [ { + "id" : "3", + "x" : 0, + "y" : 0 + }, { + "id" : "7", + "x" : 0, + "y" : 0 + }, { + "id" : "8", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "15" + }, { + "id" : "13" + } ] + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-export/src/test/resources/structurizr-36141-workspace.json b/structurizr-export/src/test/resources/structurizr-36141-workspace.json new file mode 100644 index 000000000..0a52ae906 --- /dev/null +++ b/structurizr-export/src/test/resources/structurizr-36141-workspace.json @@ -0,0 +1,1999 @@ +{ + "id": 36141, + "name": "Big Bank plc", + "description": "This is an example workspace to illustrate the key features of Structurizr, based around a fictional online banking system.", + "model": { + "enterprise": { + "name": "Big Bank plc" + }, + "people": [ + { + "id": "15", + "tags": "Element,Person,Bank Staff", + "name": "Back Office Staff", + "description": "Administration and support staff within the bank.", + "relationships": [ + { + "id": "16", + "tags": "Relationship", + "sourceId": "15", + "destinationId": "4", + "description": "Uses" + } + ], + "location": "Internal" + }, + { + "id": "12", + "tags": "Element,Person,Bank Staff", + "name": "Customer Service Staff", + "description": "Customer service staff within the bank.", + "relationships": [ + { + "id": "13", + "tags": "Relationship", + "sourceId": "12", + "destinationId": "4", + "description": "Uses" + } + ], + "location": "Internal" + }, + { + "id": "1", + "tags": "Element,Person", + "name": "Personal Banking Customer", + "description": "A customer of the bank, with personal bank accounts.", + "relationships": [ + { + "id": "3", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "2", + "description": "Views account balances, and makes payments using" + }, + { + "id": "23", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "17", + "description": "Views account balances, and makes payments using" + }, + { + "id": "22", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "19", + "description": "Visits bigbank.com/ib using", + "technology": "HTTPS" + }, + { + "id": "24", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "18", + "description": "Views account balances, and makes payments using" + }, + { + "id": "11", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "9", + "description": "Withdraws cash using" + }, + { + "id": "14", + "tags": "Relationship,Synchronous", + "sourceId": "1", + "destinationId": "12", + "description": "Asks questions to", + "technology": "Telephone", + "interactionStyle": "Synchronous" + } + ], + "location": "External" + } + ], + "softwareSystems": [ + { + "id": "9", + "tags": "Element,Software System,Existing System", + "name": "ATM", + "description": "Allows customers to withdraw cash.", + "relationships": [ + { + "id": "10", + "tags": "Relationship", + "sourceId": "9", + "destinationId": "4", + "description": "Uses" + } + ], + "location": "Internal" + }, + { + "id": "6", + "tags": "Element,Software System,Existing System", + "name": "E-mail System", + "description": "The internal Microsoft Exchange e-mail system.", + "relationships": [ + { + "id": "8", + "tags": "Relationship", + "sourceId": "6", + "destinationId": "1", + "description": "Sends e-mails to" + } + ], + "location": "Internal" + }, + { + "id": "2", + "tags": "Element,Software System", + "name": "Internet Banking System", + "description": "Allows customers to view information about their bank accounts, and make payments.", + "relationships": [ + { + "id": "5", + "tags": "Relationship", + "sourceId": "2", + "destinationId": "4", + "description": "Gets account information from, and makes payments using" + }, + { + "id": "7", + "tags": "Relationship", + "sourceId": "2", + "destinationId": "6", + "description": "Sends e-mail using" + } + ], + "location": "Internal", + "containers": [ + { + "id": "20", + "tags": "Element,Container", + "name": "API Application", + "description": "Provides Internet banking functionality via a JSON/HTTPS API.", + "relationships": [ + { + "id": "26", + "tags": "Relationship", + "sourceId": "20", + "destinationId": "21", + "description": "Reads from and writes to", + "technology": "JDBC" + }, + { + "id": "27", + "tags": "Relationship", + "sourceId": "20", + "destinationId": "4", + "description": "Makes API calls to", + "technology": "XML/HTTPS" + }, + { + "id": "28", + "tags": "Relationship", + "sourceId": "20", + "destinationId": "6", + "description": "Sends e-mail using", + "technology": "SMTP" + } + ], + "technology": "Java and Spring MVC", + "components": [ + { + "id": "30", + "tags": "Element,Component", + "name": "Accounts Summary Controller", + "description": "Provides customers with a summary of their bank accounts.", + "relationships": [ + { + "id": "44", + "tags": "Relationship", + "sourceId": "30", + "destinationId": "33", + "description": "Uses" + } + ], + "technology": "Spring MVC Rest Controller", + "size": 0 + }, + { + "id": "34", + "tags": "Element,Component", + "name": "E-mail Component", + "description": "Sends e-mails to users.", + "relationships": [ + { + "id": "49", + "tags": "Relationship", + "sourceId": "34", + "destinationId": "6", + "description": "Sends e-mail using" + } + ], + "technology": "Spring Bean", + "size": 0 + }, + { + "id": "33", + "tags": "Element,Component", + "name": "Mainframe Banking System Facade", + "description": "A facade onto the mainframe banking system.", + "relationships": [ + { + "id": "48", + "tags": "Relationship", + "sourceId": "33", + "destinationId": "4", + "description": "Uses", + "technology": "XML/HTTPS" + } + ], + "technology": "Spring Bean", + "size": 0 + }, + { + "id": "31", + "tags": "Element,Component", + "name": "Reset Password Controller", + "description": "Allows users to reset their passwords with a single use URL.", + "relationships": [ + { + "id": "45", + "tags": "Relationship", + "sourceId": "31", + "destinationId": "32", + "description": "Uses" + }, + { + "id": "46", + "tags": "Relationship", + "sourceId": "31", + "destinationId": "34", + "description": "Uses" + } + ], + "technology": "Spring MVC Rest Controller", + "size": 0 + }, + { + "id": "32", + "tags": "Element,Component", + "name": "Security Component", + "description": "Provides functionality related to signing in, changing passwords, etc.", + "relationships": [ + { + "id": "47", + "tags": "Relationship", + "sourceId": "32", + "destinationId": "21", + "description": "Reads from and writes to", + "technology": "JDBC" + } + ], + "technology": "Spring Bean", + "size": 0 + }, + { + "id": "29", + "tags": "Element,Component", + "name": "Sign In Controller", + "description": "Allows users to sign in to the Internet Banking System.", + "relationships": [ + { + "id": "43", + "tags": "Relationship", + "sourceId": "29", + "destinationId": "32", + "description": "Uses" + } + ], + "technology": "Spring MVC Rest Controller", + "size": 0 + } + ] + }, + { + "id": "21", + "tags": "Element,Container,Database", + "name": "Database", + "description": "Stores user registration information, hashed authentication credentials, access logs, etc.", + "technology": "Oracle Database Schema" + }, + { + "id": "18", + "tags": "Element,Container,Mobile App", + "name": "Mobile App", + "description": "Provides a limited subset of the Internet banking functionality to customers via their mobile device.", + "relationships": [ + { + "id": "39", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "29", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "41", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "31", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "42", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "30", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "40", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "20", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + } + ], + "technology": "Xamarin" + }, + { + "id": "17", + "tags": "Element,Container,Web Browser", + "name": "Single-Page Application", + "description": "Provides all of the Internet banking functionality to customers via their web browser.", + "relationships": [ + { + "id": "38", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "30", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "36", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "20", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "37", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "31", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "35", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "29", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + } + ], + "technology": "JavaScript and Angular" + }, + { + "id": "19", + "tags": "Element,Container", + "name": "Web Application", + "description": "Delivers the static content and the Internet banking single page application.", + "relationships": [ + { + "id": "25", + "tags": "Relationship", + "sourceId": "19", + "destinationId": "17", + "description": "Delivers to the customer's web browser" + } + ], + "technology": "Java and Spring MVC" + } + ] + }, + { + "id": "4", + "tags": "Element,Software System,Existing System", + "name": "Mainframe Banking System", + "description": "Stores all of the core banking information about customers, accounts, transactions, etc.", + "location": "Internal" + } + ], + "deploymentNodes": [ + { + "id": "55", + "tags": "Element,Deployment Node", + "name": "Big Bank plc", + "environment": "Development", + "technology": "Big Bank plc data center", + "instances": 1, + "children": [ + { + "id": "56", + "tags": "Element,Deployment Node", + "name": "bigbank-dev001", + "environment": "Development", + "instances": 1, + "softwareSystemInstances": [ + { + "id": "57", + "tags": "Software System Instance", + "environment": "Development", + "instanceId": 1, + "softwareSystemId": "4", + "properties": {} + } + ], + "children": [], + "containerInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "50", + "tags": "Element,Deployment Node", + "name": "Developer Laptop", + "description": "A developer laptop.", + "environment": "Development", + "technology": "Microsoft Windows 10 or Apple macOS", + "instances": 1, + "children": [ + { + "id": "59", + "tags": "Element,Deployment Node", + "name": "Docker Container - Database Server", + "description": "A Docker container.", + "environment": "Development", + "technology": "Docker", + "instances": 1, + "children": [ + { + "id": "60", + "tags": "Element,Deployment Node", + "name": "Database Server", + "description": "A development database.", + "environment": "Development", + "technology": "Oracle 12c", + "instances": 1, + "containerInstances": [ + { + "id": "61", + "tags": "Container Instance", + "environment": "Development", + "instanceId": 1, + "containerId": "21", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "51", + "tags": "Element,Deployment Node", + "name": "Docker Container - Web Server", + "description": "A Docker container.", + "environment": "Development", + "technology": "Docker", + "instances": 1, + "children": [ + { + "id": "52", + "tags": "Element,Deployment Node", + "properties": { + "Java Version": "8", + "Xms": "1024M", + "Xmx": "512M" + }, + "name": "Apache Tomcat", + "description": "An open source Java EE web server.", + "environment": "Development", + "technology": "Apache Tomcat 8.x", + "instances": 1, + "containerInstances": [ + { + "id": "54", + "tags": "Container Instance", + "relationships": [ + { + "id": "62", + "sourceId": "54", + "destinationId": "61", + "description": "Reads from and writes to", + "technology": "JDBC", + "linkedRelationshipId": "26" + }, + { + "id": "58", + "sourceId": "54", + "destinationId": "57", + "description": "Makes API calls to", + "technology": "XML/HTTPS", + "linkedRelationshipId": "27" + } + ], + "environment": "Development", + "instanceId": 1, + "containerId": "20", + "properties": {} + }, + { + "id": "53", + "tags": "Container Instance", + "relationships": [ + { + "id": "66", + "sourceId": "53", + "destinationId": "64", + "description": "Delivers to the customer's web browser", + "linkedRelationshipId": "25" + } + ], + "environment": "Development", + "instanceId": 1, + "containerId": "19", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "63", + "tags": "Element,Deployment Node", + "name": "Web Browser", + "environment": "Development", + "technology": "Chrome, Firefox, Safari, or Edge", + "instances": 1, + "containerInstances": [ + { + "id": "64", + "tags": "Container Instance", + "relationships": [ + { + "id": "65", + "sourceId": "64", + "destinationId": "54", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "36" + } + ], + "environment": "Development", + "instanceId": 1, + "containerId": "17", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "72", + "tags": "Element,Deployment Node", + "name": "Big Bank plc", + "environment": "Live", + "technology": "Big Bank plc data center", + "instances": 1, + "children": [ + { + "id": "79", + "tags": "Element,Deployment Node", + "properties": { + "Location": "London and Reading" + }, + "name": "bigbank-api***", + "description": "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 8, + "children": [ + { + "id": "80", + "tags": "Element,Deployment Node", + "properties": { + "Java Version": "8", + "Xms": "1024M", + "Xmx": "512M" + }, + "name": "Apache Tomcat", + "description": "An open source Java EE web server.", + "environment": "Live", + "technology": "Apache Tomcat 8.x", + "instances": 1, + "containerInstances": [ + { + "id": "81", + "tags": "Container Instance", + "relationships": [ + { + "id": "88", + "sourceId": "81", + "destinationId": "87", + "description": "Reads from and writes to", + "technology": "JDBC", + "linkedRelationshipId": "26" + }, + { + "id": "84", + "sourceId": "81", + "destinationId": "74", + "description": "Makes API calls to", + "technology": "XML/HTTPS", + "linkedRelationshipId": "27" + }, + { + "id": "92", + "tags": "Failover", + "sourceId": "81", + "destinationId": "91", + "description": "Reads from and writes to", + "technology": "JDBC", + "linkedRelationshipId": "26" + } + ], + "environment": "Live", + "instanceId": 1, + "containerId": "20", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "85", + "tags": "Element,Deployment Node", + "properties": { + "Location": "London" + }, + "name": "bigbank-db01", + "description": "The primary database server.", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 1, + "children": [ + { + "id": "86", + "tags": "Element,Deployment Node", + "name": "Oracle - Primary", + "description": "The primary, live database server.", + "relationships": [ + { + "id": "93", + "tags": "Relationship,Synchronous", + "sourceId": "86", + "destinationId": "90", + "description": "Replicates data to", + "interactionStyle": "Synchronous" + } + ], + "environment": "Live", + "technology": "Oracle 12c", + "instances": 1, + "containerInstances": [ + { + "id": "87", + "tags": "Container Instance", + "environment": "Live", + "instanceId": 1, + "containerId": "21", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "89", + "tags": "Element,Deployment Node,Failover", + "properties": { + "Location": "Reading" + }, + "name": "bigbank-db02", + "description": "The secondary database server.", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 1, + "children": [ + { + "id": "90", + "tags": "Element,Deployment Node,Failover", + "name": "Oracle - Secondary", + "description": "A secondary, standby database server, used for failover purposes only.", + "environment": "Live", + "technology": "Oracle 12c", + "instances": 1, + "containerInstances": [ + { + "id": "91", + "tags": "Container Instance,Failover", + "environment": "Live", + "instanceId": 2, + "containerId": "21", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "73", + "tags": "Element,Deployment Node", + "name": "bigbank-prod001", + "environment": "Live", + "instances": 1, + "softwareSystemInstances": [ + { + "id": "74", + "tags": "Software System Instance", + "environment": "Live", + "instanceId": 1, + "softwareSystemId": "4", + "properties": {} + } + ], + "children": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "75", + "tags": "Element,Deployment Node", + "properties": { + "Location": "London and Reading" + }, + "name": "bigbank-web***", + "description": "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 4, + "children": [ + { + "id": "76", + "tags": "Element,Deployment Node", + "properties": { + "Java Version": "8", + "Xms": "1024M", + "Xmx": "512M" + }, + "name": "Apache Tomcat", + "description": "An open source Java EE web server.", + "environment": "Live", + "technology": "Apache Tomcat 8.x", + "instances": 1, + "containerInstances": [ + { + "id": "77", + "tags": "Container Instance", + "relationships": [ + { + "id": "78", + "sourceId": "77", + "destinationId": "71", + "description": "Delivers to the customer's web browser", + "linkedRelationshipId": "25" + } + ], + "environment": "Live", + "instanceId": 1, + "containerId": "19", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "69", + "tags": "Element,Deployment Node", + "name": "Customer's computer", + "environment": "Live", + "technology": "Microsoft Windows or Apple macOS", + "instances": 1, + "children": [ + { + "id": "70", + "tags": "Element,Deployment Node", + "name": "Web Browser", + "environment": "Live", + "technology": "Chrome, Firefox, Safari, or Edge", + "instances": 1, + "containerInstances": [ + { + "id": "71", + "tags": "Container Instance", + "relationships": [ + { + "id": "83", + "sourceId": "71", + "destinationId": "81", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "36" + } + ], + "environment": "Live", + "instanceId": 1, + "containerId": "17", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "67", + "tags": "Element,Deployment Node", + "name": "Customer's mobile device", + "environment": "Live", + "technology": "Apple iOS or Android", + "instances": 1, + "containerInstances": [ + { + "id": "68", + "tags": "Container Instance", + "relationships": [ + { + "id": "82", + "sourceId": "68", + "destinationId": "81", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "40" + } + ], + "environment": "Live", + "instanceId": 1, + "containerId": "18", + "properties": {} + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ] + }, + "documentation": { + "sections": [ + { + "elementId": "2", + "title": "Context", + "order": 1, + "format": "Markdown", + "content": "Here is some context about the Internet Banking System...\n![](embed:SystemLandscape)\n![](embed:SystemContext)\n### Internet Banking System\n...\n### Mainframe Banking System\n...\n" + }, + { + "elementId": "19", + "title": "Components", + "order": 3, + "format": "Markdown", + "content": "Here is some information about the API Application...\n![](embed:Components)\n### Sign in process\nHere is some information about the Sign In Controller, including how the sign in process works...\n![](embed:SignIn)" + }, + { + "elementId": "2", + "title": "Development Environment", + "order": 4, + "format": "AsciiDoc", + "content": "Here is some information about how to set up a development environment for the Internet Banking System...\nimage::embed:DevelopmentDeployment[]" + }, + { + "elementId": "2", + "title": "Containers", + "order": 2, + "format": "Markdown", + "content": "Here is some information about the containers within the Internet Banking System...\n![](embed:Containers)\n### Web Application\n...\n### Database\n...\n" + }, + { + "elementId": "2", + "title": "Deployment", + "order": 5, + "format": "AsciiDoc", + "content": "Here is some information about the live deployment environment for the Internet Banking System...\nimage::embed:LiveDeployment[]" + } + ], + "template": { + "name": "Software Guidebook", + "author": "Simon Brown", + "url": "https://leanpub.com/visualising-software-architecture" + }, + "decisions": [], + "images": [] + }, + "views": { + "systemLandscapeViews": [ + { + "description": "The system landscape diagram for Big Bank plc.", + "key": "SystemLandscape", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "1", + "2", + "4", + "6" + ], + "relationships": [ + "3", + "5", + "7", + "8" + ] + }, + { + "order": 2, + "elements": [ + "9" + ], + "relationships": [ + "11", + "10" + ] + }, + { + "order": 3, + "elements": [ + "12", + "15" + ], + "relationships": [ + "13", + "14", + "16" + ] + } + ], + "enterpriseBoundaryVisible": true, + "elements": [ + { + "id": "1", + "x": 87, + "y": 643 + }, + { + "id": "12", + "x": 1947, + "y": 36 + }, + { + "id": "2", + "x": 1012, + "y": 813 + }, + { + "id": "4", + "x": 1922, + "y": 693 + }, + { + "id": "15", + "x": 1947, + "y": 1241 + }, + { + "id": "6", + "x": 1012, + "y": 1326 + }, + { + "id": "9", + "x": 1012, + "y": 301 + } + ], + "relationships": [ + { + "id": "16" + }, + { + "id": "3" + }, + { + "id": "14", + "vertices": [ + { + "x": 285, + "y": 240 + } + ] + }, + { + "id": "5" + }, + { + "id": "13" + }, + { + "id": "11" + }, + { + "id": "7" + }, + { + "id": "8" + }, + { + "id": "10" + } + ] + } + ], + "systemContextViews": [ + { + "softwareSystemId": "2", + "description": "The system context diagram for the Internet Banking System.", + "key": "SystemContext", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "2" + ], + "relationships": [] + }, + { + "order": 2, + "elements": [ + "1" + ], + "relationships": [ + "3" + ] + }, + { + "order": 3, + "elements": [ + "4" + ], + "relationships": [ + "5" + ] + }, + { + "order": 4, + "elements": [ + "6" + ], + "relationships": [ + "7", + "8" + ] + } + ], + "enterpriseBoundaryVisible": false, + "elements": [ + { + "id": "1", + "x": 632, + "y": 69 + }, + { + "id": "2", + "x": 607, + "y": 714 + }, + { + "id": "4", + "x": 607, + "y": 1259 + }, + { + "id": "6", + "x": 1422, + "y": 714 + } + ], + "relationships": [ + { + "id": "3" + }, + { + "id": "5" + }, + { + "id": "7" + }, + { + "id": "8" + } + ] + } + ], + "containerViews": [ + { + "softwareSystemId": "2", + "description": "The container diagram for the Internet Banking System.", + "key": "Containers", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "1", + "4", + "6" + ], + "relationships": [ + "8" + ] + }, + { + "order": 2, + "elements": [ + "19" + ], + "relationships": [ + "22" + ] + }, + { + "order": 3, + "elements": [ + "17" + ], + "relationships": [ + "23", + "25" + ] + }, + { + "order": 4, + "elements": [ + "18" + ], + "relationships": [ + "24" + ] + }, + { + "order": 5, + "elements": [ + "20" + ], + "relationships": [ + "36", + "27", + "28", + "40" + ] + }, + { + "order": 6, + "elements": [ + "21" + ], + "relationships": [ + "26" + ] + } + ], + "externalSoftwareSystemBoundariesVisible": false, + "elements": [ + { + "id": "1", + "x": 1056, + "y": 24 + }, + { + "id": "4", + "x": 2012, + "y": 1214 + }, + { + "id": "17", + "x": 780, + "y": 664 + }, + { + "id": "6", + "x": 2012, + "y": 664 + }, + { + "id": "18", + "x": 1283, + "y": 664 + }, + { + "id": "19", + "x": 37, + "y": 664 + }, + { + "id": "20", + "x": 1031, + "y": 1214 + }, + { + "id": "21", + "x": 37, + "y": 1214 + } + ], + "relationships": [ + { + "id": "28" + }, + { + "id": "27" + }, + { + "id": "26" + }, + { + "id": "25" + }, + { + "id": "36" + }, + { + "id": "40" + }, + { + "id": "24" + }, + { + "id": "23" + }, + { + "id": "22" + }, + { + "id": "8" + } + ] + } + ], + "componentViews": [ + { + "description": "The component diagram for the API Application.", + "key": "Components", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "4", + "17", + "6", + "18", + "21" + ], + "relationships": [] + }, + { + "order": 2, + "elements": [ + "29", + "32" + ], + "relationships": [ + "35", + "47", + "39", + "43" + ] + }, + { + "order": 3, + "elements": [ + "33", + "30" + ], + "relationships": [ + "44", + "48", + "38", + "42" + ] + }, + { + "order": 4, + "elements": [ + "34", + "31" + ], + "relationships": [ + "45", + "46", + "37", + "49", + "41" + ] + } + ], + "containerId": "20", + "externalContainerBoundariesVisible": false, + "elements": [ + { + "id": "33", + "x": 1925, + "y": 817 + }, + { + "id": "34", + "x": 1015, + "y": 817 + }, + { + "id": "4", + "x": 1925, + "y": 1307 + }, + { + "id": "17", + "x": 560, + "y": 10 + }, + { + "id": "6", + "x": 1015, + "y": 1307 + }, + { + "id": "18", + "x": 1470, + "y": 11 + }, + { + "id": "29", + "x": 105, + "y": 436 + }, + { + "id": "30", + "x": 1925, + "y": 436 + }, + { + "id": "31", + "x": 1015, + "y": 436 + }, + { + "id": "21", + "x": 105, + "y": 1307 + }, + { + "id": "32", + "x": 105, + "y": 817 + } + ], + "relationships": [ + { + "id": "41", + "position": 40 + }, + { + "id": "42", + "position": 40 + }, + { + "id": "43", + "position": 55 + }, + { + "id": "37", + "position": 45 + }, + { + "id": "35", + "position": 35 + }, + { + "id": "44", + "position": 50 + }, + { + "id": "45" + }, + { + "id": "46" + }, + { + "id": "47", + "position": 60 + }, + { + "id": "48" + }, + { + "id": "38", + "position": 85 + }, + { + "id": "49" + }, + { + "id": "39", + "position": 85 + } + ] + } + ], + "dynamicViews": [ + { + "description": "Summarises how the sign in feature works in the single-page application.", + "key": "SignIn", + "paperSize": "A5_Landscape", + "elementId": "20", + "relationships": [ + { + "id": "35", + "description": "Submits credentials to", + "order": "1", + "response": false, + "vertices": [ + { + "x": 1238, + "y": 236 + } + ], + "routing": "Curved", + "position": 50 + }, + { + "id": "43", + "description": "Validates credentials using", + "order": "2", + "response": false, + "vertices": [ + { + "x": 2065, + "y": 845 + } + ], + "routing": "Curved" + }, + { + "id": "47", + "description": "select * from users where username = ?", + "order": "3", + "response": false, + "vertices": [ + { + "x": 1218, + "y": 1416 + } + ], + "routing": "Curved" + }, + { + "id": "47", + "description": "Returns user data to", + "order": "4", + "response": true, + "vertices": [ + { + "x": 1240, + "y": 1220 + } + ], + "routing": "Curved" + }, + { + "id": "43", + "description": "Returns true if the hashed password matches", + "order": "5", + "response": true, + "vertices": [ + { + "x": 1828, + "y": 841 + } + ], + "routing": "Curved" + }, + { + "id": "35", + "description": "Sends back an authentication token to", + "order": "6", + "response": true, + "vertices": [ + { + "x": 1210, + "y": 450 + } + ], + "routing": "Curved" + } + ], + "elements": [ + { + "id": "17", + "x": 290, + "y": 192 + }, + { + "id": "29", + "x": 1720, + "y": 192 + }, + { + "id": "32", + "x": 1720, + "y": 1182 + }, + { + "id": "21", + "x": 290, + "y": 1182 + } + ] + } + ], + "deploymentViews": [ + { + "softwareSystemId": "2", + "description": "An example live deployment scenario for the Internet Banking System.", + "key": "LiveDeployment", + "paperSize": "A4_Landscape", + "environment": "Live", + "animations": [ + { + "order": 1, + "elements": [ + "69", + "70", + "71" + ] + }, + { + "order": 2, + "elements": [ + "67", + "68" + ] + }, + { + "order": 3, + "elements": [ + "77", + "79", + "80", + "81", + "72", + "75", + "76" + ], + "relationships": [ + "78", + "82", + "83" + ] + }, + { + "order": 4, + "elements": [ + "85", + "86", + "87" + ], + "relationships": [ + "88" + ] + }, + { + "order": 5, + "elements": [ + "89", + "90", + "91" + ], + "relationships": [ + "92", + "93" + ] + }, + { + "order": 6, + "elements": [ + "74" + ], + "relationships": [ + "84" + ] + } + ], + "elements": [ + { + "id": "77", + "x": 1504, + "y": 184 + }, + { + "id": "89", + "x": 0, + "y": 0 + }, + { + "id": "67", + "x": 0, + "y": 0 + }, + { + "id": "79", + "x": 0, + "y": 0 + }, + { + "id": "68", + "x": 424, + "y": 1071 + }, + { + "id": "69", + "x": 0, + "y": 0 + }, + { + "id": "90", + "x": 0, + "y": 0 + }, + { + "id": "91", + "x": 2584, + "y": 184 + }, + { + "id": "80", + "x": 0, + "y": 0 + }, + { + "id": "81", + "x": 1504, + "y": 1071 + }, + { + "id": "70", + "x": 0, + "y": 0 + }, + { + "id": "71", + "x": 424, + "y": 184 + }, + { + "id": "72", + "x": 0, + "y": 0 + }, + { + "id": "73", + "x": 0, + "y": 0 + }, + { + "id": "74", + "x": 2584, + "y": 1959 + }, + { + "id": "85", + "x": 0, + "y": 0 + }, + { + "id": "86", + "x": 0, + "y": 0 + }, + { + "id": "75", + "x": 0, + "y": 0 + }, + { + "id": "87", + "x": 2584, + "y": 1071 + }, + { + "id": "76", + "x": 0, + "y": 0 + } + ], + "relationships": [ + { + "id": "93" + }, + { + "id": "82" + }, + { + "id": "83" + }, + { + "id": "92" + }, + { + "id": "84" + }, + { + "id": "78" + }, + { + "id": "88" + } + ] + }, + { + "softwareSystemId": "2", + "description": "An example development deployment scenario for the Internet Banking System.", + "key": "DevelopmentDeployment", + "paperSize": "A5_Landscape", + "environment": "Development", + "animations": [ + { + "order": 1, + "elements": [ + "50", + "63", + "64" + ] + }, + { + "order": 2, + "elements": [ + "51", + "52", + "53", + "54" + ], + "relationships": [ + "66", + "65" + ] + }, + { + "order": 3, + "elements": [ + "59", + "60", + "61" + ], + "relationships": [ + "62" + ] + }, + { + "order": 4, + "elements": [ + "57" + ], + "relationships": [ + "58" + ] + } + ], + "elements": [ + { + "id": "55", + "x": 0, + "y": 0 + }, + { + "id": "56", + "x": 0, + "y": 0 + }, + { + "id": "57", + "x": 1827, + "y": 1236 + }, + { + "id": "59", + "x": 0, + "y": 0 + }, + { + "id": "60", + "x": 0, + "y": 0 + }, + { + "id": "61", + "x": 1827, + "y": 176 + }, + { + "id": "50", + "x": 0, + "y": 0 + }, + { + "id": "51", + "x": 0, + "y": 0 + }, + { + "id": "63", + "x": 0, + "y": 0 + }, + { + "id": "52", + "x": 0, + "y": 0 + }, + { + "id": "64", + "x": 152, + "y": 346 + }, + { + "id": "53", + "x": 989, + "y": 176 + }, + { + "id": "54", + "x": 989, + "y": 516 + } + ], + "relationships": [ + { + "id": "62", + "position": 50 + }, + { + "id": "65" + }, + { + "id": "66" + }, + { + "id": "58" + } + ] + } + ], + "configuration": { + "branding": {}, + "styles": { + "elements": [ + { + "tag": "Software System", + "background": "#1168bd", + "color": "#ffffff" + }, + { + "tag": "Container", + "background": "#438dd5", + "color": "#ffffff" + }, + { + "tag": "Component", + "background": "#85bbf0", + "color": "#000000" + }, + { + "tag": "Person", + "background": "#08427b", + "color": "#ffffff", + "fontSize": 22, + "shape": "Person" + }, + { + "tag": "Existing System", + "background": "#999999", + "color": "#ffffff" + }, + { + "tag": "Bank Staff", + "background": "#999999", + "color": "#ffffff" + }, + { + "tag": "Web Browser", + "shape": "WebBrowser" + }, + { + "tag": "Mobile App", + "shape": "MobileDeviceLandscape" + }, + { + "tag": "Database", + "shape": "Cylinder" + }, + { + "tag": "Failover", + "opacity": 25 + } + ], + "relationships": [ + { + "tag": "Failover", + "position": 70, + "opacity": 25 + } + ] + }, + "terminology": {}, + "lastSavedView": "SignIn", + "themes": [] + }, + "filteredViews": [] + } +} \ No newline at end of file diff --git a/structurizr-export/src/test/resources/structurizr-54915-workspace.json b/structurizr-export/src/test/resources/structurizr-54915-workspace.json new file mode 100644 index 000000000..28e62ac94 --- /dev/null +++ b/structurizr-export/src/test/resources/structurizr-54915-workspace.json @@ -0,0 +1,353 @@ +{ + "id": 54915, + "name": "Amazon Web Services Example", + "description": "An example AWS deployment architecture.", + "model": { + "softwareSystems": [ + { + "id": "1", + "tags": "Element,Software System", + "name": "Spring PetClinic", + "description": "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", + "location": "Unspecified", + "containers": [ + { + "id": "3", + "tags": "Element,Container,Database", + "name": "Database", + "description": "Stores information regarding the veterinarians, the clients, and their pets.", + "technology": "Relational database schema" + }, + { + "id": "2", + "tags": "Element,Container,Application", + "name": "Web Application", + "description": "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", + "relationships": [ + { + "id": "4", + "tags": "Relationship", + "sourceId": "2", + "destinationId": "3", + "description": "Reads from and writes to", + "technology": "MySQL Protocol/SSL" + } + ], + "technology": "Java and Spring Boot" + } + ], + "documentation": {} + } + ], + "deploymentNodes": [ + { + "id": "5", + "tags": "Element,Deployment Node,Amazon Web Services - Cloud", + "name": "Amazon Web Services", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "6", + "tags": "Element,Deployment Node,Amazon Web Services - Region", + "name": "US-East-1", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "12", + "tags": "Element,Deployment Node,Amazon Web Services - RDS", + "name": "Amazon RDS", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "13", + "tags": "Element,Deployment Node,Amazon Web Services - RDS MySQL instance", + "name": "MySQL", + "environment": "Live", + "instances": 1, + "containerInstances": [ + { + "id": "14", + "tags": "Container Instance", + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "3" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "9", + "tags": "Element,Deployment Node,Amazon Web Services - Auto Scaling", + "name": "Autoscaling group", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "10", + "tags": "Element,Deployment Node,Amazon Web Services - EC2", + "name": "Amazon EC2", + "environment": "Live", + "instances": 1, + "containerInstances": [ + { + "id": "11", + "tags": "Container Instance", + "relationships": [ + { + "id": "15", + "sourceId": "11", + "destinationId": "14", + "description": "Reads from and writes to", + "technology": "MySQL Protocol/SSL", + "linkedRelationshipId": "4" + } + ], + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "2" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + } + ], + "infrastructureNodes": [ + { + "id": "8", + "tags": "Element,Infrastructure Node,Amazon Web Services - Elastic Load Balancing", + "name": "Elastic Load Balancer", + "description": "Automatically distributes incoming application traffic.", + "relationships": [ + { + "id": "17", + "tags": "Relationship", + "sourceId": "8", + "destinationId": "11", + "description": "Forwards requests to", + "technology": "HTTPS" + } + ], + "environment": "Live" + }, + { + "id": "7", + "tags": "Element,Infrastructure Node,Amazon Web Services - Route 53", + "name": "Route 53", + "description": "Highly available and scalable cloud DNS service.", + "relationships": [ + { + "id": "16", + "tags": "Relationship", + "sourceId": "7", + "destinationId": "8", + "description": "Forwards requests to", + "technology": "HTTPS" + } + ], + "environment": "Live" + } + ], + "softwareSystemInstances": [], + "containerInstances": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + } + ], + "customElements": [], + "people": [] + }, + "documentation": { + "sections": [], + "decisions": [], + "images": [] + }, + "views": { + "deploymentViews": [ + { + "softwareSystemId": "1", + "key": "AmazonWebServicesDeployment", + "order": 1, + "paperSize": "A3_Landscape", + "dimensions": { + "width": 3925, + "height": 1816 + }, + "automaticLayout": { + "implementation": "Graphviz", + "rankDirection": "LeftRight", + "rankSeparation": 300, + "nodeSeparation": 300, + "edgeSeparation": 0, + "vertices": false + }, + "environment": "Live", + "animations": [ + { + "order": 1, + "elements": [ + "5", + "6", + "7" + ] + }, + { + "order": 2, + "elements": [ + "8" + ], + "relationships": [ + "16" + ] + }, + { + "order": 3, + "elements": [ + "11", + "9", + "10" + ], + "relationships": [ + "17" + ] + }, + { + "order": 4, + "elements": [ + "12", + "13", + "14" + ], + "relationships": [ + "15" + ] + } + ], + "elements": [ + { + "id": "11", + "x": 1987, + "y": 672 + }, + { + "id": "12", + "x": 175, + "y": 175 + }, + { + "id": "13", + "x": 175, + "y": 175 + }, + { + "id": "14", + "x": 2887, + "y": 672 + }, + { + "id": "5", + "x": 175, + "y": 175 + }, + { + "id": "6", + "x": 175, + "y": 175 + }, + { + "id": "7", + "x": 487, + "y": 672 + }, + { + "id": "8", + "x": 1237, + "y": 672 + }, + { + "id": "9", + "x": 175, + "y": 175 + }, + { + "id": "10", + "x": 175, + "y": 175 + } + ], + "relationships": [ + { + "id": "17" + }, + { + "id": "16" + }, + { + "id": "15" + } + ] + } + ], + "configuration": { + "branding": {}, + "styles": { + "elements": [ + { + "tag": "Element", + "background": "#ffffff", + "shape": "RoundedBox" + }, + { + "tag": "Container", + "background": "#ffffff" + }, + { + "tag": "Application", + "background": "#ffffff" + }, + { + "tag": "Database", + "shape": "Cylinder" + } + ], + "relationships": [] + }, + "themes": [ + "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json" + ], + "terminology": {}, + "lastSavedView": "AmazonWebServicesDeployment" + }, + "customViews": [], + "systemLandscapeViews": [], + "systemContextViews": [], + "containerViews": [], + "componentViews": [], + "dynamicViews": [], + "filteredViews": [] + } +} \ No newline at end of file diff --git a/structurizr-graphviz/README.md b/structurizr-graphviz/README.md new file mode 100644 index 000000000..bed0d0a76 --- /dev/null +++ b/structurizr-graphviz/README.md @@ -0,0 +1,26 @@ +# structurizr-graphviz + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-graphviz.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-graphviz) + +This library provides automatic facilities for Structurizr views. +It's a wrapper around the [Graphviz tool](http://www.graphviz.org), +which allows you to apply the Graphviz layout algorithm to the views in a Structurizr workspace. + +> You will need Graphviz installed. + +For example: + +```java +Workspace workspace = ... + +GraphvizAutomaticLayout graphviz = new GraphvizAutomaticLayout(); +graphviz.apply(workspace); +``` + +The ```structurizr-graphviz``` library does the following for every view in the workspace: + +1. Export the view to a DOT file. +2. Run Graphviz (via the ```dot``` command), with the output format set to SVG. +3. Parse the generated SVG to extract layout information, and apply this to the Structurizr view (element x,y positions, relationship vertices, and paper size). + +Once the layout has been applied, you can upload your workspace to the Structurizr cloud service/on-premises installation as usual. diff --git a/structurizr-graphviz/build.gradle b/structurizr-graphviz/build.gradle new file mode 100644 index 000000000..c40b607ba --- /dev/null +++ b/structurizr-graphviz/build.gradle @@ -0,0 +1,10 @@ +dependencies { + + api project(':structurizr-export') + + testImplementation project(':structurizr-client') + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + +} + +description = 'Automatic layout facilities for Structurizr views' \ No newline at end of file diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/Constants.java b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/Constants.java new file mode 100644 index 000000000..37340f9cb --- /dev/null +++ b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/Constants.java @@ -0,0 +1,17 @@ +package com.structurizr.graphviz; + +/** + * Some constants used when applying graphviz. + */ +class Constants { + + // diagrams created by the Structurizr cloud service/on-premises installation/Lite are sized for 300dpi + static final double STRUCTURIZR_DPI = 300.0; + + // graphviz uses 72dpi by default + private static final double GRAPHVIZ_DPI = 72.0; + + // this is needed to convert coordinates provided by graphviz, to those used by Structurizr + static final double DPI_RATIO = STRUCTURIZR_DPI / GRAPHVIZ_DPI; + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTDiagram.java b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTDiagram.java new file mode 100644 index 000000000..40b232858 --- /dev/null +++ b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTDiagram.java @@ -0,0 +1,17 @@ +package com.structurizr.graphviz; + +import com.structurizr.export.Diagram; +import com.structurizr.view.ModelView; + +class DOTDiagram extends Diagram { + + DOTDiagram(ModelView view, String definition) { + super(view, definition); + } + + @Override + public String getFileExtension() { + return "dot"; + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTExporter.java b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTExporter.java new file mode 100644 index 000000000..c3c73b6cc --- /dev/null +++ b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTExporter.java @@ -0,0 +1,620 @@ +package com.structurizr.graphviz; + +import com.structurizr.export.AbstractDiagramExporter; +import com.structurizr.export.Diagram; +import com.structurizr.export.IndentingWriter; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.DeploymentView; +import com.structurizr.view.ModelView; +import com.structurizr.view.RelationshipView; + +import java.util.Locale; + +/** + * Writes a Structurizr view to a graphviz dot file. Please note that this is not a full export (colours, shapes, etc); + * it just contains the basics required for layout purposes. + */ +class DOTExporter extends AbstractDiagramExporter { + + private static final int CLUSTER_INTERNAL_MARGIN = 25; + + private Locale locale = Locale.US; + private RankDirection rankDirection; + private double rankSeparation; + private double nodeSeparation; + + private int groupId = 1; + + DOTExporter(RankDirection rankDirection, double rankSeparation, double nodeSeparation) { + this.rankDirection = rankDirection; + this.rankSeparation = rankSeparation; + this.nodeSeparation = nodeSeparation; + } + + void setLocale(Locale locale) { + this.locale = locale; + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + if (view.getAutomaticLayout() != null) { + if (view.getAutomaticLayout().getRankDirection() == null) { + rankDirection = RankDirection.TopBottom; + } else { + switch (view.getAutomaticLayout().getRankDirection()) { + case TopBottom: + rankDirection = RankDirection.TopBottom; + break; + case BottomTop: + rankDirection = RankDirection.BottomTop; + break; + case LeftRight: + rankDirection = RankDirection.LeftRight; + break; + case RightLeft: + rankDirection = RankDirection.RightLeft; + break; + } + } + + rankSeparation = view.getAutomaticLayout().getRankSeparation(); + nodeSeparation = view.getAutomaticLayout().getNodeSeparation(); + } + + rankSeparation = rankSeparation / Constants.STRUCTURIZR_DPI; + nodeSeparation = nodeSeparation / Constants.STRUCTURIZR_DPI; + + writer.writeLine("digraph {"); + writer.indent(); + writer.writeLine("compound=true"); + writer.writeLine(String.format(locale, "graph [splines=polyline,rankdir=%s,ranksep=%s,nodesep=%s,fontsize=5]", rankDirection.getCode(), rankSeparation, nodeSeparation)); + writer.writeLine("node [shape=box,fontsize=5]"); + writer.writeLine("edge []"); + writer.writeLine(); + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + } + + @Override + protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { + writer.writeLine("subgraph cluster_enterprise {"); + writer.indent(); + writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + } + + @Override + protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + writer.writeLine("subgraph \"cluster_group_" + (groupId++) + "\" {"); + + writer.indent(); + writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + } + + @Override + protected void endGroupBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { + writer.writeLine(String.format("subgraph cluster_%s {", softwareSystem.getId())); + writer.indent(); + writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + } + + @Override + protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { + writer.writeLine(String.format("subgraph cluster_%s {", container.getId())); + writer.indent(); + writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + } + + @Override + protected void endContainerBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { + writer.writeLine(String.format("subgraph cluster_%s {", deploymentNode.getId())); + writer.indent(); + writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + } + + @Override + protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) { + writer.outdent(); + writer.writeLine("}"); + writer.writeLine(); + } + + @Override + protected void writeElement(ModelView view, Element element, IndentingWriter writer) { + writer.writeLine(String.format(locale, "%s [width=%f,height=%f,fixedsize=true,id=%s,label=\"%s: %s\"]", + element.getId(), + getElementWidth(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches + getElementHeight(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches + element.getId(), + element.getId(), + escape(element.getName()) + )); + } + + @Override + protected void writeRelationship(ModelView view, RelationshipView relationshipView, IndentingWriter writer) { + if (relationshipView.getRelationship().getSource() instanceof DeploymentNode || relationshipView.getRelationship().getDestination() instanceof DeploymentNode) { + Element source = relationshipView.getRelationship().getSource(); + if (source instanceof DeploymentNode) { + source = findElementInside((DeploymentNode)source, view); + } + + Element destination = relationshipView.getRelationship().getDestination(); + if (destination instanceof DeploymentNode) { + destination = findElementInside((DeploymentNode)destination, view); + } + + if (source != null && destination != null) { + String clusterConfig = ""; + + if (relationshipView.getRelationship().getSource() instanceof DeploymentNode) { + clusterConfig += ",ltail=cluster_" + relationshipView.getRelationship().getSource().getId(); + } + + if (relationshipView.getRelationship().getDestination() instanceof DeploymentNode) { + clusterConfig += ",lhead=cluster_" + relationshipView.getRelationship().getDestination().getId(); + } + + writer.writeLine(String.format(locale, "%s -> %s [id=%s%s]", + source.getId(), + destination.getId(), + relationshipView.getId(), + clusterConfig + )); + } + } else { + Element source = relationshipView.getRelationship().getSource(); + Element destination = relationshipView.getRelationship().getDestination(); + + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + source = relationshipView.getRelationship().getDestination(); + destination = relationshipView.getRelationship().getSource(); + } + + writer.writeLine(String.format(locale, "%s -> %s [id=%s]", + source.getId(), + destination.getId(), + relationshipView.getId() + )); + } + } + + @Override + protected Diagram createDiagram(ModelView view, String definition) { + return new DOTDiagram(view, definition); + } + +// private void write(ModelView view, boolean enterpriseBoundaryIsVisible) throws Exception { +// File file = new File(path, view.getKey() + ".dot"); +// FileWriter fileWriter = new FileWriter(file); +// writeHeader(fileWriter, view); +// +// if (enterpriseBoundaryIsVisible) { +// fileWriter.write(" subgraph cluster_enterprise {\n"); +// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// Set<GroupableElement> elementsInsideEnterpriseBoundary = new LinkedHashSet<>(); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() == Location.Internal) { +// elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); +// } +// if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() == Location.Internal) { +// elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); +// } +// } +// writeElements(view, " ", elementsInsideEnterpriseBoundary, fileWriter); +// fileWriter.write(" }\n\n"); +// +// Set<GroupableElement> elementsOutsideEnterpriseBoundary = new LinkedHashSet<>(); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() != Location.Internal) { +// elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); +// } +// if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() != Location.Internal) { +// elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); +// } +// if (elementView.getElement() instanceof CustomElement) { +// elementsOutsideEnterpriseBoundary.add((CustomElement)elementView.getElement()); +// } +// } +// +// writeElements(view, " ", elementsOutsideEnterpriseBoundary, fileWriter); +// } else { +// Set<GroupableElement> elements = new LinkedHashSet<>(); +// for (ElementView elementView : view.getElements()) { +// elements.add((GroupableElement)elementView.getElement()); +// } +// writeElements(view, " ", elements, fileWriter); +// } +// +// writeRelationships(view, fileWriter); +// writeFooter(fileWriter); +// fileWriter.close(); +// } +// +// void write(ContainerView view) throws Exception { +// File file = new File(path, view.getKey() + ".dot"); +// FileWriter fileWriter = new FileWriter(file); +// writeHeader(fileWriter, view); +// +// Set<SoftwareSystem> softwareSystems = new HashSet<>(); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() instanceof SoftwareSystem) { +// softwareSystems.add((SoftwareSystem)elementView.getElement().getParent()); +// } +// } +// List<SoftwareSystem> sortedSoftwareSystems = new ArrayList<>(softwareSystems); +// sortedSoftwareSystems.sort(Comparator.comparing(Element::getId)); +// +// for (SoftwareSystem softwareSystem : sortedSoftwareSystems) { +// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", softwareSystem.getId())); +// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// +// Set<GroupableElement> scopedElements = new LinkedHashSet<>(); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() == softwareSystem) { +// scopedElements.add((StaticStructureElement) elementView.getElement()); +// } +// } +// writeElements(view, " ", scopedElements, fileWriter); +// fileWriter.write(" }\n"); +// +// } +// +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() == null) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } +// +// writeRelationships(view, fileWriter); +// +// writeFooter(fileWriter); +// fileWriter.close(); +// } +// +// void write(ComponentView view) throws Exception { +// File file = new File(path, view.getKey() + ".dot"); +// FileWriter fileWriter = new FileWriter(file); +// writeHeader(fileWriter, view); +// +// Set<Container> containers = new HashSet<>(); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() instanceof Container) { +// containers.add((Container)elementView.getElement().getParent()); +// } +// } +// List<Container> sortedContainers = new ArrayList<>(containers); +// sortedContainers.sort(Comparator.comparing(Element::getId)); +// +// for (Container container : sortedContainers) { +// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", container.getId())); +// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// +// Set<GroupableElement> scopedElements = new LinkedHashSet<>(); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() == container) { +// scopedElements.add((StaticStructureElement) elementView.getElement()); +// } +// } +// writeElements(view, " ", scopedElements, fileWriter); +// fileWriter.write(" }\n"); +// } +// +// for (ElementView elementView : view.getElements()) { +// if (!(elementView.getElement().getParent() instanceof Container)) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } +// +// writeRelationships(view, fileWriter); +// +// writeFooter(fileWriter); +// fileWriter.close(); +// } +// +// void write(DynamicView view) throws Exception { +// File file = new File(path, view.getKey() + ".dot"); +// FileWriter fileWriter = new FileWriter(file); +// writeHeader(fileWriter, view); +// +// Element element = view.getElement(); +// +// if (element == null) { +// for (ElementView elementView : view.getElements()) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } else if (element instanceof SoftwareSystem) { +// List<SoftwareSystem> softwareSystems = new ArrayList<>(view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Container).map(c -> ((Container)c).getSoftwareSystem()).collect(Collectors.toSet())); +// softwareSystems.sort(Comparator.comparing(Element::getId)); +// +// for (SoftwareSystem softwareSystem : softwareSystems) { +// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", softwareSystem.getId())); +// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() == softwareSystem) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } +// fileWriter.write(" }\n"); +// } +// +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() == null) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } +// } else if (element instanceof Container) { +// List<Container> containers = new ArrayList<>(view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Component).map(c -> ((Component)c).getContainer()).collect(Collectors.toSet())); +// containers.sort(Comparator.comparing(Element::getId)); +// +// for (Container container : containers) { +// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", container.getId())); +// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement().getParent() == container) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } +// fileWriter.write(" }\n"); +// } +// +// for (ElementView elementView : view.getElements()) { +// if (!(elementView.getElement().getParent() instanceof Container)) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } +// } +// +// writeRelationships(view, fileWriter); +// +// writeFooter(fileWriter); +// fileWriter.close(); +// } +// +// void write(DeploymentView view) throws Exception { +// File file = new File(path, view.getKey() + ".dot"); +// FileWriter fileWriter = new FileWriter(file); +// writeHeader(fileWriter, view); +// +// for (ElementView elementView : view.getElements()) { +// if (elementView.getElement() instanceof DeploymentNode && elementView.getElement().getParent() == null) { +// write(view, (DeploymentNode)elementView.getElement(), fileWriter, ""); +// } else if (elementView.getElement() instanceof CustomElement) { +// writeElement(view, " ", elementView.getElement(), fileWriter); +// } +// } +// +// writeRelationships(view, fileWriter); +// +// writeFooter(fileWriter); +// fileWriter.close(); +// } +// +// private void write(DeploymentView view, DeploymentNode deploymentNode, FileWriter fileWriter, String indent) throws Exception { +// fileWriter.write(String.format(locale, indent + "subgraph cluster_%s {\n", deploymentNode.getId())); +// fileWriter.write(indent + " margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// fileWriter.write(String.format(locale, indent + " label=\"%s: %s\"\n", deploymentNode.getId(), deploymentNode.getName())); +// +// for (DeploymentNode child : deploymentNode.getChildren()) { +// if (view.isElementInView(child)) { +// write(view, child, fileWriter, indent + " "); +// +// } +// } +// +// for (InfrastructureNode infrastructureNode : deploymentNode.getInfrastructureNodes()) { +// if (view.isElementInView(infrastructureNode)) { +// writeElement(view, indent + " ", infrastructureNode, fileWriter); +// } +// } +// +// for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { +// if (view.isElementInView(softwareSystemInstance)) { +// writeElement(view, indent + " ", softwareSystemInstance, fileWriter); +// } +// } +// +// for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { +// if (view.isElementInView(containerInstance)) { +// writeElement(view, indent + " ", containerInstance, fileWriter); +// } +// } +// +// fileWriter.write(indent + "}\n"); +// } +// +// private void writeElements(ModelView view, String padding, Set<GroupableElement> elements, Writer writer) throws Exception { +// String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); +// boolean nested = !StringUtils.isNullOrEmpty(groupSeparator); +// +// Set<String> groups = new HashSet<>(); +// for (GroupableElement element : elements) { +// String group = element.getGroup(); +// +// if (!StringUtils.isNullOrEmpty(group)) { +// groups.add(group); +// +// if (nested) { +// while (group.contains(groupSeparator)) { +// group = group.substring(0, group.lastIndexOf(groupSeparator)); +// groups.add(group); +// } +// } +// } +// } +// +// List<String> sortedGroups = new ArrayList<>(groups); +// sortedGroups.sort(String::compareTo); +// +// // first render grouped elements +// if (nested) { +// if (groups.size() > 0) { +// String context = ""; +// for (String group : sortedGroups) { +// int groupCount = group.split(groupSeparator).length; +// int contextCount = context.split(groupSeparator).length; +// +// if (groupCount > contextCount) { +// // moved from a to a/b +// // - increase padding +// padding = padding + INDENT; +// } else if (groupCount == contextCount) { +// // moved from a/b to a/c +// // - close off previous subgraph +// if (context.length() > 0) { +// writer.write(padding + "}\n"); +// } +// } else { +// // moved from a/b/c to a/b or a +// // - close off previous subgraphs +// // - close off current subgraph +// for (int i = 0; i < (contextCount - groupCount); i++) { +// writer.write(padding + "}\n"); +// padding = padding.substring(0, padding.length() - INDENT.length()); +// } +// writer.write(padding + "}\n"); +// } +// +// writer.write(padding + "subgraph cluster_group_" + groupId + " {\n"); +//// writer.write(padding + " // " + group + "\n"); +// writer.write(padding + " margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// for (GroupableElement element : elements) { +// if (group.equals(element.getGroup())) { +// writeElement(view, padding + INDENT, element, writer); +// } +// } +// groupId++; +// context = group; +// } +// +// int contextCount = context.split(groupSeparator).length; +// for (int i = 0; i < contextCount; i++) { +// writer.write(padding + "}\n"); +// padding = padding.substring(0, padding.length() - INDENT.length()); +// } +// } +// } else { +// for (String group : sortedGroups) { +// writer.write(padding + "subgraph cluster_group_" + groupId + " {\n"); +// writer.write(padding + " margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); +// for (GroupableElement element : elements) { +// if (group.equals(element.getGroup())) { +// writeElement(view, padding + INDENT, element, writer); +// } +// } +// writer.write(padding + "}\n"); +// groupId++; +// } +// } +// +// // then render ungrouped elements +// for (GroupableElement element : elements) { +// if (StringUtils.isNullOrEmpty(element.getGroup())) { +// writeElement(view, padding, element, writer); +// } +// } +// } +// +// private void writeElement(ModelView view, String padding, Element element, Writer writer) throws Exception { +// writer.write(String.format(locale, "%s%s [width=%f,height=%f,fixedsize=true,id=%s,label=\"%s: %s\"]", +// padding, +// element.getId(), +// getElementWidth(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches +// getElementHeight(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches +// element.getId(), +// element.getId(), +// escape(element.getName()) +// )); +// writer.write("\n"); +// } +// + private String escape(String s) { + if (StringUtils.isNullOrEmpty(s)) { + return s; + } else { + return s.replaceAll("\"", "\\\\\""); + } + } +// +// private void writeRelationships(ModelView view, Writer writer) throws Exception { +// writer.write("\n"); +// +// for (RelationshipView relationshipView : view.getRelationships()) { +// } +// } + + private Element findElementInside(DeploymentNode deploymentNode, ModelView view) { + for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { + if (view.isElementInView(softwareSystemInstance)) { + return softwareSystemInstance; + } + } + + for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { + if (view.isElementInView(containerInstance)) { + return containerInstance; + } + } + + for (InfrastructureNode infrastructureNode : deploymentNode.getInfrastructureNodes()) { + if (view.isElementInView(infrastructureNode)) { + return infrastructureNode; + } + } + + if (deploymentNode.hasChildren()) { + for (DeploymentNode child : deploymentNode.getChildren()) { + Element element = findElementInside(child, view); + + if (element != null) { + return element; + } + } + } + + return null; + } + + private int getElementWidth(ModelView view, String elementId) { + Element element = view.getModel().getElement(elementId); + return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getWidth(); + } + + private int getElementHeight(ModelView view, String elementId) { + Element element = view.getModel().getElement(elementId); + return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getHeight(); + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java new file mode 100644 index 000000000..defdc86f0 --- /dev/null +++ b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java @@ -0,0 +1,217 @@ +package com.structurizr.graphviz; + +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; + +/** + * Applies the graphviz automatic layout to views in a Structurizr workspace. + * + * Note: this class assumes that the "dot" command is available. + */ +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; + + private RankDirection rankDirection = RankDirection.TopBottom; + private double rankSeparation = 1.0; + private double nodeSeparation = 1.0; + + private int margin = 400; + private boolean changePaperSize = true; + + private Locale locale = Locale.US; + + public GraphvizAutomaticLayout() { + this(new File(".")); + } + + public GraphvizAutomaticLayout(File path) { + this.path = path; + } + + public void setRankDirection(RankDirection rankDirection) { + this.rankDirection = rankDirection; + } + + public void setRankSeparation(double rankSeparation) { + this.rankSeparation = rankSeparation; + } + + public void setNodeSeparation(double nodeSeparation) { + this.nodeSeparation = nodeSeparation; + } + + public void setMargin(int margin) { + this.margin = margin; + } + + public void setChangePaperSize(boolean changePaperSize) { + this.changePaperSize = changePaperSize; + } + + /** + * Sets the locale used when writing DOT files. + * + * @param locale a Locale instance + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + private DOTExporter createDOTExporter() { + DOTExporter exporter = new DOTExporter(rankDirection, rankSeparation, nodeSeparation); + exporter.setLocale(locale); + + return exporter; + } + + 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() { + return new SVGReader(path, margin, changePaperSize); + } + + private void runGraphviz(View view) throws Exception { + ProcessBuilder processBuilder = new ProcessBuilder().inheritIO(); + List<String> 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); + createSVGReader().parseAndApplyLayout(view); + } + + 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); + createSVGReader().parseAndApplyLayout(view); + } + + 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); + createSVGReader().parseAndApplyLayout(view); + } + + 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); + createSVGReader().parseAndApplyLayout(view); + } + + 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); + createSVGReader().parseAndApplyLayout(view); + } + + 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); + createSVGReader().parseAndApplyLayout(view); + } + + 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); + createSVGReader().parseAndApplyLayout(view); + } + + public void apply(Workspace workspace) throws Exception { + for (CustomView view : workspace.getViews().getCustomViews()) { + apply(view); + } + + for (SystemLandscapeView view : workspace.getViews().getSystemLandscapeViews()) { + apply(view); + } + + for (SystemContextView view : workspace.getViews().getSystemContextViews()) { + apply(view); + } + + for (ContainerView view : workspace.getViews().getContainerViews()) { + apply(view); + } + + for (ComponentView view : workspace.getViews().getComponentViews()) { + apply(view); + } + + for (DynamicView view : workspace.getViews().getDynamicViews()) { + apply(view); + } + + for (DeploymentView view : workspace.getViews().getDeploymentViews()) { + apply(view); + } + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/RankDirection.java b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/RankDirection.java new file mode 100644 index 000000000..4bbe7486e --- /dev/null +++ b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/RankDirection.java @@ -0,0 +1,23 @@ +package com.structurizr.graphviz; + +/** + * The various rank directions used by graphviz. + */ +public enum RankDirection { + + TopBottom("TB"), + BottomTop("BT"), + LeftRight("LR"), + RightLeft("RL"); + + private String code; + + RankDirection(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/SVGReader.java b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/SVGReader.java new file mode 100644 index 000000000..4fde3f242 --- /dev/null +++ b/structurizr-graphviz/src/main/java/com/structurizr/graphviz/SVGReader.java @@ -0,0 +1,203 @@ +package com.structurizr.graphviz; + +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; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import java.io.File; +import java.io.FileInputStream; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Parses an SVG file created by graphviz, extracts the layout information, and applies it to a Structurizr view. + */ +class SVGReader { + + private static final Log log = LogFactory.getLog(GraphvizAutomaticLayout.class); + + private final File path; + private final int margin; + private final boolean changePaperSize; + + SVGReader(File path, int margin, boolean changePaperSize) { + this.path = path; + this.margin = margin; + this.changePaperSize = changePaperSize; + } + + void parseAndApplyLayout(ModelView view) throws Exception { + File file = new File(path, view.getKey() + ".dot.svg"); + 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 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; + } + + String pointsAsString = nodeList.item(0).getAttributes().getNamedItem("points").getNodeValue(); + String[] points = pointsAsString.split(" "); + String[] coordinates = points[1].split(","); + + double x = Double.parseDouble(coordinates[0]) + transformX; + double y = Double.parseDouble(coordinates[1]) + transformY; + + elementView.setX((int) (x * Constants.DPI_RATIO)); + elementView.setY((int) (y * Constants.DPI_RATIO)); + + minimumX = Math.min(elementView.getX(), minimumX); + minimumY = Math.min(elementView.getY(), minimumY); + + ElementStyle style = view.getViewSet().getConfiguration().getStyles().findElementStyle(view.getModel().getElement(elementView.getId())); + + maximumX = Math.max(elementView.getX() + style.getWidth(), maximumX); + maximumY = Math.max(elementView.getY() + style.getHeight(), maximumY); + } + + 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; + } + + String dAsString = nodeList.item(0).getAttributes().getNamedItem("d").getNodeValue(); + String[] d = dAsString.split(" "); + + Set<Vertex> 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); + } + } + + // 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); + + 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; + } + } + } + + 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); + } + } + + // 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()); + } + } + + private int getElementWidth(ModelView view, String elementId) { + Element element = view.getModel().getElement(elementId); + return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getWidth(); + } + + private int getElementHeight(ModelView view, String elementId) { + Element element = view.getModel().getElement(elementId); + return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getHeight(); + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java new file mode 100644 index 000000000..b3ffc35de --- /dev/null +++ b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java @@ -0,0 +1,616 @@ +package com.structurizr.graphviz; + +import com.structurizr.Workspace; +import com.structurizr.export.Diagram; +import com.structurizr.model.*; +import com.structurizr.util.WorkspaceUtils; +import com.structurizr.view.*; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DOTExporterTests { + + @Test + public void test_writeCustomView() { + Workspace workspace = new Workspace("Name", ""); + CustomElement box1 = workspace.getModel().addCustomElement("Box 1"); + CustomElement box2 = workspace.getModel().addCustomElement("Box 2"); + box1.uses(box2, "Uses"); + + CustomView view = workspace.getViews().createCustomView("CustomView", "Title", "Description"); + view.add(box1); + view.add(box2); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box 1\"]\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: Box 2\"]\n" + + "\n" + + " 1 -> 2 [id=3]\n" + + "}", content); + } + + @Test + public void test_writeSystemLandscapeViewWithNoEnterpriseBoundary() { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setLocation(Location.External); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setLocation(Location.Internal); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + view.add(box); + view.setEnterpriseBoundaryVisible(false); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + + "\n" + + " 2 -> 3 [id=4]\n" + + "}", content); + } + + @Test + public void test_writeSystemLandscapeViewWithGroupedElements() throws Exception { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setGroup("External"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setGroup("Internal"); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + view.add(box); + view.setEnterpriseBoundaryVisible(false); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph \"cluster_group_1\" {\n" + + " margin=25\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_2\" {\n" + + " margin=25\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + + " }\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + "\n" + + " 2 -> 3 [id=4]\n" + + "}", content); + } + + @Test + public void test_writeSystemLandscapeViewWithNestedGroupedElements() throws Exception { + Workspace workspace = new Workspace("Name", ""); + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + a.setGroup("Enterprise 1/Department 1/Team 1"); + + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + b.setGroup("Enterprise 1/Department 1/Team 2"); + + SoftwareSystem c = workspace.getModel().addSoftwareSystem("C"); + c.setGroup("Enterprise 1/Department 2"); + + SoftwareSystem d = workspace.getModel().addSoftwareSystem("D"); + d.setGroup("Enterprise 2"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph \"cluster_group_1\" {\n" + + " margin=25\n" + + " subgraph \"cluster_group_2\" {\n" + + " margin=25\n" + + " subgraph \"cluster_group_3\" {\n" + + " margin=25\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: A\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_4\" {\n" + + " margin=25\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: B\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_5\" {\n" + + " margin=25\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: C\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_6\" {\n" + + " margin=25\n" + + " 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label=\"4: D\"]\n" + + " }\n" + + "\n" + + "\n" + + "}", content); + } + + @Test + public void test_writeSystemLandscapeViewWithNoEnterpriseBoundaryInGermanLocale() throws Exception { + // ranksep=1.0 was being output as ranksep=1,0 + Locale.setDefault(new Locale("de", "DE")); + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setLocation(Location.External); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setLocation(Location.Internal); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + view.add(box); + view.setEnterpriseBoundaryVisible(false); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + + "\n" + + " 2 -> 3 [id=4]\n" + + "}", content); + } + + @Test + public void test_writeSystemLandscapeViewWithAnEnterpriseBoundary() throws Exception { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setLocation(Location.External); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setLocation(Location.Internal); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + view.add(box); + view.setEnterpriseBoundaryVisible(true); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph cluster_enterprise {\n" + + " margin=25\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + + " }\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + + "\n" + + " 2 -> 3 [id=4]\n" + + "}", content); + } + + @Test + public void test_writeSystemContextViewWithNoEnterpriseBoundary() throws Exception { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setLocation(Location.External); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setLocation(Location.Internal); + user.uses(softwareSystem, "Uses"); + + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); + view.addAllElements(); + view.add(box); + view.setEnterpriseBoundaryVisible(false); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + + "\n" + + " 2 -> 3 [id=4]\n" + + "}", content); + } + + + @Test + public void test_writeSystemContextViewWithAnEnterpriseBoundary() throws Exception { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setLocation(Location.External); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setLocation(Location.Internal); + user.uses(softwareSystem, "Uses"); + + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); + view.addAllElements(); + view.add(box); + view.setEnterpriseBoundaryVisible(true); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph cluster_enterprise {\n" + + " margin=25\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + + " }\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + + "\n" + + " 2 -> 3 [id=4]\n" + + "}", content); + } + + @Test + public void test_writeSystemContextViewWithGroupedElements() throws Exception { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setGroup("External"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setGroup("Internal"); + user.uses(softwareSystem, "Uses"); + + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); + view.addAllElements(); + view.add(box); + view.setEnterpriseBoundaryVisible(false); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph \"cluster_group_1\" {\n" + + " margin=25\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_2\" {\n" + + " margin=25\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + + " }\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + "\n" + + " 2 -> 3 [id=4]\n" + + "}", content); + } + + @Test + public void test_writeContainerViewWithGroupedElementsInASingleSoftwareSystem() throws Exception { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + Container container1 = softwareSystem.addContainer("Container 1"); + container1.setGroup("Group 1"); + Container container2 = softwareSystem.addContainer("Container 2"); + container2.setGroup("Group 2"); + Container container3 = softwareSystem.addContainer("Container 3"); + + container1.uses(container2, "Uses"); + container2.uses(container3, "Uses"); + + ContainerView view = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); + view.addAllElements(); + view.add(box); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + "\n" + + " subgraph cluster_2 {\n" + + " margin=25\n" + + " subgraph \"cluster_group_1\" {\n" + + " margin=25\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Container 1\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_2\" {\n" + + " margin=25\n" + + " 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label=\"4: Container 2\"]\n" + + " }\n" + + "\n" + + " 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label=\"5: Container 3\"]\n" + + " }\n" + + "\n" + + " 3 -> 4 [id=6]\n" + + " 4 -> 5 [id=7]\n" + + "}", content); + } + + @Test + public void test_writeContainerViewWithGroupedElementsInMultipleSoftwareSystems() throws Exception { + Workspace workspace = new Workspace("Name", ""); + + SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + container1.setGroup("Group"); + + SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + container2.setGroup("Group"); + + container1.uses(container2, "Uses"); + + ContainerView view = workspace.getViews().createContainerView(softwareSystem1, "Containers", ""); + view.add(container1); + view.add(container2); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph cluster_1 {\n" + + " margin=25\n" + + " subgraph \"cluster_group_1\" {\n" + + " margin=25\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: Container 1\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " subgraph cluster_3 {\n" + + " margin=25\n" + + " subgraph \"cluster_group_2\" {\n" + + " margin=25\n" + + " 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label=\"4: Container 2\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " 2 -> 4 [id=5]\n" + + "}", content); + } + + @Test + public void test_writeComponentViewWithGroupedElements() throws Exception { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container"); + Component component1 = container.addComponent("Component 1", ""); + Component component2 = container.addComponent("Component 2", ""); + component2.setGroup("Group 2"); + Component component3 = container.addComponent("Component 3", ""); + component3.setGroup("Group 3"); + + component1.uses(component2, "Uses"); + component2.uses(component3, "Uses"); + + ComponentView view = workspace.getViews().createComponentView(container, "Components", ""); + view.addAllElements(); + view.add(box); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals("digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + + "\n" + + " subgraph cluster_3 {\n" + + " margin=25\n" + + " subgraph \"cluster_group_1\" {\n" + + " margin=25\n" + + " 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label=\"5: Component 2\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_2\" {\n" + + " margin=25\n" + + " 6 [width=1.500000,height=1.000000,fixedsize=true,id=6,label=\"6: Component 3\"]\n" + + " }\n" + + "\n" + + " 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label=\"4: Component 1\"]\n" + + " }\n" + + "\n" + + " 4 -> 5 [id=7]\n" + + " 5 -> 6 [id=8]\n" + + "}", content); + } + + @Test + public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSeparator() throws Exception { + Workspace workspace = new Workspace("Name", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + Container container1 = softwareSystem.addContainer("Container 1"); + container1.setGroup("Group 1"); + Container container2 = softwareSystem.addContainer("Container 2"); + container2.setGroup("Group 2"); + + ContainerView view = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); + view.addAllElements(); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + + String expectedResult = "digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph cluster_1 {\n" + + " margin=25\n" + + " subgraph \"cluster_group_1\" {\n" + + " margin=25\n" + + " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: Container 1\"]\n" + + " }\n" + + "\n" + + " subgraph \"cluster_group_2\" {\n" + + " margin=25\n" + + " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Container 2\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + "}"; + + assertEquals(expectedResult, content); + + // this should be the same + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + diagram = exporter.export(view); + + content = diagram.getDefinition(); + assertEquals(expectedResult, content); + } + + @Test + public void test_AmazonWebServicesExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("src/test/resources/structurizr-54915-workspace.json")); + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(workspace.getViews().getDeploymentViews().iterator().next()); + + String content = diagram.getDefinition(); + + String expectedResult = "digraph {\n" + + " compound=true\n" + + " graph [splines=polyline,rankdir=LR,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + + " node [shape=box,fontsize=5]\n" + + " edge []\n" + + "\n" + + " subgraph cluster_5 {\n" + + " margin=25\n" + + " subgraph cluster_6 {\n" + + " margin=25\n" + + " subgraph cluster_12 {\n" + + " margin=25\n" + + " subgraph cluster_13 {\n" + + " margin=25\n" + + " 14 [width=1.500000,height=1.000000,fixedsize=true,id=14,label=\"14: Database\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " 7 [width=1.500000,height=1.000000,fixedsize=true,id=7,label=\"7: Route 53\"]\n" + + " 8 [width=1.500000,height=1.000000,fixedsize=true,id=8,label=\"8: Elastic Load Balancer\"]\n" + + " subgraph cluster_9 {\n" + + " margin=25\n" + + " subgraph cluster_10 {\n" + + " margin=25\n" + + " 11 [width=1.500000,height=1.000000,fixedsize=true,id=11,label=\"11: Web Application\"]\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " }\n" + + "\n" + + " 11 -> 14 [id=15]\n" + + " 7 -> 8 [id=16]\n" + + " 8 -> 11 [id=17]\n" + + "}"; + + assertEquals(expectedResult, content); + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/GraphvizAutomaticLayoutTests.java b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/GraphvizAutomaticLayoutTests.java new file mode 100644 index 000000000..c92385bb4 --- /dev/null +++ b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/GraphvizAutomaticLayoutTests.java @@ -0,0 +1,49 @@ +package com.structurizr.graphviz; + +import com.structurizr.Workspace; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.Tags; +import com.structurizr.view.Shape; +import com.structurizr.view.SystemContextView; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GraphvizAutomaticLayoutTests { + + @Test + public void test() throws Exception { + Workspace workspace = new Workspace("Name", ""); + Person user = workspace.getModel().addPerson("User"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + user.uses(softwareSystem, "Uses"); + + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); + view.addAllElements(); + + workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.PERSON).shape(Shape.Person); + + assertEquals(0, view.getElementView(user).getX()); + assertEquals(0, view.getElementView(user).getY()); + assertEquals(0, view.getElementView(softwareSystem).getX()); + assertEquals(0, view.getElementView(softwareSystem).getY()); + + File tempDir = Files.createTempDirectory("graphviz").toFile(); + GraphvizAutomaticLayout graphviz = new GraphvizAutomaticLayout(tempDir); + graphviz.setRankSeparation(300); + graphviz.setNodeSeparation(300); + graphviz.setMargin(400); + + graphviz.apply(workspace); + + assertEquals(233, view.getElementView(user).getX()); + assertEquals(208, view.getElementView(user).getY()); + assertEquals(208, view.getElementView(softwareSystem).getX()); + assertEquals(908, view.getElementView(softwareSystem).getY()); + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java new file mode 100644 index 000000000..0c5ecc86b --- /dev/null +++ b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java @@ -0,0 +1,66 @@ +package com.structurizr.graphviz; + +import com.structurizr.Workspace; +import com.structurizr.model.Location; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.Tags; +import com.structurizr.view.PaperSize; +import com.structurizr.view.Shape; +import com.structurizr.view.SystemContextView; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class SVGReaderTests { + + private static final File PATH = new File("./src/test/resources/graphviz"); + + @Test + public void test_readView() throws Exception { + Workspace workspace = createWorkspace(); + SystemContextView view = workspace.getViews().getSystemContextViews().iterator().next(); + Person user = workspace.getModel().getPersonWithName("User"); + SoftwareSystem softwareSystem = workspace.getModel().getSoftwareSystemWithName("Software System"); + + assertEquals(0, view.getElementView(user).getX()); + assertEquals(0, view.getElementView(user).getY()); + + assertEquals(0, view.getElementView(softwareSystem).getX()); + assertEquals(0, view.getElementView(softwareSystem).getY()); + + assertNull(view.getPaperSize()); + + SVGReader svgReader = new SVGReader(PATH, 200, true); + svgReader.parseAndApplyLayout(view); + + assertEquals(PaperSize.A6_Portrait, view.getPaperSize()); + + assertEquals(254, view.getElementView(user).getX()); + assertEquals(108, view.getElementView(user).getY()); + + assertEquals(229, view.getElementView(softwareSystem).getX()); + assertEquals(808, view.getElementView(softwareSystem).getY()); + } + + private static Workspace createWorkspace() { + Workspace workspace = new Workspace("Name", ""); + Person user = workspace.getModel().addPerson("User", ""); + user.setLocation(Location.External); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setLocation(Location.Internal); + user.uses(softwareSystem, "Uses"); + + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); + view.addAllElements(); + view.setEnterpriseBoundaryVisible(true); + + workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.PERSON).shape(Shape.Person); + + return workspace; + } + +} \ No newline at end of file diff --git a/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot b/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot new file mode 100644 index 000000000..3766cb71a --- /dev/null +++ b/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot @@ -0,0 +1,14 @@ +digraph { + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph cluster_enterprise { + margin=25 + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: Software System"] + } + + 1 [width=1.333333,height=1.333333,fixedsize=true,id=1,label="1: User"] + + 1 -> 2 [id=3] +} \ No newline at end of file diff --git a/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot.svg b/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot.svg new file mode 100644 index 000000000..216272c76 --- /dev/null +++ b/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot.svg @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.42.3 (20191010.1750) + --> +<!-- Title: %3 Pages: 1 --> +<svg width="182pt" height="281pt" + viewBox="0.00 0.00 182.00 281.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 277)"> +<title>%3 + + +cluster_enterprise + + + + +2 + +2: Software System + + + +1 + +1: User + + + +1->2 + + + + + diff --git a/structurizr-graphviz/src/test/resources/structurizr-54915-workspace.json b/structurizr-graphviz/src/test/resources/structurizr-54915-workspace.json new file mode 100644 index 000000000..28e62ac94 --- /dev/null +++ b/structurizr-graphviz/src/test/resources/structurizr-54915-workspace.json @@ -0,0 +1,353 @@ +{ + "id": 54915, + "name": "Amazon Web Services Example", + "description": "An example AWS deployment architecture.", + "model": { + "softwareSystems": [ + { + "id": "1", + "tags": "Element,Software System", + "name": "Spring PetClinic", + "description": "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", + "location": "Unspecified", + "containers": [ + { + "id": "3", + "tags": "Element,Container,Database", + "name": "Database", + "description": "Stores information regarding the veterinarians, the clients, and their pets.", + "technology": "Relational database schema" + }, + { + "id": "2", + "tags": "Element,Container,Application", + "name": "Web Application", + "description": "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", + "relationships": [ + { + "id": "4", + "tags": "Relationship", + "sourceId": "2", + "destinationId": "3", + "description": "Reads from and writes to", + "technology": "MySQL Protocol/SSL" + } + ], + "technology": "Java and Spring Boot" + } + ], + "documentation": {} + } + ], + "deploymentNodes": [ + { + "id": "5", + "tags": "Element,Deployment Node,Amazon Web Services - Cloud", + "name": "Amazon Web Services", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "6", + "tags": "Element,Deployment Node,Amazon Web Services - Region", + "name": "US-East-1", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "12", + "tags": "Element,Deployment Node,Amazon Web Services - RDS", + "name": "Amazon RDS", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "13", + "tags": "Element,Deployment Node,Amazon Web Services - RDS MySQL instance", + "name": "MySQL", + "environment": "Live", + "instances": 1, + "containerInstances": [ + { + "id": "14", + "tags": "Container Instance", + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "3" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "9", + "tags": "Element,Deployment Node,Amazon Web Services - Auto Scaling", + "name": "Autoscaling group", + "environment": "Live", + "instances": 1, + "children": [ + { + "id": "10", + "tags": "Element,Deployment Node,Amazon Web Services - EC2", + "name": "Amazon EC2", + "environment": "Live", + "instances": 1, + "containerInstances": [ + { + "id": "11", + "tags": "Container Instance", + "relationships": [ + { + "id": "15", + "sourceId": "11", + "destinationId": "14", + "description": "Reads from and writes to", + "technology": "MySQL Protocol/SSL", + "linkedRelationshipId": "4" + } + ], + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "2" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + } + ], + "infrastructureNodes": [ + { + "id": "8", + "tags": "Element,Infrastructure Node,Amazon Web Services - Elastic Load Balancing", + "name": "Elastic Load Balancer", + "description": "Automatically distributes incoming application traffic.", + "relationships": [ + { + "id": "17", + "tags": "Relationship", + "sourceId": "8", + "destinationId": "11", + "description": "Forwards requests to", + "technology": "HTTPS" + } + ], + "environment": "Live" + }, + { + "id": "7", + "tags": "Element,Infrastructure Node,Amazon Web Services - Route 53", + "name": "Route 53", + "description": "Highly available and scalable cloud DNS service.", + "relationships": [ + { + "id": "16", + "tags": "Relationship", + "sourceId": "7", + "destinationId": "8", + "description": "Forwards requests to", + "technology": "HTTPS" + } + ], + "environment": "Live" + } + ], + "softwareSystemInstances": [], + "containerInstances": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + } + ], + "customElements": [], + "people": [] + }, + "documentation": { + "sections": [], + "decisions": [], + "images": [] + }, + "views": { + "deploymentViews": [ + { + "softwareSystemId": "1", + "key": "AmazonWebServicesDeployment", + "order": 1, + "paperSize": "A3_Landscape", + "dimensions": { + "width": 3925, + "height": 1816 + }, + "automaticLayout": { + "implementation": "Graphviz", + "rankDirection": "LeftRight", + "rankSeparation": 300, + "nodeSeparation": 300, + "edgeSeparation": 0, + "vertices": false + }, + "environment": "Live", + "animations": [ + { + "order": 1, + "elements": [ + "5", + "6", + "7" + ] + }, + { + "order": 2, + "elements": [ + "8" + ], + "relationships": [ + "16" + ] + }, + { + "order": 3, + "elements": [ + "11", + "9", + "10" + ], + "relationships": [ + "17" + ] + }, + { + "order": 4, + "elements": [ + "12", + "13", + "14" + ], + "relationships": [ + "15" + ] + } + ], + "elements": [ + { + "id": "11", + "x": 1987, + "y": 672 + }, + { + "id": "12", + "x": 175, + "y": 175 + }, + { + "id": "13", + "x": 175, + "y": 175 + }, + { + "id": "14", + "x": 2887, + "y": 672 + }, + { + "id": "5", + "x": 175, + "y": 175 + }, + { + "id": "6", + "x": 175, + "y": 175 + }, + { + "id": "7", + "x": 487, + "y": 672 + }, + { + "id": "8", + "x": 1237, + "y": 672 + }, + { + "id": "9", + "x": 175, + "y": 175 + }, + { + "id": "10", + "x": 175, + "y": 175 + } + ], + "relationships": [ + { + "id": "17" + }, + { + "id": "16" + }, + { + "id": "15" + } + ] + } + ], + "configuration": { + "branding": {}, + "styles": { + "elements": [ + { + "tag": "Element", + "background": "#ffffff", + "shape": "RoundedBox" + }, + { + "tag": "Container", + "background": "#ffffff" + }, + { + "tag": "Application", + "background": "#ffffff" + }, + { + "tag": "Database", + "shape": "Cylinder" + } + ], + "relationships": [] + }, + "themes": [ + "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json" + ], + "terminology": {}, + "lastSavedView": "AmazonWebServicesDeployment" + }, + "customViews": [], + "systemLandscapeViews": [], + "systemContextViews": [], + "containerViews": [], + "componentViews": [], + "dynamicViews": [], + "filteredViews": [] + } +} \ No newline at end of file diff --git a/structurizr-import/README.md b/structurizr-import/README.md new file mode 100644 index 000000000..210022b6c --- /dev/null +++ b/structurizr-import/README.md @@ -0,0 +1,11 @@ +# structurizr-import + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-import.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-import) + +This library includes a number of classes for importing the following into a Structurizr workspace: + +- Diagrams from PlantUML, Mermaid, and Kroki. +- Supplementary Markdown/AsciiDoc documentation. +- Architecture decision records (ADRs). +- Images (for use in the above). + diff --git a/structurizr-import/build.gradle b/structurizr-import/build.gradle new file mode 100644 index 000000000..bf562441f --- /dev/null +++ b/structurizr-import/build.gradle @@ -0,0 +1,9 @@ +dependencies { + + api project(':structurizr-core') + + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + +} + +description = 'Utilities to import diagrams and documentation into a Structurizr workspace' \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java new file mode 100644 index 000000000..04682876a --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java @@ -0,0 +1,33 @@ +package com.structurizr.importer.diagrams; + +import com.structurizr.view.View; +import com.structurizr.view.ViewSet; + +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractDiagramImporter { + + protected static final Map CONTENT_TYPES_BY_FORMAT = new HashMap<>(); + + protected static final String CONTENT_TYPE_IMAGE_PNG = "image/png"; + protected static final String CONTENT_TYPE_IMAGE_SVG = "image/svg+xml"; + + protected static final String PNG_FORMAT = "png"; + protected static final String SVG_FORMAT = "svg"; + + static { + CONTENT_TYPES_BY_FORMAT.put(PNG_FORMAT, CONTENT_TYPE_IMAGE_PNG); + CONTENT_TYPES_BY_FORMAT.put(SVG_FORMAT, CONTENT_TYPE_IMAGE_SVG); + } + + protected String getViewOrViewSetProperty(View view, String name) { + ViewSet views = view.getViewSet(); + + return + view.getProperties().getOrDefault(name, + views.getConfiguration().getProperties().get(name) + ); + } + +} diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiEncoder.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiEncoder.java new file mode 100644 index 000000000..22b2c4f38 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiEncoder.java @@ -0,0 +1,30 @@ +package com.structurizr.importer.diagrams.kroki; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.zip.Deflater; + +/** + * See https://docs.kroki.io/kroki/setup/encode-diagram/#java + */ +class KrokiEncoder { + + public String encode(String decoded) throws IOException { + byte[] bytes = Base64.getUrlEncoder().encode(compress(decoded.getBytes())); + return new String(bytes, StandardCharsets.UTF_8); + } + + private byte[] compress(byte[] source) throws IOException { + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); + deflater.setInput(source); + deflater.finish(); + + byte[] buffer = new byte[2048]; + int compressedLength = deflater.deflate(buffer); + byte[] result = new byte[compressedLength]; + System.arraycopy(buffer, 0, result, 0, compressedLength); + return result; + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java new file mode 100644 index 000000000..6c01587e4 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java @@ -0,0 +1,45 @@ +package com.structurizr.importer.diagrams.kroki; + +import com.structurizr.importer.diagrams.AbstractDiagramImporter; +import com.structurizr.util.StringUtils; +import com.structurizr.view.ImageView; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public class KrokiImporter extends AbstractDiagramImporter { + + private static final String KROKI_URL_PROPERTY = "kroki.url"; + private static final String KROKI_FORMAT_PROPERTY = "kroki.format"; + + public void importDiagram(ImageView view, String format, File file) throws Exception { + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + view.setTitle(file.getName()); + + importDiagram(view, format, content); + } + + public void importDiagram(ImageView view, String format, String content) throws Exception { + String krokiServer = getViewOrViewSetProperty(view, KROKI_URL_PROPERTY); + if (StringUtils.isNullOrEmpty(krokiServer)) { + throw new IllegalArgumentException("Please define a view/viewset property named " + KROKI_URL_PROPERTY + " to specify your Kroki server"); + } + + String imageFormat = getViewOrViewSetProperty(view, KROKI_FORMAT_PROPERTY); + if (StringUtils.isNullOrEmpty(imageFormat)) { + imageFormat = PNG_FORMAT; + } + + if (!imageFormat.equals(PNG_FORMAT) && !imageFormat.equals(SVG_FORMAT)) { + throw new IllegalArgumentException(String.format("Expected a format of %s or %s", PNG_FORMAT, SVG_FORMAT)); + } + + String encodedDiagram = new KrokiEncoder().encode(content); + String url = String.format("%s/%s/%s/%s", krokiServer, format, imageFormat, encodedDiagram); + + view.setContent(url); + view.setContentType(CONTENT_TYPES_BY_FORMAT.get(imageFormat)); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java new file mode 100644 index 000000000..8bbc4bc91 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java @@ -0,0 +1,15 @@ +package com.structurizr.importer.diagrams.mermaid; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Encodes a Mermaid diagram definition to base64 format, for use with image URLs, etc. + */ +public class MermaidEncoder { + + public String encode(String mermaidDefinition) { + return Base64.getUrlEncoder().encodeToString(mermaidDefinition.getBytes(StandardCharsets.UTF_8)); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java new file mode 100644 index 000000000..9168fff71 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java @@ -0,0 +1,50 @@ +package com.structurizr.importer.diagrams.mermaid; + +import com.structurizr.importer.diagrams.AbstractDiagramImporter; +import com.structurizr.util.StringUtils; +import com.structurizr.view.ImageView; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public class MermaidImporter extends AbstractDiagramImporter { + + private static final String MERMAID_URL_PROPERTY = "mermaid.url"; + private static final String MERMAID_FORMAT_PROPERTY = "mermaid.format"; + + public void importDiagram(ImageView view, File file) throws Exception { + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + view.setTitle(file.getName()); + + importDiagram(view, content); + } + + public void importDiagram(ImageView view, String content) { + String mermaidServer = getViewOrViewSetProperty(view, MERMAID_URL_PROPERTY); + if (StringUtils.isNullOrEmpty(mermaidServer)) { + throw new IllegalArgumentException("Please define a view/viewset property named " + MERMAID_URL_PROPERTY + " to specify your Mermaid server"); + } + + String format = getViewOrViewSetProperty(view, MERMAID_FORMAT_PROPERTY); + if (StringUtils.isNullOrEmpty(format)) { + format = PNG_FORMAT; + } + + if (!format.equals(PNG_FORMAT) && !format.equals(SVG_FORMAT)) { + throw new IllegalArgumentException(String.format("Expected a format of %s or %s", PNG_FORMAT, SVG_FORMAT)); + } + + String encodedMermaid = new MermaidEncoder().encode(content); + String url; + if (format.equals(PNG_FORMAT)) { + url = String.format("%s/img/%s?type=png", mermaidServer, encodedMermaid); + } else { + url = String.format("%s/svg/%s", mermaidServer, encodedMermaid); + } + + view.setContent(url); + view.setContentType(CONTENT_TYPES_BY_FORMAT.get(format)); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java new file mode 100644 index 000000000..20238926b --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java @@ -0,0 +1,73 @@ +package com.structurizr.importer.diagrams.plantuml; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +/** + * A Java implementation of http://plantuml.com/code-javascript-synchronous + * that uses Java's built-in Deflate algorithm. + */ +class PlantUMLEncoder { + + String encode(String plantUMLDefinition) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true); + + DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, true); + dos.write(plantUMLDefinition.getBytes(StandardCharsets.UTF_8)); + dos.finish(); + + return encode(baos.toByteArray()); + } + + private String encode(byte[] bytes) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < bytes.length; i += 3) { + int b1 = (bytes[i]) & 0xFF; + int b2 = (i + 1 < bytes.length ? bytes[i + 1] : (byte)0) & 0xFF; + int b3 = (i + 2 < bytes.length ? bytes[i + 2] : (byte)0) & 0xFF; + + append3bytes(buf, b1, b2, b3); + } + + return buf.toString(); + } + + private char encode6bit(byte b) { + if (b < 10) { + return (char) ('0' + b); + } + b -= 10; + if (b < 26) { + return (char) ('A' + b); + } + b -= 26; + if (b < 26) { + return (char) ('a' + b); + } + b -= 26; + if (b == 0) { + return '-'; + } + if (b == 1) { + return '_'; + } + + return '?'; + } + + private void append3bytes(StringBuilder buf, int b1, int b2, int b3) { + int c1 = b1 >> 2; + int c2 = (b1 & 0x3) << 4 | b2 >> 4; + int c3 = (b2 & 0xF) << 2 | b3 >> 6; + int c4 = b3 & 0x3F; + + buf.append(encode6bit((byte)(c1 & 0x3F))); + buf.append(encode6bit((byte)(c2 & 0x3F))); + buf.append(encode6bit((byte)(c3 & 0x3F))); + buf.append(encode6bit((byte)(c4 & 0x3F))); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java new file mode 100644 index 000000000..56ed5a835 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -0,0 +1,54 @@ +package com.structurizr.importer.diagrams.plantuml; + +import com.structurizr.importer.diagrams.AbstractDiagramImporter; +import com.structurizr.util.StringUtils; +import com.structurizr.view.ImageView; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public class PlantUMLImporter extends AbstractDiagramImporter { + + private static final String PLANTUML_URL_PROPERTY = "plantuml.url"; + private static final String PLANTUML_FORMAT_PROPERTY = "plantuml.format"; + private static final String TITLE_STRING = "title "; + private static final String NEWLINE = "\n"; + + public void importDiagram(ImageView view, File file) throws Exception { + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + view.setTitle(file.getName()); + + importDiagram(view, content); + } + + public void importDiagram(ImageView view, String content) throws Exception { + String plantUMLServer = getViewOrViewSetProperty(view, PLANTUML_URL_PROPERTY); + if (StringUtils.isNullOrEmpty(plantUMLServer)) { + throw new IllegalArgumentException("Please define a view/viewset property named " + PLANTUML_URL_PROPERTY + " to specify your PlantUML server"); + } + + String format = getViewOrViewSetProperty(view, PLANTUML_FORMAT_PROPERTY); + if (StringUtils.isNullOrEmpty(format)) { + format = PNG_FORMAT; + } + + if (!format.equals(PNG_FORMAT) && !format.equals(SVG_FORMAT)) { + throw new IllegalArgumentException(String.format("Expected a format of %s or %s", PNG_FORMAT, SVG_FORMAT)); + } + + String encodedPlantUML = new PlantUMLEncoder().encode(content); + String url = String.format("%s/%s/%s", plantUMLServer, format, encodedPlantUML); + view.setContent(url); + view.setContentType(CONTENT_TYPES_BY_FORMAT.get(format)); + + String[] lines = content.split(NEWLINE); + for (String line : lines) { + if (line.startsWith(TITLE_STRING)) { + String title = line.substring(TITLE_STRING.length()); + view.setTitle(title); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java new file mode 100644 index 000000000..0a14e90e3 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java @@ -0,0 +1,237 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Format; +import com.structurizr.util.StringUtils; + +import java.io.File; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Imports architecture decision records created/managed by adr-tools (https://github.com/npryce/adr-tools). + * The format for ADRs is as follows: + * + * Filename: {DECISION_ID:0000}-*.md + * + * Content: + * # {DECISION_ID}. {DECISION_TITLE} + * + * Date: {DECISION_DATE:YYYY-MM-DD} + * + * ## Status + * + * {DECISION_STATUS and links} + * + * ## Context + * ... + */ +public class AdrToolsDecisionImporter implements DocumentationImporter { + + private static final String STATUS_PROPOSED = "Proposed"; + private static final String STATUS_SUPERSEDED = "Superseded"; + private static final String SUPERCEDED_ALTERNATIVE_SPELLING = "Superceded"; + + private static final String DATE_PREFIX = "Date: "; + private static final String STATUS_HEADING = "## Status"; + private static final String CONTEXT_HEADING = "## Context"; + + private static final Pattern LINK_REGEX = Pattern.compile("(.*) \\[.*]\\((.*)\\)"); + + private String dateFormat = "yyyy-MM-dd"; + private TimeZone timeZone = TimeZone.getDefault(); + + /** + * Sets the date format to use when parsing dates (the default is "yyyy-MM-dd"). + * + * @param dateFormat a date format, as a String + */ + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + + /** + * Sets the time zone to use when parsing dates (the default is UTC) + * + * @param timeZone a time zone as a String (e.g. "Europe/London" or "UTC") + */ + public void setTimeZone(String timeZone) { + this.timeZone = TimeZone.getTimeZone(timeZone); + } + + /** + * Sets the time zone to use when parsing dates. + * + * @param timeZone a TimeZone instance + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Imports Markdown files from the specified path, one per decision. + * + * @param documentable the item that documentation should be associated with + * @param path the path to import documentation from + */ + @Override + public void importDocumentation(Documentable documentable, File path) { + if (documentable == null) { + throw new IllegalArgumentException("A workspace, software system, container, or component must be specified."); + } + + if (path == null) { + throw new IllegalArgumentException("A path must be specified."); + } else if (!path.exists()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " does not exist."); + } + + if (!path.isDirectory()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " is not a directory."); + } + + try { + Map decisionsById = new LinkedHashMap<>(); + + File[] markdownFiles = path.listFiles((dir, name) -> name.endsWith(".md")); + if (markdownFiles != null) { + Map decisionsByFilename = new HashMap<>(); + + for (File file : markdownFiles) { + Decision decision = importDecision(file); + documentable.getDocumentation().addDecision(decision); + + decisionsById.put(decision.getId(), decision); + decisionsByFilename.put(file.getName(), decision); + } + + for (Decision decision : decisionsById.values()) { + extractLinks(decision, decisionsByFilename); + + // and replace file references, for example "0008-some-decision.md" -> "#8" + String content = decision.getContent(); + for (String filename : decisionsByFilename.keySet()) { + content = content.replace(filename, calculateUrl(decisionsByFilename.get(filename))); + } + decision.setContent(content); + } + } + } catch (Exception e) { + throw new DocumentationImportException(e); + } + } + + protected Decision importDecision(File file) throws Exception { + String id = extractIntegerIDFromFileName(file); + Decision decision = new Decision(id); + + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + content = content.replace("\r", ""); + decision.setContent(content); + + String[] lines = content.split("\\n"); + decision.setTitle(extractTitle(lines)); + decision.setDate(extractDate(lines)); + decision.setStatus(extractStatus(lines)); + decision.setFormat(Format.Markdown); + + return decision; + } + + protected String extractIntegerIDFromFileName(File file) { + return "" + Integer.parseInt(file.getName().substring(0, 4)); + } + + protected String extractTitle(String[] lines) { + // the title is assumed to be the first line of the content, in the format: + // # {DECISION_ID}. {DECISION_TITLE} + String titleLine = lines[0]; + + return titleLine.substring(titleLine.indexOf(".") + 2); + } + + protected Date extractDate(String[] lines) throws Exception { + // the date is on a line of its own, in the format: + // Date: {DECISION_DATE:YYYY-MM-DD} + SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); + sdf.setTimeZone(timeZone); + + for (String line : lines) { + if (line.startsWith(DATE_PREFIX)) { + String dateAsString = line.substring(DATE_PREFIX.length()); + + return sdf.parse(dateAsString); + } + } + + return new Date(); + } + + protected String extractStatus(String[] lines) { + // the status is on a line of its own, after the ## Status header: + boolean inStatusSection = false; + for (String line : lines) { + if (!inStatusSection) { + if (line.startsWith(STATUS_HEADING)) { + inStatusSection = true; + } + } else { + if (!StringUtils.isNullOrEmpty(line)) { + String status = line.split(" ")[0]; + // early versions of adr-tools used the alternative spelling + if (SUPERCEDED_ALTERNATIVE_SPELLING.equals(status)) { + status = STATUS_SUPERSEDED; + } + + return status; + } + } + } + + return STATUS_PROPOSED; + } + + protected void extractLinks(Decision decision, Map decisionsByFilename) { + // adr-tools allows users to create arbitrary links between ADRs, which reside inside the ## Status section + String[] lines = decision.getContent().split("\\n"); + boolean inStatusSection = false; + for (String line : lines) { + if (!inStatusSection) { + if (line.startsWith(STATUS_HEADING)) { + inStatusSection = true; + } + } else { + if (line.startsWith(CONTEXT_HEADING)) { + // we're done + return; + } else if (!StringUtils.isNullOrEmpty(line)) { + Matcher matcher = LINK_REGEX.matcher(line); + if (matcher.find()) { + String linkDescription = matcher.group(1); + String markdownFile = matcher.group(2); + + Decision targetDecision = decisionsByFilename.get(markdownFile); + if (targetDecision != null) { + decision.addLink(targetDecision, linkDescription); + } + } + } + } + } + } + + protected String calculateUrl(Decision decision) throws Exception { + return "#" + urlEncode(decision.getId()); + } + + protected String urlEncode(String value) throws Exception { + return URLEncoder.encode(value, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20"); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultDocumentationImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultDocumentationImporter.java new file mode 100644 index 000000000..1572a5ccd --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultDocumentationImporter.java @@ -0,0 +1,81 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; + +/** + * This implementation scans a given directory and automatically imports all Markdown or AsciiDoc + * files in that directory. + * + * See https://structurizr.com/help/documentation/headings for details of how section headings and numbering are handled. + */ +public class DefaultDocumentationImporter implements DocumentationImporter { + + /** + * Imports Markdown/AsciiDoc files from the specified path, each in its own section. + * + * @param documentable the item that documentation should be associated with + * @param path the path to import documentation from + */ + @Override + public void importDocumentation(Documentable documentable, File path) { + if (documentable == null) { + throw new IllegalArgumentException("A workspace, software system, container, or component must be specified."); + } + + if (path == null) { + throw new IllegalArgumentException("A path must be specified."); + } else if (!path.exists()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " does not exist."); + } + + try { + if (path.isDirectory()) { + File[] filesInDirectory = path.listFiles(); + if (filesInDirectory != null) { + Arrays.sort(filesInDirectory); + + for (File file : filesInDirectory) { + if (!file.isDirectory() && !file.getName().startsWith(".")) { + importFile(documentable, file); + } + } + } + } else { + importFile(documentable, path); + } + + // now trim the filenames + for (Section section : documentable.getDocumentation().getSections()) { + String filename = section.getFilename(); + + filename = filename.replace(path.getCanonicalPath(), ""); + if (filename.startsWith("/")) { + filename = filename.substring(1); + } + + section.setFilename(filename); + } + } catch (Exception e) { + throw new DocumentationImportException(e); + } + } + + protected void importFile(Documentable documentable, File file) throws Exception { + if (FormatFinder.isMarkdownOrAsciiDoc(file)) { + Format format = FormatFinder.findFormat(file); + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + + Section section = new Section(format, content); + section.setFilename(file.getCanonicalPath()); + documentable.getDocumentation().addSection(section); + } + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultImageImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultImageImporter.java new file mode 100644 index 000000000..f1cc2a3cd --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DefaultImageImporter.java @@ -0,0 +1,91 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Image; +import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; + +/** + * This implementation scans a given directory and automatically imports all Markdown or AsciiDoc + * files in that directory. + * + * - Each file must represent a separate documentation section. + * - The second level heading ("## Section Title" in Markdown and "== Section Title" in AsciiDoc) will be used as the section title. + */ +public class DefaultImageImporter implements DocumentationImporter { + + /** + * Imports one or more png/jpg/jpeg/gif images from the specified path. + * + * @param documentable the item that documentation should be associated with + * @param path the path to import images from + */ + public void importDocumentation(Documentable documentable, File path) { + if (documentable == null) { + throw new IllegalArgumentException("A workspace or software system must be specified."); + } + + if (path == null) { + throw new IllegalArgumentException("A path must be specified."); + } else if (!path.exists()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " does not exist."); + } + + try { + if (path.isDirectory()) { + importImages(documentable, "", path); + } else { + importImage(documentable, "", path); + } + } catch (Exception e) { + throw new DocumentationImportException(e.getMessage(), e); + } + } + + private void importImages(Documentable documentable, String root, File path) throws IOException { + File[] files = path.listFiles(); + if (files != null) { + for (File file : files) { + String name = file.getName().toLowerCase(); + if (file.isDirectory() && !file.isHidden()) { + if (StringUtils.isNullOrEmpty(root)) { + importImages(documentable, file.getName(), file); + } else { + importImages(documentable, root + "/" + file.getName(), file); + } + } else { + if (name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".gif") || name.endsWith(".svg")) { + importImage(documentable, root, file); + } + } + } + } + } + + private void importImage(Documentable documentable, String path, File file) throws IOException { + String contentType = ImageUtils.getContentType(file); + String base64Content; + + String name; + if (StringUtils.isNullOrEmpty(path)) { + name = file.getName(); + } else { + name = path + "/" + file.getName(); + } + + if (ImageUtils.CONTENT_TYPE_IMAGE_SVG.equalsIgnoreCase(contentType)) { + base64Content = Base64.getEncoder().encodeToString(Files.readAllBytes(file.toPath())); + } else { + contentType = ImageUtils.getContentType(file); + base64Content = ImageUtils.getImageAsBase64(file); + } + + documentable.getDocumentation().addImage(new Image(name, contentType, base64Content)); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImportException.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImportException.java new file mode 100644 index 000000000..3d068a298 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImportException.java @@ -0,0 +1,13 @@ +package com.structurizr.importer.documentation; + +public class DocumentationImportException extends RuntimeException { + + public DocumentationImportException(Throwable cause) { + super(cause); + } + + public DocumentationImportException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImporter.java new file mode 100644 index 000000000..ee006b3bc --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/DocumentationImporter.java @@ -0,0 +1,20 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Documentable; + +import java.io.File; + +/** + * An interface implemented by documentation importers. + */ +public interface DocumentationImporter { + + /** + * Imports documentation from the specified path. + * + * @param documentable the item that documentation should be associated with + * @param path the path to import documentation from + */ + void importDocumentation(Documentable documentable, File path); + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/FormatFinder.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/FormatFinder.java new file mode 100644 index 000000000..30e7f376c --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/FormatFinder.java @@ -0,0 +1,38 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Format; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class FormatFinder { + + private static final Set MARKDOWN_EXTENSIONS = new HashSet<>(Arrays.asList(".md", ".markdown", ".text")); + + private static final Set ASCIIDOC_EXTENSIONS = new HashSet<>(Arrays.asList(".asciidoc", ".adoc", ".asc")); + + public static boolean isMarkdownOrAsciiDoc(File file) { + String extension = file.getName().substring(file.getName().lastIndexOf(".")); + + return MARKDOWN_EXTENSIONS.contains(extension) || ASCIIDOC_EXTENSIONS.contains(extension); + } + + public static Format findFormat(File file) { + if (file == null) { + throw new IllegalArgumentException("A file must be specified."); + } + + String extension = file.getName().substring(file.getName().lastIndexOf(".")); + if (MARKDOWN_EXTENSIONS.contains(extension)) { + return Format.Markdown; + } else if (ASCIIDOC_EXTENSIONS.contains(extension)) { + return Format.AsciiDoc; + } else { + // just assume Markdown + return Format.Markdown; + } + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentationImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentationImporter.java new file mode 100644 index 000000000..26bfe6880 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentationImporter.java @@ -0,0 +1,70 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Section; + +import java.io.File; +import java.util.Arrays; + +/** + * This implementation extends the DefaultDocumentationImporter to recursively import documentation. + */ +public class RecursiveDefaultDocumentationImporter extends DefaultDocumentationImporter { + + /** + * Imports Markdown/AsciiDoc files from the specified path, each in its own section. + * + * @param documentable the item that documentation should be associated with + * @param path the path to import documentation from + */ + @Override + public void importDocumentation(Documentable documentable, File path) { + try { + if (documentable == null) { + throw new IllegalArgumentException("A workspace or software system must be specified"); + } + + if (path == null) { + throw new IllegalArgumentException("A path must be specified"); + } else if (!path.exists()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " does not exist"); + } + + if (path.isDirectory()) { + importDirectory(documentable, path); + } else { + importFile(documentable, path); + } + + // now trim the filenames + for (Section section : documentable.getDocumentation().getSections()) { + String filename = section.getFilename(); + + filename = filename.replace(path.getCanonicalPath(), ""); + if (filename.startsWith("/")) { + filename = filename.substring(1); + } + + section.setFilename(filename); + } + } catch (Exception e) { + throw new DocumentationImportException(e); + } + } + + private void importDirectory(Documentable documentable, File path) throws Exception { + File[] filesInDirectory = path.listFiles(); + if (filesInDirectory != null) { + Arrays.sort(filesInDirectory); + + for (File file : filesInDirectory) { + if (!file.isDirectory() && !file.getName().startsWith(".")) { + importFile(documentable, file); + } else if (file.isDirectory()) { + importDirectory(documentable, file); + } + } + } + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiEncoderTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiEncoderTests.java new file mode 100644 index 000000000..5d24e00ab --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiEncoderTests.java @@ -0,0 +1,68 @@ +package com.structurizr.importer.diagrams.kroki; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class KrokiEncoderTests { + + @Test + public void encode_plantuml() throws Exception { + assertEquals("eNpzKC5JLCopzc3hcspPUtC1U3DMyUxOVbBSyEjNycnnckjNSwFKAgD4CQzA", + new KrokiEncoder().encode("@startuml\n" + + "Bob -> Alice : hello\n" + + "@enduml")); + } + + @Test + public void encode_seqdiag() throws Exception { + assertEquals("eNorTi1MyUxMV6jmUlBIKsovL04tUlDQtVMoT00CMsuAvOicxKTUHAVbBSV31xAF_WKIBv3isnT9pMTiVDMTpVhroGaEBpD2gqL85NTi4nxk7c75eUDpEoWS1Aogka-QmZuYnoqu2UZXF6HZGslRIAm4MmuuWgA13z1R", + new KrokiEncoder().encode("seqdiag {\n" + + " browser -> webserver [label = \"GET /seqdiag/svg/base64\"];\n" + + " webserver -> processor [label = \"Convert text to image\"];\n" + + " webserver <-- processor;\n" + + " browser <-- webserver;\n" + + "}")); + } + + @Test + public void encode_erd() throws Exception { + assertEquals( + "eNqLDkgtKs7Pi-XSykvMTeXKSM1MzyjhKodQ2kmZRSUZ8Tn5yYklmfl58ZkpXFzRPlAeUAuQn5xZUslVXJJYksqVnF-aV1JUycUFMVJBS1fXUAGmGgCFAiQX", + new KrokiEncoder().encode("[Person]\n" + + "*name\n" + + "height\n" + + "weight\n" + + "+birth_location_id\n" + + "\n" + + "[Location]\n" + + "*id\n" + + "city\n" + + "state\n" + + "country\n" + + "\n" + + "Person *--1 Location")); + } + + @Test + public void encode_graphviz() throws Exception { + assertEquals( + "eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", + new KrokiEncoder().encode("digraph G {Hello->World}\n")); + } + + @Test + public void encode_blockdiag() throws Exception { + assertEquals( + "eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==", + new KrokiEncoder().encode("blockdiag {\n" + + " Kroki -> generates -> \"Block diagrams\";\n" + + " Kroki -> is -> \"very easy!\";\n" + + "\n" + + " Kroki [color = \"greenyellow\"];\n" + + " \"Block diagrams\" [color = \"pink\"];\n" + + " \"very easy!\" [color = \"orange\"];\n" + + "}")); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java new file mode 100644 index 000000000..cd3b0c6a8 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java @@ -0,0 +1,88 @@ +package com.structurizr.importer.diagrams.kroki; + +import com.structurizr.Workspace; +import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class KrokiImporterTests { + + @Test + public void importDiagram() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("kroki.url", "https://kroki.io"); + ImageView view = workspace.getViews().createImageView("key"); + + new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("diagram.dot", view.getTitle()); + assertEquals("https://kroki.io/graphviz/png/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + + @Test + public void importDiagram_AsPNG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("kroki.url", "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty("kroki.format", "png"); + ImageView view = workspace.getViews().createImageView("key"); + + new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("diagram.dot", view.getTitle()); + assertEquals("https://kroki.io/graphviz/png/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + + @Test + public void importDiagram_AsSVG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("kroki.url", "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty("kroki.format", "svg"); + ImageView view = workspace.getViews().createImageView("key"); + + new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("diagram.dot", view.getTitle()); + assertEquals("https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); + } + + @Test + public void importDiagram_WhenTheKrokiUrlIsNotDefined() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + ImageView view = workspace.getViews().createImageView("key"); + + try { + new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Please define a view/viewset property named kroki.url to specify your Kroki server", e.getMessage()); + } + } + + @Test + public void importDiagram_WhenAnInvalidFormatIsSpecified() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("kroki.url", "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty("kroki.format", "jpg"); + ImageView view = workspace.getViews().createImageView("key"); + + try { + new KrokiImporter().importDiagram(view, "graphviz", "..."); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Expected a format of png or svg", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java new file mode 100644 index 000000000..b249e45ff --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java @@ -0,0 +1,28 @@ +package com.structurizr.importer.diagrams.mermaid; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MermaidEncoderTests { + + @Test + public void encode_flowchart() throws Exception { + File file = new File("./src/test/resources/diagrams/mermaid/flowchart.mmd"); + String mermaid = Files.readString(file.toPath()); + assertEquals("Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==", new MermaidEncoder().encode(mermaid)); + } + + @Test + public void encode_class() throws Exception { + File file = new File("./src/test/resources/diagrams/mermaid/class.mmd"); + String mermaid = Files.readString(file.toPath()); + assertEquals("Y2xhc3NEaWFncmFtCiAgICBBbmltYWwgPHwtLSBEdWNrCiAgICBBbmltYWwgPHwtLSBGaXNoCiAgICBBbmltYWwgPHwtLSBaZWJyYQogICAgQW5pbWFsIDogK2ludCBhZ2UKICAgIEFuaW1hbCA6ICtTdHJpbmcgZ2VuZGVyCiAgICBBbmltYWw6ICtpc01hbW1hbCgpCiAgICBBbmltYWw6ICttYXRlKCkKICAgIGNsYXNzIER1Y2t7CiAgICAgICtTdHJpbmcgYmVha0NvbG9yCiAgICAgICtzd2ltKCkKICAgICAgK3F1YWNrKCkKICAgIH0KICAgIGNsYXNzIEZpc2h7CiAgICAgIC1pbnQgc2l6ZUluRmVldAogICAgICAtY2FuRWF0KCkKICAgIH0KICAgIGNsYXNzIFplYnJhewogICAgICArYm9vbCBpc193aWxkCiAgICAgICtydW4oKQogICAgfQo=", new MermaidEncoder().encode(mermaid)); + + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java new file mode 100644 index 000000000..3cccf9ac8 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java @@ -0,0 +1,88 @@ +package com.structurizr.importer.diagrams.mermaid; + +import com.structurizr.Workspace; +import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class MermaidImporterTests { + + @Test + public void importDiagram() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); + ImageView view = workspace.getViews().createImageView("key"); + + new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("flowchart.mmd", view.getTitle()); + assertEquals("https://mermaid.ink/img/Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==?type=png", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + + @Test + public void importDiagram_AsPNG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty("mermaid.format", "png"); + ImageView view = workspace.getViews().createImageView("key"); + + new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("flowchart.mmd", view.getTitle()); + assertEquals("https://mermaid.ink/img/Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==?type=png", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + + @Test + public void importDiagram_AsSVG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty("mermaid.format", "svg"); + ImageView view = workspace.getViews().createImageView("key"); + + new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("flowchart.mmd", view.getTitle()); + assertEquals("https://mermaid.ink/svg/Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); + } + + @Test + public void importDiagram_WhenTheMermaidUrlIsNotDefined() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + ImageView view = workspace.getViews().createImageView("key"); + + try { + new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Please define a view/viewset property named mermaid.url to specify your Mermaid server", e.getMessage()); + } + } + + @Test + public void importDiagram_WhenAnInvalidFormatIsSpecified() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty("mermaid.format", "jpg"); + ImageView view = workspace.getViews().createImageView("key"); + + try { + new MermaidImporter().importDiagram(view, "..."); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Expected a format of png or svg", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java new file mode 100644 index 000000000..867cf0bb2 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java @@ -0,0 +1,20 @@ +package com.structurizr.importer.diagrams.plantuml; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlantUMLEncoderTests { + + @Test + public void encode() throws Exception { + File file = new File("./src/test/resources/diagrams/plantuml/with-title.puml"); + String mermaid = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + assertEquals("SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", new PlantUMLEncoder().encode(mermaid)); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java new file mode 100644 index 000000000..7e51dcd50 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java @@ -0,0 +1,95 @@ +package com.structurizr.importer.diagrams.plantuml; + +import com.structurizr.Workspace; +import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class PlantUMLImporterTests { + + @Test + public void importDiagram_WhenATitleIsDefined() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); + ImageView view = workspace.getViews().createImageView("key"); + + new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("Sequence diagram example", view.getTitle()); + assertEquals("https://plantuml.com/plantuml/png/SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + + @Test + public void importDiagram_WhenATitleIsNotDefined() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); + ImageView view = workspace.getViews().createImageView("key"); + + new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/without-title.puml")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("without-title.puml", view.getTitle()); + assertEquals("https://plantuml.com/plantuml/png/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + + @Test + public void importDiagram_AsPNG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty("plantuml.format", "png"); + ImageView view = workspace.getViews().createImageView("key"); + + new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + assertEquals("https://plantuml.com/plantuml/png/SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + + @Test + public void importDiagram_AsSVG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty("plantuml.format", "svg"); + ImageView view = workspace.getViews().createImageView("key"); + + new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); + } + + @Test + public void importDiagram_WhenThePlantUMLURLIsNotSpecified() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + ImageView view = workspace.getViews().createImageView("key"); + + try { + new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Please define a view/viewset property named plantuml.url to specify your PlantUML server", e.getMessage()); + } + } + + @Test + public void importDiagram_WhenAnInvalidFormatIsSpecified() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty("plantuml.format", "jpg"); + ImageView view = workspace.getViews().createImageView("key"); + + try { + new PlantUMLImporter().importDiagram(view, "..."); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Expected a format of png or svg", e.getMessage()); + } + } + +} diff --git a/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java new file mode 100644 index 000000000..8b31bbbc0 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java @@ -0,0 +1,128 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Documentation; +import com.structurizr.documentation.Format; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import static org.junit.jupiter.api.Assertions.*; + + +public class AdrToolsDecisionImporterTests { + + private AdrToolsDecisionImporter decisionImporter; + private Workspace workspace; + private Documentation documentation; + + @BeforeEach + public void setUp() { + decisionImporter = new AdrToolsDecisionImporter(); + workspace = new Workspace("Name", "Description"); + documentation = workspace.getDocumentation(); + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenANullDocumentableIsSpecified() { + try { + decisionImporter.importDocumentation(null, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A workspace, software system, container, or component must be specified.", iae.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenADirectoryIsNotSpecified() { + try { + decisionImporter.importDocumentation(workspace, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A path must be specified.", iae.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenADirectoryIsSpecifiedButItDoesNotExist() { + try { + File directory = new File("foo"); + assertFalse(directory.exists()); + decisionImporter.importDocumentation(workspace, directory); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("foo does not exist.")); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenAPathIsSpecifiedButItIsNotADirectory() { + try { + decisionImporter.importDocumentation(workspace, new File("build.gradle")); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("/build.gradle is not a directory.")); + } + } + + @Test + public void test_importDecisions() { + decisionImporter.importDocumentation(workspace, new File("./src/test/resources/adrs")); + + assertEquals(9, documentation.getDecisions().size()); + + Decision decision1 = documentation.getDecisions().stream().filter(d -> d.getId().equals("1")).findFirst().get(); + assertEquals("1", decision1.getId()); + assertEquals("Record architecture decisions", decision1.getTitle()); + SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss ZZZ"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + assertEquals("12-Feb-2016 00:00:00 +0000", sdf.format(decision1.getDate())); + assertEquals("Accepted", decision1.getStatus()); + Assertions.assertEquals(Format.Markdown, decision1.getFormat()); + assertEquals("# 1. Record architecture decisions\n" + + "\n" + + "Date: 2016-02-12\n" + + "\n" + + "## Status\n" + + "\n" + + "Accepted\n" + + "\n" + + "## Context\n" + + "\n" + + "We need to record the architectural decisions made on this project.\n" + + "\n" + + "## Decision\n" + + "\n" + + "We will use Architecture Decision Records, as described by Michael Nygard in this article: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions\n" + + "\n" + + "## Consequences\n" + + "\n" + + "See Michael Nygard's article, linked above.\n", + decision1.getContent()); + } + + @Test + public void test_importDocumentation_CapturesLinksBetweenDecisions() { + decisionImporter.importDocumentation(workspace, new File("./src/test/resources/adrs")); + + Decision decision5 = documentation.getDecisions().stream().filter(d -> d.getId().equals("5")).findFirst().get(); + assertEquals(1, decision5.getLinks().size()); + Decision.Link link = decision5.getLinks().iterator().next(); + assertEquals("9", link.getId()); + assertEquals("Amended by", link.getDescription()); + } + + @Test + public void test_importDocumentation_RewritesLinksBetweenDecisions() { + decisionImporter.importDocumentation(workspace, new File("./src/test/resources/adrs")); + + Decision decision5 = documentation.getDecisions().stream().filter(d -> d.getId().equals("5")).findFirst().get(); + assertTrue(decision5.getContent().contains("Amended by [9. Help scripts](#9)")); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultDocumentImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultDocumentImporterTests.java new file mode 100644 index 000000000..5b3835ff1 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultDocumentImporterTests.java @@ -0,0 +1,81 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class DefaultDocumentImporterTests { + + private Workspace workspace; + private DefaultDocumentationImporter documentationImporter; + + @BeforeEach + public void setUp() { + documentationImporter = new DefaultDocumentationImporter(); + workspace = new Workspace("Name", "Description"); + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenANullDocumentableIsSpecified() { + try { + documentationImporter.importDocumentation(null, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A workspace, software system, container, or component must be specified.", iae.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenANullPathIsSpecified() { + try { + documentationImporter.importDocumentation(workspace, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A path must be specified.", iae.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenThePathDoesNotExist() { + try { + File directory = new File("foo"); + assertFalse(directory.exists()); + documentationImporter.importDocumentation(workspace, directory); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("foo does not exist.")); + } + } + + @Test + public void test_importDocumentation() { + File directory = new File("./src/test/resources/docs/docs"); + documentationImporter.importDocumentation(workspace, directory); + Collection

sections = workspace.getDocumentation().getSections(); + assertEquals(6, sections.size()); + + assertSection(Format.Markdown, "## Section 1", 1, "01-section-1.md", sections.stream().filter(s -> s.getOrder() == 1).findFirst().get()); + assertSection(Format.Markdown, "## Section 2", 2, "02-section-2.markdown", sections.stream().filter(s -> s.getOrder() == 2).findFirst().get()); + assertSection(Format.Markdown, "## Section 3", 3, "03-section-3.text", sections.stream().filter(s -> s.getOrder() == 3).findFirst().get()); + assertSection(Format.AsciiDoc, "== Section 4", 4, "04-section-4.adoc", sections.stream().filter(s -> s.getOrder() == 4).findFirst().get()); + assertSection(Format.AsciiDoc, "== Section 5", 5, "05-section-5.asciidoc", sections.stream().filter(s -> s.getOrder() == 5).findFirst().get()); + assertSection(Format.AsciiDoc, "== Section 6", 6, "06-section-6.asc", sections.stream().filter(s -> s.getOrder() == 6).findFirst().get()); + } + + private void assertSection(Format format, String content, int order, String filename, Section section) { + assertTrue(workspace.getDocumentation().getSections().contains(section)); + assertEquals(format, section.getFormat()); + assertEquals(content, section.getContent()); + assertEquals(order, section.getOrder()); + assertEquals(filename, section.getFilename()); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultImageImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultImageImporterTests.java new file mode 100644 index 000000000..58324c1b0 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/DefaultImageImporterTests.java @@ -0,0 +1,181 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Documentation; +import com.structurizr.documentation.Image; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class DefaultImageImporterTests { + + private Workspace workspace; + private DefaultImageImporter imageImporter; + + @BeforeEach + public void setUp() { + workspace = new Workspace("Name", "Description"); + imageImporter = new DefaultImageImporter(); + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenTheDocumentableIsNull() { + try { + imageImporter.importDocumentation(null, null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A workspace or software system must be specified.", e.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenThePathIsNull() { + try { + imageImporter.importDocumentation(workspace, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A path must be specified.", iae.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenThePathDoesNotExist() { + try { + imageImporter.importDocumentation(workspace, new File("./src/test/resources/java/com/structurizr/documentation/foo")); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("foo does not exist.")); + } + } + + @Test + public void test_importDocumentation_DoesNothing_WhenThereAreNoImageFilesInThePath() { + File directory = new File("./src/test/resources/docs/images/noimages"); + assertTrue(directory.exists()); + imageImporter.importDocumentation(workspace, directory); + assertTrue(workspace.getDocumentation().getImages().isEmpty()); + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenTheSpecifiedDirectoryIsNotAnImage() throws IOException { + try { + imageImporter.importDocumentation(workspace, new File("README.md")); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().endsWith("README.md is not a supported image file.")); + } + } + + @Test + public void test_importDocumentation_AddsAllImages_NonRecursively() { + Documentation documentation = workspace.getDocumentation(); + assertTrue(documentation.getImages().isEmpty()); + + imageImporter.importDocumentation(workspace, new File("./src/test/resources/docs/images/images")); + + Set images = documentation.getImages(); + assertEquals(4, documentation.getImages().size()); + + Image png = documentation.getImages().stream().filter(i -> i.getName().equals("image.png")).findFirst().get(); + assertEquals("image/png", png.getType()); + assertTrue(png.getContent().startsWith("iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAD")); + assertTrue(images.contains(png)); + + Image jpg = documentation.getImages().stream().filter(i -> i.getName().equals("image.jpg")).findFirst().get(); + assertEquals("image/jpeg", jpg.getType()); + assertEquals("/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAaACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDxzUdRu9Vv5r69nea4mcu7uxJyTnv2GeBT49I1CWNXS1cqwyDkDP5mqLfdP0NeueF9Pt9Q1AR3Kb40h3begJ4HNerhqEal+boebiK0qdra3PNP7D1L/nzf/vpf8afbnVvD17BqEPnWs0UgZJVbHIOccHvjoete8/8ACP6R/wA+EP5V598R9Ot9PttttHsSRUbaOgIcDitZ4anytpszhXqcyTS1PNHVl3KykMMgg8c16n4b1q20+6W5dt8MkWwlCCR05x+Fc78UraC1+IGqx28EcKeYW2xoFGSTk4Hc1x5RSCSoz9KwoV/Zpu17m9ah7RpXtY99/wCEy0j+9N/3x/8AXrhfH+swavEotwf4I0U43Md2ScCvPPLT+4v5V2XwttLa5+IOlRz28UqeaG2yIGGQRg4NVPFrlaUfxFHC2km5bH//2Q==", jpg.getContent()); + assertTrue(images.contains(jpg)); + + Image jpeg = documentation.getImages().stream().filter(i -> i.getName().equals("image.jpeg")).findFirst().get(); + assertEquals("image/jpeg", jpeg.getType()); + assertEquals("/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAaACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDxzUdRu9Vv5r69nea4mcu7uxJyTnv2GeBT49I1CWNXS1cqwyDkDP5mqLfdP0NeueF9Pt9Q1AR3Kb40h3begJ4HNerhqEal+boebiK0qdra3PNP7D1L/nzf/vpf8afbnVvD17BqEPnWs0UgZJVbHIOccHvjoete8/8ACP6R/wA+EP5V598R9Ot9PttttHsSRUbaOgIcDitZ4anytpszhXqcyTS1PNHVl3KykMMgg8c16n4b1q20+6W5dt8MkWwlCCR05x+Fc78UraC1+IGqx28EcKeYW2xoFGSTk4Hc1x5RSCSoz9KwoV/Zpu17m9ah7RpXtY99/wCEy0j+9N/3x/8AXrhfH+swavEotwf4I0U43Md2ScCvPPLT+4v5V2XwttLa5+IOlRz28UqeaG2yIGGQRg4NVPFrlaUfxFHC2km5bH//2Q==", jpeg.getContent()); + assertTrue(images.contains(jpeg)); + + Image gif = documentation.getImages().stream().filter(i -> i.getName().equals("image.gif")).findFirst().get(); + assertEquals("image/gif", gif.getType()); + assertEquals("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", gif.getContent()); + assertTrue(images.contains(gif)); + } + + @Test + public void test_importDocumentation_AddsAllImages_Recursively() { + Documentation documentation = workspace.getDocumentation(); + assertTrue(documentation.getImages().isEmpty()); + + imageImporter.importDocumentation(workspace, new File("./src/test/resources/docs/images")); + assertEquals(9, documentation.getImages().size()); + + Image pngInDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("image.png")).findFirst().get(); + assertEquals("image/png", pngInDirectory.getType()); + assertTrue(pngInDirectory.getContent().startsWith("iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAD")); + + Image jpgInDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("image.jpg")).findFirst().get(); + assertEquals("image/jpeg", jpgInDirectory.getType()); + assertEquals("/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAaACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDxzUdRu9Vv5r69nea4mcu7uxJyTnv2GeBT49I1CWNXS1cqwyDkDP5mqLfdP0NeueF9Pt9Q1AR3Kb40h3begJ4HNerhqEal+boebiK0qdra3PNP7D1L/nzf/vpf8afbnVvD17BqEPnWs0UgZJVbHIOccHvjoete8/8ACP6R/wA+EP5V598R9Ot9PttttHsSRUbaOgIcDitZ4anytpszhXqcyTS1PNHVl3KykMMgg8c16n4b1q20+6W5dt8MkWwlCCR05x+Fc78UraC1+IGqx28EcKeYW2xoFGSTk4Hc1x5RSCSoz9KwoV/Zpu17m9ah7RpXtY99/wCEy0j+9N/3x/8AXrhfH+swavEotwf4I0U43Md2ScCvPPLT+4v5V2XwttLa5+IOlRz28UqeaG2yIGGQRg4NVPFrlaUfxFHC2km5bH//2Q==", jpgInDirectory.getContent()); + + Image jpegInDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("image.jpeg")).findFirst().get(); + assertEquals("image/jpeg", jpegInDirectory.getType()); + assertEquals("/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAaACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDxzUdRu9Vv5r69nea4mcu7uxJyTnv2GeBT49I1CWNXS1cqwyDkDP5mqLfdP0NeueF9Pt9Q1AR3Kb40h3begJ4HNerhqEal+boebiK0qdra3PNP7D1L/nzf/vpf8afbnVvD17BqEPnWs0UgZJVbHIOccHvjoete8/8ACP6R/wA+EP5V598R9Ot9PttttHsSRUbaOgIcDitZ4anytpszhXqcyTS1PNHVl3KykMMgg8c16n4b1q20+6W5dt8MkWwlCCR05x+Fc78UraC1+IGqx28EcKeYW2xoFGSTk4Hc1x5RSCSoz9KwoV/Zpu17m9ah7RpXtY99/wCEy0j+9N/3x/8AXrhfH+swavEotwf4I0U43Md2ScCvPPLT+4v5V2XwttLa5+IOlRz28UqeaG2yIGGQRg4NVPFrlaUfxFHC2km5bH//2Q==", jpegInDirectory.getContent()); + + Image gifInDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("image.gif")).findFirst().get(); + assertEquals("image/gif", gifInDirectory.getType()); + assertEquals("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", gifInDirectory.getContent()); + + Image svgInDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("image.svg")).findFirst().get(); + assertEquals("image/svg+xml", svgInDirectory.getType()); + assertEquals("PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIzIiBmaWxsPSJyZWQiIC8+Cjwvc3ZnPiA=", svgInDirectory.getContent()); + + Image pngInSubDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("images/image.png")).findFirst().get(); + assertEquals("image/png", pngInSubDirectory.getType()); + assertTrue(pngInSubDirectory.getContent().startsWith("iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAD")); + + Image jpgInSubDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("images/image.jpg")).findFirst().get(); + assertEquals("image/jpeg", jpgInSubDirectory.getType()); + assertEquals("/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAaACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDxzUdRu9Vv5r69nea4mcu7uxJyTnv2GeBT49I1CWNXS1cqwyDkDP5mqLfdP0NeueF9Pt9Q1AR3Kb40h3begJ4HNerhqEal+boebiK0qdra3PNP7D1L/nzf/vpf8afbnVvD17BqEPnWs0UgZJVbHIOccHvjoete8/8ACP6R/wA+EP5V598R9Ot9PttttHsSRUbaOgIcDitZ4anytpszhXqcyTS1PNHVl3KykMMgg8c16n4b1q20+6W5dt8MkWwlCCR05x+Fc78UraC1+IGqx28EcKeYW2xoFGSTk4Hc1x5RSCSoz9KwoV/Zpu17m9ah7RpXtY99/wCEy0j+9N/3x/8AXrhfH+swavEotwf4I0U43Md2ScCvPPLT+4v5V2XwttLa5+IOlRz28UqeaG2yIGGQRg4NVPFrlaUfxFHC2km5bH//2Q==", jpgInSubDirectory.getContent()); + + Image jpegInSubDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("images/image.jpeg")).findFirst().get(); + assertEquals("image/jpeg", jpegInSubDirectory.getType()); + assertEquals("/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAaACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDxzUdRu9Vv5r69nea4mcu7uxJyTnv2GeBT49I1CWNXS1cqwyDkDP5mqLfdP0NeueF9Pt9Q1AR3Kb40h3begJ4HNerhqEal+boebiK0qdra3PNP7D1L/nzf/vpf8afbnVvD17BqEPnWs0UgZJVbHIOccHvjoete8/8ACP6R/wA+EP5V598R9Ot9PttttHsSRUbaOgIcDitZ4anytpszhXqcyTS1PNHVl3KykMMgg8c16n4b1q20+6W5dt8MkWwlCCR05x+Fc78UraC1+IGqx28EcKeYW2xoFGSTk4Hc1x5RSCSoz9KwoV/Zpu17m9ah7RpXtY99/wCEy0j+9N/3x/8AXrhfH+swavEotwf4I0U43Md2ScCvPPLT+4v5V2XwttLa5+IOlRz28UqeaG2yIGGQRg4NVPFrlaUfxFHC2km5bH//2Q==", jpegInSubDirectory.getContent()); + + Image gifInSubDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("images/image.gif")).findFirst().get(); + assertEquals("image/gif", gifInSubDirectory.getType()); + assertEquals("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", gifInSubDirectory.getContent()); + } + + @Test + public void test_importDocumentation_AddsASingleImage() { + Documentation documentation = workspace.getDocumentation(); + assertTrue(documentation.getImages().isEmpty()); + + imageImporter.importDocumentation(workspace, new File("./src/test/resources/docs/images/image.png")); + assertEquals(1, documentation.getImages().size()); + + Image png = documentation.getImages().stream().filter(i -> i.getName().equals("image.png")).findFirst().get(); + assertEquals("image/png", png.getType()); + assertTrue(png.getContent().startsWith("iVBORw0KGgoAAAANSUhEUgAAACAAAAAaCAYAAADWm14/AAAD")); + } + + @Test + public void test_importDocumentation_IgnoresHiddenFolders() throws Exception { + Documentation documentation = workspace.getDocumentation(); + + File tempDirectory = Files.createTempDirectory("test").toFile(); + File hiddenFolder = new File(tempDirectory, ".structurizr"); + hiddenFolder.mkdir(); + + File source = new File("./src/test/resources/docs/images/images/image.png"); + File destination = new File(hiddenFolder, "image.png"); + Files.copy(source.toPath(), destination.toPath()); + assertTrue(destination.exists()); + + imageImporter.importDocumentation(workspace, tempDirectory); + assertEquals(0, documentation.getImages().size()); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/documentation/FormatFinderTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/FormatFinderTests.java new file mode 100644 index 000000000..14c530a71 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/FormatFinderTests.java @@ -0,0 +1,38 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Format; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class FormatFinderTests { + + @Test + public void test_findFormat_ThrowsAnException_WhenAFileIsNotSpecified() { + try { + FormatFinder.findFormat(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A file must be specified.", iae.getMessage()); + } + } + + @Test + public void test_findFormat_ReturnsMarkdown_WhenAMarkdownFileIsSpecified() { + Assertions.assertEquals(Format.Markdown, FormatFinder.findFormat(new File("foo.md"))); + assertEquals(Format.Markdown, FormatFinder.findFormat(new File("foo.markdown"))); + assertEquals(Format.Markdown, FormatFinder.findFormat(new File("foo.text"))); + } + + @Test + public void test_findFormat_ReturnsAsciiDoc_WhenAnAsciiDocFileIsSpecified() { + assertEquals(Format.AsciiDoc, FormatFinder.findFormat(new File("foo.adoc"))); + assertEquals(Format.AsciiDoc, FormatFinder.findFormat(new File("foo.asciidoc"))); + assertEquals(Format.AsciiDoc, FormatFinder.findFormat(new File("foo.asc"))); + } + +} diff --git a/structurizr-import/src/test/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentImporterTests.java new file mode 100644 index 000000000..76cc55dca --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/RecursiveDefaultDocumentImporterTests.java @@ -0,0 +1,51 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RecursiveDefaultDocumentImporterTests { + + private Workspace workspace; + private RecursiveDefaultDocumentationImporter documentationImporter; + + @BeforeEach + public void setUp() { + documentationImporter = new RecursiveDefaultDocumentationImporter(); + workspace = new Workspace("Name", "Description"); + } + + @Test + public void test_importDocumentation_WithRecursiveSetToTrue() { + File directory = new File("./src/test/resources/docs/docs"); + + documentationImporter.importDocumentation(workspace, directory); + Collection
sections = workspace.getDocumentation().getSections(); + assertEquals(7, sections.size()); + + assertSection(Format.Markdown, "## Section 1", 1, "01-section-1.md", sections.stream().filter(s -> s.getOrder() == 1).findFirst().get()); + assertSection(Format.Markdown, "## Section 2", 2, "02-section-2.markdown", sections.stream().filter(s -> s.getOrder() == 2).findFirst().get()); + assertSection(Format.Markdown, "## Section 3", 3, "03-section-3.text", sections.stream().filter(s -> s.getOrder() == 3).findFirst().get()); + assertSection(Format.AsciiDoc, "== Section 4", 4, "04-section-4.adoc", sections.stream().filter(s -> s.getOrder() == 4).findFirst().get()); + assertSection(Format.AsciiDoc, "== Section 5", 5, "05-section-5.asciidoc", sections.stream().filter(s -> s.getOrder() == 5).findFirst().get()); + assertSection(Format.AsciiDoc, "== Section 6", 6, "06-section-6.asc", sections.stream().filter(s -> s.getOrder() == 6).findFirst().get()); + assertSection(Format.Markdown, "## Section 7", 7, "07-subdirectory/01-section-1.md", sections.stream().filter(s -> s.getOrder() == 7).findFirst().get()); + } + + private void assertSection(Format format, String content, int order, String filename, Section section) { + assertTrue(workspace.getDocumentation().getSections().contains(section)); + assertEquals(format, section.getFormat()); + assertEquals(content, section.getContent()); + assertEquals(order, section.getOrder()); + assertEquals(filename, section.getFilename()); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/resources/adrs/0001-record-architecture-decisions.md b/structurizr-import/src/test/resources/adrs/0001-record-architecture-decisions.md new file mode 100644 index 000000000..f30860000 --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0001-record-architecture-decisions.md @@ -0,0 +1,19 @@ +# 1. Record architecture decisions + +Date: 2016-02-12 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as described by Michael Nygard in this article: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions + +## Consequences + +See Michael Nygard's article, linked above. diff --git a/structurizr-import/src/test/resources/adrs/0002-implement-as-shell-scripts.md b/structurizr-import/src/test/resources/adrs/0002-implement-as-shell-scripts.md new file mode 100644 index 000000000..8e6ea15e6 --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0002-implement-as-shell-scripts.md @@ -0,0 +1,28 @@ +# 2. Implement as shell scripts + +Date: 2016-02-12 + +## Status + +Accepted + +## Context + +ADRs are plain text files stored in a subdirectory of the project. + +The tool needs to create new files and apply small edits to +the Status section of existing files. + +## Decision + +The tool is implemented as shell scripts that use standard Unix +tools -- grep, sed, awk, etc. + +## Consequences + +The tool won't support Windows. Being plain text files, ADRs can +be created by hand and edited in any text editor. This tool just +makes the process more convenient. + +Development will have to cope with differences between Unix +variants, particularly Linux and MacOS X. diff --git a/structurizr-import/src/test/resources/adrs/0003-single-command-with-subcommands.md b/structurizr-import/src/test/resources/adrs/0003-single-command-with-subcommands.md new file mode 100644 index 000000000..f64db8da1 --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0003-single-command-with-subcommands.md @@ -0,0 +1,45 @@ +# 3. Single command with subcommands + +Date: 2016-02-12 + +## Status + +Accepted + +## Context + +The tool provides a number of related commands to create +and manipulate architecture decision records. + +How can the user find out about the commands that are available? + +## Decision + +The tool defines a single command, called `adr`. + +The first argument to `adr` (the subcommand) specifies the +action to perform. Further arguments are interpreted by the +subcommand. + +Running `adr` without any arguments lists the available +subcommands. + +Subcommands are implemented as scripts in the same +directory as the `adr` script. E.g. the subcommand `new` is +implemented as the script `adr-new`, the subcommand `help` +as the script `adr-help` and so on. + +Helper scripts that are part of the implementation but not +subcommands follow a different naming convention, so that +subcommands can be listed by filtering and transforming script +file names. + +## Consequences + +Users can more easily explore the capabilities of the tool. + +Users are already used to this style of command-line tool. For +example, Git works this way. + +Each subcommand can be implemented in the most appropriate +language. diff --git a/structurizr-import/src/test/resources/adrs/0004-markdown-format.md b/structurizr-import/src/test/resources/adrs/0004-markdown-format.md new file mode 100644 index 000000000..160d1689f --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0004-markdown-format.md @@ -0,0 +1,40 @@ +# 4. Markdown format + +Date: 2016-02-12 + +## Status + +Accepted + +## Context + +The decision records must be stored in a plain text format: + +* This works well with version control systems. + +* It allows the tool to modify the status of records and insert + hyperlinks when one decision supercedes another. + +* Decisions can be read in the terminal, IDE, version control + browser, etc. + +People will want to use some formatting: lists, code examples, +and so on. + +People will want to view the decision records in a more readable +format than plain text, and maybe print them out. + + +## Decision + +Record architecture decisions in [Markdown format](https://daringfireball.net/projects/markdown/). + +## Consequences + +Decisions can be read in the terminal. + +Decisions will be formatted nicely and hyperlinked by the +browsers of project hosting sites like GitHub and Bitbucket. + +Tools like [Pandoc](http://pandoc.org/) can be used to convert +the decision records into HTML or PDF. diff --git a/structurizr-import/src/test/resources/adrs/0005-help-comments.md b/structurizr-import/src/test/resources/adrs/0005-help-comments.md new file mode 100644 index 000000000..b19bf0fb1 --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0005-help-comments.md @@ -0,0 +1,42 @@ +# 5. Help comments + +Date: 2016-02-13 + +## Status + +Accepted + +Amended by [9. Help scripts](0009-help-scripts.md) + +## Context + +The tool will have a `help` subcommand to provide documentation +for users. + +It's nice to have usage documentation in the script files +themselves, in comments. When reading the code, that's the first +place to look for information about how to run a script. + +## Decision + +Write usage documentation in comments in the source file. + +Distinguish between documentation comments and normal comments. +Documentation comments have two hash characters at the start of +the line. + +The `adr help` command can parse comments out from the script +using the standard Unix tools `grep` and `cut`. + +## Consequences + +No need to maintain help text in a separate file. + +Help text can easily be kept up to date as the script is edited. + +There's no automated check that the help text is up to date. The +tests do not work well as documentation for users, and the help +text is not easily cross-checked against the code. + +This won't work if any subcommands are not implemented as scripts +that use '#' as a comment character. diff --git a/structurizr-import/src/test/resources/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md b/structurizr-import/src/test/resources/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md new file mode 100644 index 000000000..4a3485f79 --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md @@ -0,0 +1,41 @@ +# 6. Packaging and distribution in other version control repositories + +Date: 2016-02-16 + +## Status + +Accepted + +## Context + +Users want to install adr-tools with their preferred package +manager. For example, Ubuntu users use `apt`, RedHat users use +`yum` and Mac OS X users use [Homebrew](http://brew.sh). + +The developers of `adr-tools` don't know how, nor have permissions, +to use all these packaging and distribution systems. Therefore packaging +and distribution must be done by "downstream" parties. + +The developers of the tool should not favour any one particular +packaging and distribution solution. + +## Decision + +The `adr-tools` project will not contain any packaging or +distribution scripts and config. + +Packaging and distribution will be managed by other projects in +separate version control repositories. + +## Consequences + +The git repo of this project will be simpler. + +Eventually, users will not have to use Git to get the software. + +We will have to tag releases in the `adr-tools` repository so that +packaging projects know what can be published and how it should be +identified. + +We will document how users can install the software in this +project's README file. diff --git a/structurizr-import/src/test/resources/adrs/0007-invoke-adr-config-executable-to-get-configuration.md b/structurizr-import/src/test/resources/adrs/0007-invoke-adr-config-executable-to-get-configuration.md new file mode 100644 index 000000000..a649b2356 --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0007-invoke-adr-config-executable-to-get-configuration.md @@ -0,0 +1,31 @@ +# 7. Invoke adr-config executable to get configuration + +Date: 2016-12-17 + +## Status + +Accepted + +## Context + +Packagers (e.g. Homebrew developers) want to configure adr-tools to match the conventions of their installation. + +Currently, this is done by sourcing a file `config.sh`, which should sit beside the `adr` executable. + +This name is too common. + +The `config.sh` file is not executable, and so doesn't belong in a bin directory. + +## Decision + +Replace `config.sh` with an executable, named `adr-config` that outputs configuration. + +Each script in ADR Tools will eval the output of `adr-config` to configure itself. + +## Consequences + +Configuration within ADR Tools is a little more complicated. + +Packagers can write their own implementation of `adr-config` that outputs configuration that matches the platform's installation conventions, and deploy it next to the `adr` script. + +To make development easier, the implementation of `adr-config` in the project's src/ directory will output configuration that lets the tool to run from the src/ directory without any installation step. (Packagers should not include this script in a deployable package.) diff --git a/structurizr-import/src/test/resources/adrs/0008-use-iso-8601-format-for-dates.md b/structurizr-import/src/test/resources/adrs/0008-use-iso-8601-format-for-dates.md new file mode 100644 index 000000000..4146f11df --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0008-use-iso-8601-format-for-dates.md @@ -0,0 +1,43 @@ +# 8. Use ISO 8601 Format for Dates + +Date: 2017-02-21 + +## Status + +Accepted + +## Context + +`adr-tools` seeks to communicate the history of architectural decisions of a +project. An important component of the history is the time at which a decision +was made. + +To communicate effectively, `adr-tools` should present information as +unambiguously as possible. That means that culture-neutral data formats should +be preferred over culture-specific formats. + +Existing `adr-tools` deployments format dates as `dd/mm/yyyy` by default. That +formatting is common formatting in the United Kingdom (where the `adr-tools` +project was originally written), but is easily confused with the `mm/dd/yyyy` +format preferred in the United States. + +The default date format may be overridden by setting `ADR_DATE` in `config.sh`. + +## Decision + +`adr-tools` will use the ISO 8601 format for dates: `yyyy-mm-dd` + +## Consequences + +Dates are displayed in a standard, culture-neutral format. + +The UK-style and ISO 8601 formats can be distinguished by their separator +character. The UK-style dates used a slash (`/`), while the ISO dates use a +hyphen (`-`). + +Prior to this decision, `adr-tools` was deployed using the UK format for dates. +After adopting the ISO 8601 format, existing deployments of `adr-tools` must do +one of the following: + + * Accept mixed formatting of dates within their documentation library. + * Update existing documents to use ISO 8601 dates by running `adr upgrade-repository` diff --git a/structurizr-import/src/test/resources/adrs/0009-help-scripts.md b/structurizr-import/src/test/resources/adrs/0009-help-scripts.md new file mode 100644 index 000000000..0146d127c --- /dev/null +++ b/structurizr-import/src/test/resources/adrs/0009-help-scripts.md @@ -0,0 +1,28 @@ +# 9. Help scripts + +Date: 2018-06-26 + +## Status + +Accepted + +Amends [5. Help comments](0005-help-comments.md) + +## Context + +Currently help text is generated by extracting specially formatted comments from the top of the command script. + +This makes it easy for developers of the tool: documentation and code is all in one place. + +But, it means that help text cannot include calculated values, such as the location of files. + +## Decision + +Where necessary, help text can be generated by a script. + +The script will be called _adr_help__ + +## Consequences + +Help scripts can include helper scripts to locate files, giving more accurate instructions to the user that reflect how the tool is deployed in their environment. + diff --git a/structurizr-import/src/test/resources/diagrams/kroki/diagram.dot b/structurizr-import/src/test/resources/diagrams/kroki/diagram.dot new file mode 100644 index 000000000..3f2b18926 --- /dev/null +++ b/structurizr-import/src/test/resources/diagrams/kroki/diagram.dot @@ -0,0 +1 @@ +digraph G {Hello->World} diff --git a/structurizr-import/src/test/resources/diagrams/mermaid/class.mmd b/structurizr-import/src/test/resources/diagrams/mermaid/class.mmd new file mode 100644 index 000000000..eee0f009f --- /dev/null +++ b/structurizr-import/src/test/resources/diagrams/mermaid/class.mmd @@ -0,0 +1,21 @@ +classDiagram + Animal <|-- Duck + Animal <|-- Fish + Animal <|-- Zebra + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class Duck{ + +String beakColor + +swim() + +quack() + } + class Fish{ + -int sizeInFeet + -canEat() + } + class Zebra{ + +bool is_wild + +run() + } diff --git a/structurizr-import/src/test/resources/diagrams/mermaid/flowchart.mmd b/structurizr-import/src/test/resources/diagrams/mermaid/flowchart.mmd new file mode 100644 index 000000000..a5bd75ae5 --- /dev/null +++ b/structurizr-import/src/test/resources/diagrams/mermaid/flowchart.mmd @@ -0,0 +1,6 @@ +flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car] \ No newline at end of file diff --git a/structurizr-import/src/test/resources/diagrams/plantuml/with-title.puml b/structurizr-import/src/test/resources/diagrams/plantuml/with-title.puml new file mode 100644 index 000000000..caf9f13a5 --- /dev/null +++ b/structurizr-import/src/test/resources/diagrams/plantuml/with-title.puml @@ -0,0 +1,4 @@ +@startuml +title Sequence diagram example +Bob -> Alice : hello +@enduml \ No newline at end of file diff --git a/structurizr-import/src/test/resources/diagrams/plantuml/without-title.puml b/structurizr-import/src/test/resources/diagrams/plantuml/without-title.puml new file mode 100644 index 000000000..1da6ac585 --- /dev/null +++ b/structurizr-import/src/test/resources/diagrams/plantuml/without-title.puml @@ -0,0 +1,3 @@ +@startuml +Bob -> Alice : hello +@enduml \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/01-section-1.md b/structurizr-import/src/test/resources/docs/docs/01-section-1.md new file mode 100644 index 000000000..4721380c0 --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/01-section-1.md @@ -0,0 +1 @@ +## Section 1 \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/02-section-2.markdown b/structurizr-import/src/test/resources/docs/docs/02-section-2.markdown new file mode 100644 index 000000000..2fc0e3f87 --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/02-section-2.markdown @@ -0,0 +1 @@ +## Section 2 \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/03-section-3.text b/structurizr-import/src/test/resources/docs/docs/03-section-3.text new file mode 100644 index 000000000..e847bfa94 --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/03-section-3.text @@ -0,0 +1 @@ +## Section 3 \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/04-section-4.adoc b/structurizr-import/src/test/resources/docs/docs/04-section-4.adoc new file mode 100644 index 000000000..062d5dd9a --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/04-section-4.adoc @@ -0,0 +1 @@ +== Section 4 \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/05-section-5.asciidoc b/structurizr-import/src/test/resources/docs/docs/05-section-5.asciidoc new file mode 100644 index 000000000..1a501bdbf --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/05-section-5.asciidoc @@ -0,0 +1 @@ +== Section 5 \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/06-section-6.asc b/structurizr-import/src/test/resources/docs/docs/06-section-6.asc new file mode 100644 index 000000000..8728c5661 --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/06-section-6.asc @@ -0,0 +1 @@ +== Section 6 \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/07-subdirectory/01-section-1.md b/structurizr-import/src/test/resources/docs/docs/07-subdirectory/01-section-1.md new file mode 100644 index 000000000..af328a67d --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/07-subdirectory/01-section-1.md @@ -0,0 +1 @@ +## Section 7 \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/docs/images/image.gif b/structurizr-import/src/test/resources/docs/docs/images/image.gif new file mode 100644 index 0000000000000000000000000000000000000000..c06542adaf2a63a53499dfe7bedc85e442e8d2b2 GIT binary patch literal 4682 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 fQ@dnt$vnQ|8D{SZ|*E1 literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/docs/images/image.jpg b/structurizr-import/src/test/resources/docs/docs/images/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0b9e9c39da3f93ae710234a051f94428f6c66ff3 GIT binary patch literal 1467 zcmex=kR1VRQz zDF#+Bn}NZLQ5wz;V$^`DVPart&tzbMs)+*9AOJKOq#H^zFJMHNxPS?+T44b*oGk^? z_Wvz|MLoA#DyHnP8$!323`E1Vw_ae#K|QlE+HwU zs-~`?sbyknW^Q3==I-I?6&w;879J59m7J2AmY$KBRa{b9R$ftA)!fqB*51+C zHEHscsne#9glAUcUPH>GPMb-@gC&`3vMPV0c2j1tcLpL-Us)&|gd}EX*wIAb&A3m4iGk z$ik{<$R^|%$evgztYp;4A>uS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@#CfcVET6$W zhVa*I24@B)Fkof^#u76#Fs4|5fR&vU1lT#)!GHq_xWIrL1bE@#KhO|Hpcz2TY(T`z z$<7IvBbxF5E-+a#{lCS)!^{XwOw57|_6&cDK2Go1Zs%jMqV6FfsF9&`z~p)gfaUG|*z zH#xBr1v`FRo2C@hleuDl+1>oRcE_LAc+P!P5VTlIFlegVKkYBu({|6FCI4adKE?kG z7q}k(`{($d;ds`&UiBmQo9`)Hw#i?n`>(NndF)}+wQr1bk8Ipg(33dfX?&if{L@mE z@b~(EG~c($f6=}1pW#05e}<#s&#%AT{NOD6!CUT=AO0@P&aIkO&c>>;$YXj&%Q8;| z1LeMkFR$aa8MhgoUv+h^=J{EMQ>OmRd9yw}W#;q5if+SSXEm<$Sf;AX-MMbv+tTu9 z_5UyY@!Hc=F{!#zIdy87i^kz>_L!9t76*Aals8J4{+j;5{ZW1UAH_9}kEA@3&nOcS=vSIAPQE=4jzo&BRrgmbY)7lCti3b!_Cz fQ@dnt$vnQ|8D{SZ|*E1 literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/docs/images/image.png b/structurizr-import/src/test/resources/docs/docs/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..644aef77b1dfbe2bad3a82b5e15421d22f95a5e1 GIT binary patch literal 1563 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ey!3HF+&5pANQY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fIRt zPXT0ZVp4u-iLH_n$Rap^xU(cP4PjGWG1OZ?59)(t^bPe4^x1XJ8hJbBT*g_UUO;IJRJu zciBOKlo>nx&c7+~FP(n><-D1)WiOV6-}S0mwru&ipZkhe1}_hhXnS^AJN)C%pQnHP z+8rNID$;Z!!MrUx=*6)EC%21zJh{`@qWYQ1mDcX*XLk0VV|rzvwb_*Amcz=s4eUH` zi#(Kaa+vn+RxtC5js5lIyS1#0jag@!bxZ17A*)=W!zDQe|1a6)nB+*kc1}z?IYsw_ zdkn*${gb?%r6;=_-8QM<#?(m-vug@YtJQAmQNE?VWi}obe2sd^`_rXPRSRmpQ(R*E^A<}@vgo%*nN#>>+lxFC zGWJ#|mS!5-v8iP!@A$6!CI9+@Z|5dYIj88bUHZS`o5jl??EP8DrKh-HH_NnhEVehk z$CY_-aJSib+8MBXdvZEMIO6_p#?bjE6&G+^tYRr|oMs(5V}r9v_bVQmNjEx@S`^K< zzFB+6v8b zyX{HJ?Y|e_JV;yI%sHLy!(5@cLN(_$7d!AwZc%=q@a9RgJ!f4~8UMZ7=NwsEi`KpU zwA@0Y%j3s4E4}-59g^o=HLSHdqvRS5uV%hcWxQASSHWg|m%@v$YvrT=T;aa0m-zCk z<)1D0j(7sL7u1G+?+$KoH?2ka*sMZd))i?wCohc>9OqFGzBZ${SO=xQoq$Tal#>G=TFMAmlAXH*nT{`W)=2VfW4yW<&O1}U*~3u%)iTi$2A{8!q| z+xOwi8&5g4kEX6sH5R<`&yq#^biuOt#?YJWuK59cmrd8D>(zYj;BmiW$z?a^@+l+l zk3He)JDm3X6t^m$9yl+;Vfxi8b?<*<|9JK4yJv9K{+su*&feI#z^XIjEnCg81GXhh zVK==eb!^aCexPMXO~;ovofVEn=cmUheYm*LvH0Nn9siFUTlrml{`G0WlVSy}c+1@Q zgIWSzF2<#({G9S^!tX;ihxn{lzmtvm@kr>(gQQQA`fArV>#1FuqQ<*aLCp2;IiG{N q8-<@;tohWdyk|zuiSCo^KiE&kES&#X{o@T#5$x&e=d#Wzp$P!Pn~`<^ literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/images/image.gif b/structurizr-import/src/test/resources/docs/images/image.gif new file mode 100644 index 0000000000000000000000000000000000000000..c06542adaf2a63a53499dfe7bedc85e442e8d2b2 GIT binary patch literal 4682 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 fQ@dnt$vnQ|8D{SZ|*E1 literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/images/image.jpg b/structurizr-import/src/test/resources/docs/images/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0b9e9c39da3f93ae710234a051f94428f6c66ff3 GIT binary patch literal 1467 zcmex=kR1VRQz zDF#+Bn}NZLQ5wz;V$^`DVPart&tzbMs)+*9AOJKOq#H^zFJMHNxPS?+T44b*oGk^? z_Wvz|MLoA#DyHnP8$!323`E1Vw_ae#K|QlE+HwU zs-~`?sbyknW^Q3==I-I?6&w;879J59m7J2AmY$KBRa{b9R$ftA)!fqB*51+C zHEHscsne#9glAUcUPH>GPMb-@gC&`3vMPV0c2j1tcLpL-Us)&|gd}EX*wIAb&A3m4iGk z$ik{<$R^|%$evgztYp;4A>uS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@#CfcVET6$W zhVa*I24@B)Fkof^#u76#Fs4|5fR&vU1lT#)!GHq_xWIrL1bE@#KhO|Hpcz2TY(T`z z$<7IvBbxF5E-+a#{lCS)!^{XwOw57|_6&cDK2Go1Zs%jMqV6FfsF9&`z~p)gfaUG|*z zH#xBr1v`FRo2C@hleuDl+1>oRcE_LAc+P!P5VTlIFlegVKkYBu({|6FCI4adKE?kG z7q}k(`{($d;ds`&UiBmQo9`)Hw#i?n`>(NndF)}+wQr1bk8Ipg(33dfX?&if{L@mE z@b~(EG~c($f6=}1pW#05e}<#s&#%AT{NOD6!CUT=AO0@P&aIkO&c>>;$YXj&%Q8;| z1LeMkFR$aa8MhgoUv+h^=J{EMQ>OmRd9yw}W#;q5if+SSXEm<$Sf;AX-MMbv+tTu9 z_5UyY@!Hc=F{!#zIdy87i^kz>_L!9t76*Aals8J4{+j;5{ZW1UAH_9}kEA@3&nOcS=vSIAPQE=4jzo&BRrgmbY)7lCti3b!_Cz fQ@dnt$vnQ|8D{SZ|*E1 literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/images/image.png b/structurizr-import/src/test/resources/docs/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..644aef77b1dfbe2bad3a82b5e15421d22f95a5e1 GIT binary patch literal 1563 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ey!3HF+&5pANQY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fIRt zPXT0ZVp4u-iLH_n$Rap^xU(cP4PjGWG1OZ?59)(t^bPe4^x1XJ8hJbBT*g_UUO;IJRJu zciBOKlo>nx&c7+~FP(n><-D1)WiOV6-}S0mwru&ipZkhe1}_hhXnS^AJN)C%pQnHP z+8rNID$;Z!!MrUx=*6)EC%21zJh{`@qWYQ1mDcX*XLk0VV|rzvwb_*Amcz=s4eUH` zi#(Kaa+vn+RxtC5js5lIyS1#0jag@!bxZ17A*)=W!zDQe|1a6)nB+*kc1}z?IYsw_ zdkn*${gb?%r6;=_-8QM<#?(m-vug@YtJQAmQNE?VWi}obe2sd^`_rXPRSRmpQ(R*E^A<}@vgo%*nN#>>+lxFC zGWJ#|mS!5-v8iP!@A$6!CI9+@Z|5dYIj88bUHZS`o5jl??EP8DrKh-HH_NnhEVehk z$CY_-aJSib+8MBXdvZEMIO6_p#?bjE6&G+^tYRr|oMs(5V}r9v_bVQmNjEx@S`^K< zzFB+6v8b zyX{HJ?Y|e_JV;yI%sHLy!(5@cLN(_$7d!AwZc%=q@a9RgJ!f4~8UMZ7=NwsEi`KpU zwA@0Y%j3s4E4}-59g^o=HLSHdqvRS5uV%hcWxQASSHWg|m%@v$YvrT=T;aa0m-zCk z<)1D0j(7sL7u1G+?+$KoH?2ka*sMZd))i?wCohc>9OqFGzBZ${SO=xQoq$Tal#>G=TFMAmlAXH*nT{`W)=2VfW4yW<&O1}U*~3u%)iTi$2A{8!q| z+xOwi8&5g4kEX6sH5R<`&yq#^biuOt#?YJWuK59cmrd8D>(zYj;BmiW$z?a^@+l+l zk3He)JDm3X6t^m$9yl+;Vfxi8b?<*<|9JK4yJv9K{+su*&feI#z^XIjEnCg81GXhh zVK==eb!^aCexPMXO~;ovofVEn=cmUheYm*LvH0Nn9siFUTlrml{`G0WlVSy}c+1@Q zgIWSzF2<#({G9S^!tX;ihxn{lzmtvm@kr>(gQQQA`fArV>#1FuqQ<*aLCp2;IiG{N q8-<@;tohWdyk|zuiSCo^KiE&kES&#X{o@T#5$x&e=d#Wzp$P!Pn~`<^ literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/images/image.svg b/structurizr-import/src/test/resources/docs/images/image.svg new file mode 100644 index 000000000..1d526413e --- /dev/null +++ b/structurizr-import/src/test/resources/docs/images/image.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/structurizr-import/src/test/resources/docs/images/images/image.gif b/structurizr-import/src/test/resources/docs/images/images/image.gif new file mode 100644 index 0000000000000000000000000000000000000000..c06542adaf2a63a53499dfe7bedc85e442e8d2b2 GIT binary patch literal 4682 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 fQ@dnt$vnQ|8D{SZ|*E1 literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/images/images/image.jpg b/structurizr-import/src/test/resources/docs/images/images/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0b9e9c39da3f93ae710234a051f94428f6c66ff3 GIT binary patch literal 1467 zcmex=kR1VRQz zDF#+Bn}NZLQ5wz;V$^`DVPart&tzbMs)+*9AOJKOq#H^zFJMHNxPS?+T44b*oGk^? z_Wvz|MLoA#DyHnP8$!323`E1Vw_ae#K|QlE+HwU zs-~`?sbyknW^Q3==I-I?6&w;879J59m7J2AmY$KBRa{b9R$ftA)!fqB*51+C zHEHscsne#9glAUcUPH>GPMb-@gC&`3vMPV0c2j1tcLpL-Us)&|gd}EX*wIAb&A3m4iGk z$ik{<$R^|%$evgztYp;4A>uS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@#CfcVET6$W zhVa*I24@B)Fkof^#u76#Fs4|5fR&vU1lT#)!GHq_xWIrL1bE@#KhO|Hpcz2TY(T`z z$<7IvBbxF5E-+a#{lCS)!^{XwOw57|_6&cDK2Go1Zs%jMqV6FfsF9&`z~p)gfaUG|*z zH#xBr1v`FRo2C@hleuDl+1>oRcE_LAc+P!P5VTlIFlegVKkYBu({|6FCI4adKE?kG z7q}k(`{($d;ds`&UiBmQo9`)Hw#i?n`>(NndF)}+wQr1bk8Ipg(33dfX?&if{L@mE z@b~(EG~c($f6=}1pW#05e}<#s&#%AT{NOD6!CUT=AO0@P&aIkO&c>>;$YXj&%Q8;| z1LeMkFR$aa8MhgoUv+h^=J{EMQ>OmRd9yw}W#;q5if+SSXEm<$Sf;AX-MMbv+tTu9 z_5UyY@!Hc=F{!#zIdy87i^kz>_L!9t76*Aals8J4{+j;5{ZW1UAH_9}kEA@3&nOcS=vSIAPQE=4jzo&BRrgmbY)7lCti3b!_Cz fQ@dnt$vnQ|8D{SZ|*E1 literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/images/images/image.png b/structurizr-import/src/test/resources/docs/images/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..644aef77b1dfbe2bad3a82b5e15421d22f95a5e1 GIT binary patch literal 1563 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ey!3HF+&5pANQY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fIRt zPXT0ZVp4u-iLH_n$Rap^xU(cP4PjGWG1OZ?59)(t^bPe4^x1XJ8hJbBT*g_UUO;IJRJu zciBOKlo>nx&c7+~FP(n><-D1)WiOV6-}S0mwru&ipZkhe1}_hhXnS^AJN)C%pQnHP z+8rNID$;Z!!MrUx=*6)EC%21zJh{`@qWYQ1mDcX*XLk0VV|rzvwb_*Amcz=s4eUH` zi#(Kaa+vn+RxtC5js5lIyS1#0jag@!bxZ17A*)=W!zDQe|1a6)nB+*kc1}z?IYsw_ zdkn*${gb?%r6;=_-8QM<#?(m-vug@YtJQAmQNE?VWi}obe2sd^`_rXPRSRmpQ(R*E^A<}@vgo%*nN#>>+lxFC zGWJ#|mS!5-v8iP!@A$6!CI9+@Z|5dYIj88bUHZS`o5jl??EP8DrKh-HH_NnhEVehk z$CY_-aJSib+8MBXdvZEMIO6_p#?bjE6&G+^tYRr|oMs(5V}r9v_bVQmNjEx@S`^K< zzFB+6v8b zyX{HJ?Y|e_JV;yI%sHLy!(5@cLN(_$7d!AwZc%=q@a9RgJ!f4~8UMZ7=NwsEi`KpU zwA@0Y%j3s4E4}-59g^o=HLSHdqvRS5uV%hcWxQASSHWg|m%@v$YvrT=T;aa0m-zCk z<)1D0j(7sL7u1G+?+$KoH?2ka*sMZd))i?wCohc>9OqFGzBZ${SO=xQoq$Tal#>G=TFMAmlAXH*nT{`W)=2VfW4yW<&O1}U*~3u%)iTi$2A{8!q| z+xOwi8&5g4kEX6sH5R<`&yq#^biuOt#?YJWuK59cmrd8D>(zYj;BmiW$z?a^@+l+l zk3He)JDm3X6t^m$9yl+;Vfxi8b?<*<|9JK4yJv9K{+su*&feI#z^XIjEnCg81GXhh zVK==eb!^aCexPMXO~;ovofVEn=cmUheYm*LvH0Nn9siFUTlrml{`G0WlVSy}c+1@Q zgIWSzF2<#({G9S^!tX;ihxn{lzmtvm@kr>(gQQQA`fArV>#1FuqQ<*aLCp2;IiG{N q8-<@;tohWdyk|zuiSCo^KiE&kES&#X{o@T#5$x&e=d#Wzp$P!Pn~`<^ literal 0 HcmV?d00001 diff --git a/structurizr-import/src/test/resources/docs/images/noimages/readme.md b/structurizr-import/src/test/resources/docs/images/noimages/readme.md new file mode 100644 index 000000000..6e5b3dd54 --- /dev/null +++ b/structurizr-import/src/test/resources/docs/images/noimages/readme.md @@ -0,0 +1 @@ +These are not the images you are looking for... From 4e7e075545fba998ba1634b53ae8af330f8f27a6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 9 Jan 2024 15:01:54 +0000 Subject: [PATCH 139/418] Update READMEs. --- structurizr-client/README.md | 8 +++++++- structurizr-core/README.md | 2 ++ structurizr-dsl/README.md | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/structurizr-client/README.md b/structurizr-client/README.md index 60a155cf1..ea9734085 100644 --- a/structurizr-client/README.md +++ b/structurizr-client/README.md @@ -2,4 +2,10 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-client.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-client) -- [Documentation](https://docs.structurizr.com/java/workspace-api) +This library provides an API client for the workspace and admin APIs provided by the Structurizr cloud service and on-premises installation. + +- [Cloud service - Workspace API](https://docs.structurizr.com/cloud/workspace-api) +- [Cloud service - Admin API](https://docs.structurizr.com/cloud/admin-api) +- [On-premises installation - Workspace API](https://docs.structurizr.com/onpremises/workspace-api) +- [On-premises installation - Admin API](https://docs.structurizr.com/onpremises/admin-api) +- [Workspace API client](https://docs.structurizr.com/java/workspace-api) diff --git a/structurizr-core/README.md b/structurizr-core/README.md index 23e5b133d..2a1d0b062 100644 --- a/structurizr-core/README.md +++ b/structurizr-core/README.md @@ -2,5 +2,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-core.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-core) +This library provides the core functionality of Structurizr related to creating workspaces. + - [Documentation](https://docs.structurizr.com/java) diff --git a/structurizr-dsl/README.md b/structurizr-dsl/README.md index ae9e61d84..2108a6503 100644 --- a/structurizr-dsl/README.md +++ b/structurizr-dsl/README.md @@ -2,7 +2,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-dsl.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-dsl) -This GitHub repository contains an implementation of the Structurizr DSL - a way to create Structurizr software +This library is the implementation of the Structurizr DSL - a way to create Structurizr software architecture models based upon the [C4 model](https://c4model.com) using a textual domain specific language (DSL). The Structurizr DSL has appeared on the [ThoughtWorks Tech Radar - Techniques - Diagrams as code](https://www.thoughtworks.com/radar/techniques/diagrams-as-code) From ab302967e2d596b20268637908568874f38191f0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 9 Jan 2024 15:02:58 +0000 Subject: [PATCH 140/418] Exclude structurizr-graph tests from running via GitHub Actions. --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ab8bc8cd1..68c76f168 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -28,4 +28,4 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew + run: ./gradlew -x :structurizr-graphviz:test From 99d4ccc78c83424ffc4ee8e6e4b8e0238ee3b8ae Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 9 Jan 2024 17:25:24 +0000 Subject: [PATCH 141/418] Removes the `Workspace.countAndLogWarnings()` method with an initial version of something more flexible. --- settings.gradle | 1 + structurizr-assistant/README.md | 3 + structurizr-assistant/build.gradle | 7 ++ .../com/structurizr/assistant/Assistant.java | 20 +++++ .../assistant/DefaultAssistant.java | 65 +++++++++++++++ .../com/structurizr/assistant/Inspection.java | 50 ++++++++++++ .../com/structurizr/assistant/Priority.java | 9 +++ .../structurizr/assistant/Recommendation.java | 32 ++++++++ .../model/ComponentDescriptionInspection.java | 16 ++++ .../assistant/model/ComponentInspection.java | 21 +++++ .../model/ComponentTechnologyInspection.java | 28 +++++++ .../model/ContainerDescriptionInspection.java | 16 ++++ .../assistant/model/ContainerInspection.java | 21 +++++ .../model/ContainerTechnologyInspection.java | 28 +++++++ .../DeploymentNodeDescriptionInspection.java | 16 ++++ .../model/DeploymentNodeInspection.java | 21 +++++ .../DeploymentNodeTechnologyInspection.java | 28 +++++++ .../model/ElementDescriptionInspection.java | 23 ++++++ .../assistant/model/ElementInspection.java | 30 +++++++ ...lementNotIncludedInAnyViewsInspection.java | 44 +++++++++++ .../model/EmptyDeploymentNodeInspection.java | 27 +++++++ .../assistant/model/EmptyModelInspection.java | 26 ++++++ ...frastructureNodeDescriptionInspection.java | 15 ++++ .../model/InfrastructureNodeInspection.java | 21 +++++ ...nfrastructureNodeTechnologyInspection.java | 28 +++++++ .../assistant/model/ModelInspection.java | 24 ++++++ ...ipleSoftwareSystemsDetailedInspection.java | 34 ++++++++ .../model/OrphanedElementInspection.java | 44 +++++++++++ .../model/PersonDescriptionInspection.java | 16 ++++ .../RelationshipDescriptionInspection.java | 37 +++++++++ .../model/RelationshipInspection.java | 36 +++++++++ .../RelationshipTechnologyInspection.java | 37 +++++++++ .../SoftwareSystemDescriptionInspection.java | 16 ++++ ...sForMultipleSoftwareSystemsInspection.java | 34 ++++++++ .../assistant/view/EmptyViewsInspection.java | 26 ++++++ ...sForMultipleSoftwareSystemsInspection.java | 34 ++++++++ .../assistant/view/ViewsInspection.java | 23 ++++++ .../workspace/WorkspaceInspection.java | 23 ++++++ .../workspace/WorkspaceScopeInspection.java | 26 ++++++ .../ComponentDescriptionInspectionTests.java | 41 ++++++++++ .../ComponentTechnologyInspectionTests.java | 41 ++++++++++ .../ContainerDescriptionInspectionTests.java | 38 +++++++++ .../ContainerTechnologyInspectionTests.java | 38 +++++++++ ...loymentNodeDescriptionInspectionTests.java | 35 ++++++++ ...ploymentNodeTechnologyInspectionTests.java | 35 ++++++++ ...tNotIncludedInAnyViewsInspectionTests.java | 36 +++++++++ .../model/EmptyDeploymentNodeCheckTests.java | 71 +++++++++++++++++ .../model/EmptyModelInspectionTests.java | 33 ++++++++ ...ructureNodeDescriptionInspectionTests.java | 37 +++++++++ ...tructureNodeTechnologyInspectionTests.java | 37 +++++++++ ...oftwareSystemsDetailedInspectionTests.java | 79 +++++++++++++++++++ .../model/OrphanedElementInspectionTests.java | 40 ++++++++++ .../PersonDescriptionInspectionTests.java | 35 ++++++++ ...elationshipDescriptionInspectionTests.java | 57 +++++++++++++ ...RelationshipTechnologyInspectionTests.java | 55 +++++++++++++ ...twareSystemDescriptionInspectionTests.java | 35 ++++++++ ...ultipleSoftwareSystemsInspectionTests.java | 40 ++++++++++ .../view/EmptyViewsInspectionTests.java | 33 ++++++++ ...ultipleSoftwareSystemsInspectionTests.java | 40 ++++++++++ .../WorkspaceScopeInspectionTests.java | 34 ++++++++ .../structurizr/api/WorkspaceApiClient.java | 2 - .../main/java/com/structurizr/Workspace.java | 68 ---------------- .../java/com/structurizr/WorkspaceTests.java | 20 ----- 63 files changed, 1896 insertions(+), 90 deletions(-) create mode 100644 structurizr-assistant/README.md create mode 100644 structurizr-assistant/build.gradle create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/Assistant.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentTechnologyInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerTechnologyInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyDeploymentNodeInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyModelInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/ModelInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/OrphanedElementInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/PersonDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipTechnologyInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/view/EmptyViewsInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/view/ViewsInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceScopeInspection.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java diff --git a/settings.gradle b/settings.gradle index 3b97826de..e06f2f882 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'structurizr-java' +include 'structurizr-assistant' include 'structurizr-client' include 'structurizr-core' include 'structurizr-dsl' diff --git a/structurizr-assistant/README.md b/structurizr-assistant/README.md new file mode 100644 index 000000000..790b286e8 --- /dev/null +++ b/structurizr-assistant/README.md @@ -0,0 +1,3 @@ +# structurizr-assistant + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-assistant.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-assistant) diff --git a/structurizr-assistant/build.gradle b/structurizr-assistant/build.gradle new file mode 100644 index 000000000..b6bde77a2 --- /dev/null +++ b/structurizr-assistant/build.gradle @@ -0,0 +1,7 @@ +dependencies { + + api project(':structurizr-core') + + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/Assistant.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/Assistant.java new file mode 100644 index 000000000..aba2a5523 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/Assistant.java @@ -0,0 +1,20 @@ +package com.structurizr.assistant; + +import java.util.ArrayList; +import java.util.Collection; + +abstract class Assistant { + + private final Collection recommendations = new ArrayList<>(); + + public Collection getRecommendations() { + return new ArrayList<>(recommendations); + } + + protected void add(Recommendation recommendation) { + if (recommendation != null) { + recommendations.add(recommendation); + } + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java new file mode 100644 index 000000000..ec1114489 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java @@ -0,0 +1,65 @@ +package com.structurizr.assistant; + +import com.structurizr.Workspace; +import com.structurizr.assistant.model.*; +import com.structurizr.assistant.view.ContainerViewsForMultipleSoftwareSystemsInspection; +import com.structurizr.assistant.view.EmptyViewsInspection; +import com.structurizr.assistant.view.SystemContextViewsForMultipleSoftwareSystemsInspection; +import com.structurizr.assistant.workspace.WorkspaceScopeInspection; +import com.structurizr.model.*; + +public class DefaultAssistant extends Assistant { + + private static final String ALL_RECOMMENDATIONS = "structurizr.recommendations"; + + public DefaultAssistant(Workspace workspace) { + if (workspace.getProperties().getOrDefault(ALL_RECOMMENDATIONS, "true").equalsIgnoreCase("false")) { + // skip all inspections + return; + } + + runWorkspaceInspections(workspace); + runModelInspections(workspace); + runViewInspections(workspace); + } + + private void runWorkspaceInspections(Workspace workspace) { + add(new WorkspaceScopeInspection(workspace).run()); + } + + private void runModelInspections(Workspace workspace) { + add(new EmptyModelInspection(workspace).run()); + add(new MultipleSoftwareSystemsDetailedInspection(workspace).run()); + ElementNotIncludedInAnyViewsInspection elementNotIncludedInAnyViewsCheck = new ElementNotIncludedInAnyViewsInspection(workspace); + OrphanedElementInspection orphanedElementCheck = new OrphanedElementInspection(workspace); + for (Element element : workspace.getModel().getElements()) { + add(elementNotIncludedInAnyViewsCheck.run(element)); + add(orphanedElementCheck.run(element)); + + if (element instanceof Person) { + add(new PersonDescriptionInspection(workspace).run(element)); + } + + if (element instanceof SoftwareSystem) { + add(new SoftwareSystemDescriptionInspection(workspace).run(element)); + } + + if (element instanceof Container) { + add(new ContainerDescriptionInspection(workspace).run(element)); + add(new ContainerTechnologyInspection(workspace).run(element)); + } + + if (element instanceof Component) { + add(new ComponentDescriptionInspection(workspace).run(element)); + add(new ComponentTechnologyInspection(workspace).run(element)); + } + } + } + + private void runViewInspections(Workspace workspace) { + add(new EmptyViewsInspection(workspace).run()); + add(new SystemContextViewsForMultipleSoftwareSystemsInspection(workspace).run()); + add(new ContainerViewsForMultipleSoftwareSystemsInspection(workspace).run()); + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java new file mode 100644 index 000000000..22458a136 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java @@ -0,0 +1,50 @@ +package com.structurizr.assistant; + +import com.structurizr.PropertyHolder; +import com.structurizr.Workspace; + +public abstract class Inspection { + + private static final String STRUCTURIZR_RECOMMENDATIONS_PREFIX = "structurizr.recommendations."; + + private final Workspace workspace; + + protected Inspection(Workspace workspace) { + this.workspace = workspace; + } + + protected abstract String getType(); + + protected Workspace getWorkspace() { + return workspace; + } + + protected boolean isEnabled(String type, PropertyHolder... propertyHolders) { + String value = "true"; + + for (PropertyHolder propertyHolder : propertyHolders) { + if (propertyHolder != null) { + value = propertyHolder.getProperties().getOrDefault(STRUCTURIZR_RECOMMENDATIONS_PREFIX + type, value); + } + } + + return !value.equalsIgnoreCase("false"); + } + + protected Recommendation noRecommendation() { + return null; + } + + protected Recommendation lowPriorityRecommendation(String description) { + return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Priority.Low, description); + } + + protected Recommendation mediumPriorityRecommendation(String description) { + return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Priority.Medium, description); + } + + protected Recommendation highPriorityRecommendation(String description) { + return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Priority.High, description); + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java new file mode 100644 index 000000000..5a2c3085e --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java @@ -0,0 +1,9 @@ +package com.structurizr.assistant; + +public enum Priority { + + Low, + Medium, + High + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java new file mode 100644 index 000000000..1e35b3c63 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java @@ -0,0 +1,32 @@ +package com.structurizr.assistant; + +public final class Recommendation { + + private final String type; + private final Priority priority; + private final String description; + + Recommendation(String type, Priority priority, String description) { + this.type = type; + this.priority = priority; + this.description = description; + } + + public String getType() { + return type; + } + + public Priority getPriority() { + return priority; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return type + " | " + priority + " | " + description; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentDescriptionInspection.java new file mode 100644 index 000000000..ccacdefbf --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; + +public class ComponentDescriptionInspection extends ElementDescriptionInspection { + + public ComponentDescriptionInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected String getType() { + return "model.component.description"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java new file mode 100644 index 000000000..4c64146f4 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Component; +import com.structurizr.model.Element; + +abstract class ComponentInspection extends ElementInspection { + + public ComponentInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Element element) { + return inspect((Component)element); + } + + protected abstract Recommendation inspect(Component component); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentTechnologyInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentTechnologyInspection.java new file mode 100644 index 000000000..780acd976 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Component; +import com.structurizr.util.StringUtils; + +public class ComponentTechnologyInspection extends ComponentInspection { + + public ComponentTechnologyInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Component component) { + if (StringUtils.isNullOrEmpty(component.getDescription())) { + return lowPriorityRecommendation("Add a technology to the " + terminologyFor(component).toLowerCase() + " named \"" + component.getName() + "\"."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.component.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerDescriptionInspection.java new file mode 100644 index 000000000..ac4d59f8e --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; + +public class ContainerDescriptionInspection extends ElementDescriptionInspection { + + public ContainerDescriptionInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected String getType() { + return "model.container.description"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java new file mode 100644 index 000000000..4c7dea0fd --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.model.Element; + +abstract class ContainerInspection extends ElementInspection { + + public ContainerInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Element element) { + return inspect((Container)element); + } + + protected abstract Recommendation inspect(Container container); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerTechnologyInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerTechnologyInspection.java new file mode 100644 index 000000000..34baaa0fc --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.util.StringUtils; + +public class ContainerTechnologyInspection extends ContainerInspection { + + public ContainerTechnologyInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Container container) { + if (StringUtils.isNullOrEmpty(container.getTechnology())) { + return mediumPriorityRecommendation("Add a technology to the " + terminologyFor(container).toLowerCase() + " named \"" + container.getName() + "\"."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.container.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspection.java new file mode 100644 index 000000000..5dfccec44 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; + +public class DeploymentNodeDescriptionInspection extends ElementDescriptionInspection { + + public DeploymentNodeDescriptionInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected String getType() { + return "model.deploymentnode.description"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeInspection.java new file mode 100644 index 000000000..bd87be417 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.Element; + +abstract class DeploymentNodeInspection extends ElementInspection { + + public DeploymentNodeInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Element element) { + return inspect((DeploymentNode)element); + } + + protected abstract Recommendation inspect(DeploymentNode deploymentNode); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspection.java new file mode 100644 index 000000000..eeb768821 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.DeploymentNode; +import com.structurizr.util.StringUtils; + +public class DeploymentNodeTechnologyInspection extends DeploymentNodeInspection { + + public DeploymentNodeTechnologyInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(DeploymentNode deploymentNode) { + if (StringUtils.isNullOrEmpty(deploymentNode.getDescription())) { + return mediumPriorityRecommendation("Add a technology to the " + terminologyFor(deploymentNode).toLowerCase() + " named \"" + deploymentNode.getName() + "\"."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.deploymentnode.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementDescriptionInspection.java new file mode 100644 index 000000000..85b024d81 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementDescriptionInspection.java @@ -0,0 +1,23 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Element; +import com.structurizr.util.StringUtils; + +abstract class ElementDescriptionInspection extends ElementInspection { + + public ElementDescriptionInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Element element) { + if (StringUtils.isNullOrEmpty(element.getDescription())) { + return mediumPriorityRecommendation("Add a description to the " + terminologyFor(element).toLowerCase() + " named \"" + element.getName() + "\"."); + } + + return noRecommendation(); + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementInspection.java new file mode 100644 index 000000000..475b7121d --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementInspection.java @@ -0,0 +1,30 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Inspection; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Element; + +abstract class ElementInspection extends Inspection { + + public ElementInspection(Workspace workspace) { + super(workspace); + } + + public final Recommendation run(Element element) { + Element parentElement = element.getParent(); + + if (isEnabled(getType(), getWorkspace(), getWorkspace().getModel(), parentElement, element)) { + return inspect(element); + } + + return noRecommendation(); + } + + protected String terminologyFor(Element element) { + return getWorkspace().getViews().getConfiguration().getTerminology().findTerminology(element).toLowerCase(); + } + + protected abstract Recommendation inspect(Element element); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspection.java new file mode 100644 index 000000000..930d1374c --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspection.java @@ -0,0 +1,44 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Element; +import com.structurizr.view.ElementView; +import com.structurizr.view.ModelView; +import com.structurizr.view.View; + +import java.util.HashSet; +import java.util.Set; + +public class ElementNotIncludedInAnyViewsInspection extends ElementInspection { + + private final Set elementsInViews = new HashSet<>(); + + public ElementNotIncludedInAnyViewsInspection(Workspace workspace) { + super(workspace); + + for (View view : workspace.getViews().getViews()) { + if (view instanceof ModelView) { + ModelView modelView = (ModelView)view; + for (ElementView elementView : modelView.getElements()) { + elementsInViews.add(elementView.getId()); + } + } + } + } + + @Override + protected Recommendation inspect(Element element) { + if (!elementsInViews.contains(element.getId())) { + return lowPriorityRecommendation("The " + terminologyFor(element) + " named \"" + element.getName() + "\" is not included on any views - add it to a view."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.element.noview"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyDeploymentNodeInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyDeploymentNodeInspection.java new file mode 100644 index 000000000..b3c46582f --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyDeploymentNodeInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.DeploymentNode; + +public class EmptyDeploymentNodeInspection extends DeploymentNodeInspection { + + public EmptyDeploymentNodeInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(DeploymentNode deploymentNode) { + if (!deploymentNode.hasChildren() && !deploymentNode.hasSoftwareSystemInstances() && !deploymentNode.hasContainerInstances() && !deploymentNode.hasInfrastructureNodes()) { + return lowPriorityRecommendation("The " + terminologyFor(deploymentNode).toLowerCase() + " named \"" + deploymentNode.getName() + "\" is empty."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.deploymentnode.empty"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyModelInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyModelInspection.java new file mode 100644 index 000000000..d31042e8d --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/EmptyModelInspection.java @@ -0,0 +1,26 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; + +public class EmptyModelInspection extends ModelInspection { + + public EmptyModelInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Workspace workspace) { + if (workspace.getModel().isEmpty()) { + return highPriorityRecommendation("Add some elements to the model."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.empty"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspection.java new file mode 100644 index 000000000..265b961ac --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspection.java @@ -0,0 +1,15 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; + +public class InfrastructureNodeDescriptionInspection extends ElementDescriptionInspection { + + public InfrastructureNodeDescriptionInspection(Workspace workspace) { + super(workspace); + } + + protected String getType() { + return "model.infrastructurenode.description"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeInspection.java new file mode 100644 index 000000000..62097ea0f --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Element; +import com.structurizr.model.InfrastructureNode; + +abstract class InfrastructureNodeInspection extends ElementInspection { + + public InfrastructureNodeInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Element element) { + return inspect((InfrastructureNode)element); + } + + protected abstract Recommendation inspect(InfrastructureNode infrastructureNode); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspection.java new file mode 100644 index 000000000..951dc0fb1 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.InfrastructureNode; +import com.structurizr.util.StringUtils; + +public class InfrastructureNodeTechnologyInspection extends InfrastructureNodeInspection { + + public InfrastructureNodeTechnologyInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(InfrastructureNode infrastructureNode) { + if (StringUtils.isNullOrEmpty(infrastructureNode.getTechnology())) { + return mediumPriorityRecommendation("Add a technology to the " + terminologyFor(infrastructureNode).toLowerCase() + " named \"" + infrastructureNode.getName() + "\"."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.infrastructurenode.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ModelInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ModelInspection.java new file mode 100644 index 000000000..4b8ab75ac --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ModelInspection.java @@ -0,0 +1,24 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Inspection; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Element; + +abstract class ModelInspection extends Inspection { + + public ModelInspection(Workspace workspace) { + super(workspace); + } + + public final Recommendation run() { + if (isEnabled(getType(), getWorkspace(), getWorkspace().getModel())) { + return inspect(getWorkspace()); + } + + return noRecommendation(); + } + + protected abstract Recommendation inspect(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspection.java new file mode 100644 index 000000000..34fc04be9 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspection.java @@ -0,0 +1,34 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; + +public class MultipleSoftwareSystemsDetailedInspection extends ModelInspection { + + public MultipleSoftwareSystemsDetailedInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Workspace workspace) { + int softwareSystemsWithDetails = 0; + for (SoftwareSystem softwareSystem : workspace.getModel().getSoftwareSystems()) { + if (softwareSystem.hasContainers() || !softwareSystem.getDocumentation().isEmpty()) { + softwareSystemsWithDetails++; + } + } + + if (softwareSystemsWithDetails > 1) { + return highPriorityRecommendation("This workspace describes the internal details of " + softwareSystemsWithDetails + " software systems. It is recommended that a workspace contains the model, views, and documentation for a single software system only."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/OrphanedElementInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/OrphanedElementInspection.java new file mode 100644 index 000000000..7cc09eb51 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/OrphanedElementInspection.java @@ -0,0 +1,44 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +import java.util.HashSet; +import java.util.Set; + +public class OrphanedElementInspection extends ElementInspection { + + private final Set elementsWithRelationships = new HashSet<>(); + + public OrphanedElementInspection(Workspace workspace) { + super(workspace); + + for (Relationship relationship : workspace.getModel().getRelationships()) { + elementsWithRelationships.add(relationship.getSourceId()); + elementsWithRelationships.add(relationship.getDestinationId()); + } + } + + @Override + protected Recommendation inspect(Element element) { + if (element instanceof DeploymentNode) { + // deployment nodes typically won't have relationships to/from them + return noRecommendation(); + } + + if (!elementsWithRelationships.contains(element.getId())) { + return mediumPriorityRecommendation("The " + terminologyFor(element).toLowerCase() + " named \"" + element.getName() + "\" is orphaned - add a relationship to/from it, or consider removing it from the model."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.element.orphaned"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/PersonDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/PersonDescriptionInspection.java new file mode 100644 index 000000000..9f88eca21 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/PersonDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; + +public class PersonDescriptionInspection extends ElementDescriptionInspection { + + public PersonDescriptionInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected String getType() { + return "model.person.description"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipDescriptionInspection.java new file mode 100644 index 000000000..7c8bb9aa6 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipDescriptionInspection.java @@ -0,0 +1,37 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Component; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; + +public class RelationshipDescriptionInspection extends RelationshipInspection { + + public RelationshipDescriptionInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Relationship relationship) { + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + if (StringUtils.isNullOrEmpty(relationship.getDescription())) { + if (source instanceof Component && destination instanceof Component) { + return lowPriorityRecommendation("Add a description to the relationship between the " + terminologyFor(source) + " named \"" + source.getName() + "\" and the " + terminologyFor(destination) + " named \"" + destination.getName() + "\"."); + } else { + return mediumPriorityRecommendation("Add a description to the relationship between the " + terminologyFor(source) + " named \"" + source.getName() + "\" and the " + terminologyFor(destination) + " named \"" + destination.getName() + "\"."); + } + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.relationship.description"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipInspection.java new file mode 100644 index 000000000..406d80b41 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipInspection.java @@ -0,0 +1,36 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Inspection; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; + +public abstract class RelationshipInspection extends Inspection { + + public RelationshipInspection(Workspace workspace) { + super(workspace); + } + + public final Recommendation run(Relationship relationship) { + Element source = relationship.getSource(); + Relationship linkedRelationship = null; + if (!StringUtils.isNullOrEmpty(relationship.getLinkedRelationshipId())) { + getWorkspace().getModel().getRelationship(relationship.getLinkedRelationshipId()); + } + + if (isEnabled(getType(), getWorkspace(), getWorkspace().getModel(), source.getParent(), source, linkedRelationship, relationship)) { + return inspect(relationship); + } + + return noRecommendation(); + } + + protected String terminologyFor(Element element) { + return getWorkspace().getViews().getConfiguration().getTerminology().findTerminology(element).toLowerCase(); + } + + protected abstract Recommendation inspect(Relationship relationship); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipTechnologyInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipTechnologyInspection.java new file mode 100644 index 000000000..b563a70cc --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/RelationshipTechnologyInspection.java @@ -0,0 +1,37 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; + +public class RelationshipTechnologyInspection extends RelationshipInspection { + + public RelationshipTechnologyInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(Relationship relationship) { + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + if (StringUtils.isNullOrEmpty(relationship.getTechnology())) { + if (source instanceof Container && destination instanceof Container) { + return mediumPriorityRecommendation("Add a technology to the relationship between the " + terminologyFor(source) + " named \"" + source.getName() + "\" and the " + terminologyFor(destination) + " named \"" + destination.getName() + "\"."); + } else { + return lowPriorityRecommendation("Add a technology to the relationship between the " + terminologyFor(source) + " named \"" + source.getName() + "\" and the " + terminologyFor(destination) + " named \"" + destination.getName() + "\"."); + } + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.relationship.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspection.java new file mode 100644 index 000000000..abc505cc1 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; + +public class SoftwareSystemDescriptionInspection extends ElementDescriptionInspection { + + public SoftwareSystemDescriptionInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected String getType() { + return "model.softwaresystem.description"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspection.java new file mode 100644 index 000000000..2e33015c0 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspection.java @@ -0,0 +1,34 @@ +package com.structurizr.assistant.view; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.view.ContainerView; + +import java.util.HashSet; +import java.util.Set; + +public class ContainerViewsForMultipleSoftwareSystemsInspection extends ViewsInspection { + + public ContainerViewsForMultipleSoftwareSystemsInspection(Workspace workspace) { + super(workspace); + } + + @Override + public Recommendation inspect(Workspace workspace) { + Set softwareSystemsWithContainerViews = new HashSet<>(); + for (ContainerView view : workspace.getViews().getContainerViews()) { + softwareSystemsWithContainerViews.add(view.getSoftwareSystemId()); + } + if (softwareSystemsWithContainerViews.size() > 1) { + return highPriorityRecommendation("Container views exist for " + softwareSystemsWithContainerViews.size() + " software systems. It is recommended that a workspace includes container views for a single software system only."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/view/EmptyViewsInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/EmptyViewsInspection.java new file mode 100644 index 000000000..b00c8ad4b --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/EmptyViewsInspection.java @@ -0,0 +1,26 @@ +package com.structurizr.assistant.view; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; + +public class EmptyViewsInspection extends ViewsInspection { + + public EmptyViewsInspection(Workspace workspace) { + super(workspace); + } + + @Override + public Recommendation inspect(Workspace workspace) { + if (workspace.getViews().isEmpty()) { + return highPriorityRecommendation("Add some views to the workspace."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "views.empty"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspection.java new file mode 100644 index 000000000..4e7c926a5 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspection.java @@ -0,0 +1,34 @@ +package com.structurizr.assistant.view; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.view.SystemContextView; + +import java.util.HashSet; +import java.util.Set; + +public class SystemContextViewsForMultipleSoftwareSystemsInspection extends ViewsInspection { + + public SystemContextViewsForMultipleSoftwareSystemsInspection(Workspace workspace) { + super(workspace); + } + + @Override + public Recommendation inspect(Workspace workspace) { + Set softwareSystemsWithSystemContextViews = new HashSet<>(); + for (SystemContextView view : workspace.getViews().getSystemContextViews()) { + softwareSystemsWithSystemContextViews.add(view.getSoftwareSystemId()); + } + if (softwareSystemsWithSystemContextViews.size() > 1) { + return highPriorityRecommendation("System context views exist for " + softwareSystemsWithSystemContextViews.size() + " software systems. It is recommended that a workspace includes system context views for a single software system only."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/view/ViewsInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/ViewsInspection.java new file mode 100644 index 000000000..bd7af5b0b --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/view/ViewsInspection.java @@ -0,0 +1,23 @@ +package com.structurizr.assistant.view; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Inspection; +import com.structurizr.assistant.Recommendation; + +abstract class ViewsInspection extends Inspection { + + public ViewsInspection(Workspace workspace) { + super(workspace); + } + + public final Recommendation run() { + if (isEnabled(getType(), getWorkspace(), getWorkspace().getViews().getConfiguration())) { + return inspect(getWorkspace()); + } + + return noRecommendation(); + } + + protected abstract Recommendation inspect(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceInspection.java new file mode 100644 index 000000000..1e6e5c63c --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceInspection.java @@ -0,0 +1,23 @@ +package com.structurizr.assistant.workspace; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Inspection; +import com.structurizr.assistant.Recommendation; + +abstract class WorkspaceInspection extends Inspection { + + public WorkspaceInspection(Workspace workspace) { + super(workspace); + } + + public final Recommendation run() { + if (isEnabled(getType(), getWorkspace())) { + return inspect(getWorkspace()); + } + + return null; + } + + protected abstract Recommendation inspect(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceScopeInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceScopeInspection.java new file mode 100644 index 000000000..076edf608 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/workspace/WorkspaceScopeInspection.java @@ -0,0 +1,26 @@ +package com.structurizr.assistant.workspace; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; + +public class WorkspaceScopeInspection extends WorkspaceInspection { + + public WorkspaceScopeInspection(Workspace workspace) { + super(workspace); + } + + @Override + public Recommendation inspect(Workspace workspace) { + if (workspace.getConfiguration().getScope() == null) { + return highPriorityRecommendation("This workspace has no defined scope. It is recommended that the workspace scope is set to \"Landscape\" or \"SoftwareSystem\"."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java new file mode 100644 index 000000000..eb17e0c0f --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java @@ -0,0 +1,41 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ComponentDescriptionInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Name"); + + Recommendation recommendation = new ComponentDescriptionInspection(workspace).run(component); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.component.description", recommendation.getType()); + assertEquals("Add a description to the component named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Name", "Description"); + + Recommendation recommendation = new ComponentDescriptionInspection(workspace).run(component); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java new file mode 100644 index 000000000..f5f1826a2 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java @@ -0,0 +1,41 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ComponentTechnologyInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Name"); + + Recommendation recommendation = new ComponentTechnologyInspection(workspace).run(component); + Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.component.technology", recommendation.getType()); + assertEquals("Add a technology to the component named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Name", "Description", "Technology"); + + Recommendation recommendation = new ComponentTechnologyInspection(workspace).run(component); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java new file mode 100644 index 000000000..9b0655c04 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java @@ -0,0 +1,38 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ContainerDescriptionInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name"); + + Recommendation recommendation = new ContainerDescriptionInspection(workspace).run(container); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.container.description", recommendation.getType()); + assertEquals("Add a description to the container named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name", "Description"); + + Recommendation recommendation = new ContainerDescriptionInspection(workspace).run(container); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java new file mode 100644 index 000000000..3f2e08854 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java @@ -0,0 +1,38 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ContainerTechnologyInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name"); + + Recommendation recommendation = new ContainerTechnologyInspection(workspace).run(container); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.container.technology", recommendation.getType()); + assertEquals("Add a technology to the container named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name", "Description", "Technology"); + + Recommendation recommendation = new ContainerTechnologyInspection(workspace).run(container); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java new file mode 100644 index 000000000..e495234e2 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java @@ -0,0 +1,35 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.DeploymentNode; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class DeploymentNodeDescriptionInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name"); + + Recommendation recommendation = new DeploymentNodeDescriptionInspection(workspace).run(deploymentNode); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.deploymentnode.description", recommendation.getType()); + assertEquals("Add a description to the deployment node named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name", "Description", "Technology"); + + Recommendation recommendation = new DeploymentNodeDescriptionInspection(workspace).run(deploymentNode); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java new file mode 100644 index 000000000..5c0aa5660 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java @@ -0,0 +1,35 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.DeploymentNode; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class DeploymentNodeTechnologyInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name"); + + Recommendation recommendation = new DeploymentNodeTechnologyInspection(workspace).run(deploymentNode); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.deploymentnode.technology", recommendation.getType()); + assertEquals("Add a technology to the deployment node named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name", "Description", "Technology"); + + Recommendation recommendation = new DeploymentNodeTechnologyInspection(workspace).run(deploymentNode); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java new file mode 100644 index 000000000..d92e2913a --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java @@ -0,0 +1,36 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ElementNotIncludedInAnyViewsInspectionTests { + + @Test + public void run_NotInViews() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + + Recommendation recommendation = new ElementNotIncludedInAnyViewsInspection(workspace).run(softwareSystem); + Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.element.noview", recommendation.getType()); + assertEquals("The software system named \"Name\" is not included on any views - add it to a view.", recommendation.getDescription()); + } + + @Test + public void run_InViews() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + workspace.getViews().createSystemLandscapeView("key", "Description").addAllElements(); + + Recommendation recommendation = new ElementNotIncludedInAnyViewsInspection(workspace).run(softwareSystem); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java new file mode 100644 index 000000000..2bde35613 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java @@ -0,0 +1,71 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class EmptyDeploymentNodeCheckTests { + + @Test + public void run_Empty() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name"); + + Recommendation recommendation = new EmptyDeploymentNodeInspection(workspace).run(deploymentNode); + Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.deploymentnode.empty", recommendation.getType()); + assertEquals("The deployment node named \"Name\" is empty.", recommendation.getDescription()); + } + + @Test + public void run_WithDeploymentNode() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.addDeploymentNode("Deployment Node"); + + Recommendation recommendation = new EmptyDeploymentNodeInspection(workspace).run(deploymentNode); + assertNull(recommendation); + } + + @Test + public void run_WithInfrastructureNode() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.addInfrastructureNode("Infrastructure Node"); + + Recommendation recommendation = new EmptyDeploymentNodeInspection(workspace).run(deploymentNode); + assertNull(recommendation); + } + + @Test + public void run_WithSoftwareSystemInstance() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.add(softwareSystem); + + Recommendation recommendation = new EmptyDeploymentNodeInspection(workspace).run(deploymentNode); + assertNull(recommendation); + } + + @Test + public void run_WithContainerInstance() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.add(container); + + Recommendation recommendation = new EmptyDeploymentNodeInspection(workspace).run(deploymentNode); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java new file mode 100644 index 000000000..e022667e6 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java @@ -0,0 +1,33 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class EmptyModelInspectionTests { + + @Test + public void run_WhenThereAreNoElements() { + Workspace workspace = new Workspace("Name", "Description"); + + Recommendation recommendation = new EmptyModelInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.empty", recommendation.getType()); + assertEquals("Add some elements to the model.", recommendation.getDescription()); + } + + @Test + public void run_WhenThereAreElements() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Name"); + + Recommendation recommendation = new EmptyModelInspection(workspace).run(); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java new file mode 100644 index 000000000..b9257dd0c --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java @@ -0,0 +1,37 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.InfrastructureNode; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class InfrastructureNodeDescriptionInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + InfrastructureNode infrastructureNode = workspace.getModel().addDeploymentNode("Deployment Node") + .addInfrastructureNode("Name"); + + Recommendation recommendation = new InfrastructureNodeDescriptionInspection(workspace).run(infrastructureNode); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.infrastructurenode.description", recommendation.getType()); + assertEquals("Add a description to the infrastructure node named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + InfrastructureNode infrastructureNode = workspace.getModel().addDeploymentNode("Deployment Node") + .addInfrastructureNode("Name", "Description", "Technology"); + + Recommendation recommendation = new InfrastructureNodeDescriptionInspection(workspace).run(infrastructureNode); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java new file mode 100644 index 000000000..fd990cdae --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java @@ -0,0 +1,37 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.InfrastructureNode; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class InfrastructureNodeTechnologyInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + InfrastructureNode infrastructureNode = workspace.getModel().addDeploymentNode("Deployment Node") + .addInfrastructureNode("Name"); + + Recommendation recommendation = new InfrastructureNodeTechnologyInspection(workspace).run(infrastructureNode); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.infrastructurenode.technology", recommendation.getType()); + assertEquals("Add a technology to the infrastructure node named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + InfrastructureNode infrastructureNode = workspace.getModel().addDeploymentNode("Deployment Node") + .addInfrastructureNode("Name", "Description", "Technology"); + + Recommendation recommendation = new InfrastructureNodeTechnologyInspection(workspace).run(infrastructureNode); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java new file mode 100644 index 000000000..3dcce3c0d --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java @@ -0,0 +1,79 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class MultipleSoftwareSystemsDetailedInspectionTests { + + @Test + public void run_MultipleSoftwareSystemsWithContainers() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").addContainer("Container"); + workspace.getModel().addSoftwareSystem("B").addContainer("Container"); + + Recommendation recommendation = new MultipleSoftwareSystemsDetailedInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); + assertEquals("This workspace describes the internal details of 2 software systems. It is recommended that a workspace contains the model, views, and documentation for a single software system only.", recommendation.getDescription()); + } + + @Test + public void run_MultipleSoftwareSystemsWithDocumentation() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").getDocumentation().addSection(new Section(Format.Markdown, "# Section 1")); + workspace.getModel().addSoftwareSystem("B").getDocumentation().addSection(new Section(Format.Markdown, "# Section 1")); + + Recommendation recommendation = new MultipleSoftwareSystemsDetailedInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); + assertEquals("This workspace describes the internal details of 2 software systems. It is recommended that a workspace contains the model, views, and documentation for a single software system only.", recommendation.getDescription()); + } + + @Test + public void run_MultipleSoftwareSystemsWithDecisions() { + Workspace workspace = new Workspace("Name", "Description"); + Decision decision = new Decision("1"); + decision.setTitle("Decision 1"); + decision.setFormat(Format.Markdown); + decision.setContent("Content"); + decision.setStatus("Accepted"); + workspace.getModel().addSoftwareSystem("A").getDocumentation().addDecision(decision); + workspace.getModel().addSoftwareSystem("B").getDocumentation().addDecision(decision); + + Recommendation recommendation = new MultipleSoftwareSystemsDetailedInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); + assertEquals("This workspace describes the internal details of 2 software systems. It is recommended that a workspace contains the model, views, and documentation for a single software system only.", recommendation.getDescription()); + } + + @Test + public void run_SingleSoftwareSystem() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + a.addContainer("Container"); + + a.getDocumentation().addSection(new Section(Format.Markdown, "# Section 1")); + + Decision decision = new Decision("1"); + decision.setTitle("Decision 1"); + decision.setFormat(Format.Markdown); + decision.setContent("Content"); + decision.setStatus("Accepted"); + + a.getDocumentation().addDecision(decision); + + Recommendation recommendation = new MultipleSoftwareSystemsDetailedInspection(workspace).run(); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java new file mode 100644 index 000000000..a92153dea --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java @@ -0,0 +1,40 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class OrphanedElementInspectionTests { + + @Test + public void run_WithOrphan() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + + Recommendation recommendation = new OrphanedElementInspection(workspace).run(a); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.element.orphaned", recommendation.getType()); + assertEquals("The software system named \"A\" is orphaned - add a relationship to/from it, or consider removing it from the model.", recommendation.getDescription()); + } + + @Test + public void run_WithoutOrphan() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + a.uses(b, "Uses"); + + Recommendation recommendation = new OrphanedElementInspection(workspace).run(a); + assertNull(recommendation); + + recommendation = new OrphanedElementInspection(workspace).run(b); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java new file mode 100644 index 000000000..03a4b07e6 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java @@ -0,0 +1,35 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Person; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class PersonDescriptionInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + Person person = workspace.getModel().addPerson("Name"); + + Recommendation recommendation = new PersonDescriptionInspection(workspace).run(person); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.person.description", recommendation.getType()); + assertEquals("Add a description to the person named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + Person person = workspace.getModel().addPerson("Name", "Description"); + + Recommendation recommendation = new PersonDescriptionInspection(workspace).run(person); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java new file mode 100644 index 000000000..69b46f4af --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java @@ -0,0 +1,57 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class RelationshipDescriptionInspectionTests { + + @Test + public void run_WithoutDescription_MediumPriority() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, ""); + + Recommendation recommendation = new RelationshipDescriptionInspection(workspace).run(relationship); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.relationship.description", recommendation.getType()); + assertEquals("Add a description to the relationship between the software system named \"A\" and the software system named \"B\".", recommendation.getDescription()); + } + + @Test + public void run_WithoutDescription_LowPriority() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component a = container.addComponent("A"); + Component b = container.addComponent("B"); + Relationship relationship = a.uses(b, ""); + + Recommendation recommendation = new RelationshipDescriptionInspection(workspace).run(relationship); + Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.relationship.description", recommendation.getType()); + assertEquals("Add a description to the relationship between the component named \"A\" and the component named \"B\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Description"); + + Recommendation recommendation = new RelationshipDescriptionInspection(workspace).run(relationship); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java new file mode 100644 index 000000000..2fe68d6c5 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java @@ -0,0 +1,55 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class RelationshipTechnologyInspectionTests { + + @Test + public void run_WithoutDescription_LowPriority() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Description"); + + Recommendation recommendation = new RelationshipTechnologyInspection(workspace).run(relationship); + Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.relationship.technology", recommendation.getType()); + assertEquals("Add a technology to the relationship between the software system named \"A\" and the software system named \"B\".", recommendation.getDescription()); + } + + @Test + public void run_WithoutDescription_MediumPriority() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container a = softwareSystem.addContainer("A"); + Container b = softwareSystem.addContainer("B"); + Relationship relationship = a.uses(b, "Description"); + + Recommendation recommendation = new RelationshipTechnologyInspection(workspace).run(relationship); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.relationship.technology", recommendation.getType()); + assertEquals("Add a technology to the relationship between the container named \"A\" and the container named \"B\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Description", "Technology"); + + Recommendation recommendation = new RelationshipTechnologyInspection(workspace).run(relationship); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java new file mode 100644 index 000000000..49389b30d --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java @@ -0,0 +1,35 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class SoftwareSystemDescriptionInspectionTests { + + @Test + public void run_WithoutDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + + Recommendation recommendation = new SoftwareSystemDescriptionInspection(workspace).run(softwareSystem); + Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.softwaresystem.description", recommendation.getType()); + assertEquals("Add a description to the software system named \"Name\".", recommendation.getDescription()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + + Recommendation recommendation = new SoftwareSystemDescriptionInspection(workspace).run(softwareSystem); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java new file mode 100644 index 000000000..8b389b481 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java @@ -0,0 +1,40 @@ +package com.structurizr.assistant.view; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ContainerViewsForMultipleSoftwareSystemsInspectionTests { + + @Test + public void run_MultipleSoftwareSystems() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + workspace.getViews().createContainerView(a, "Containers-A", "Description"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + workspace.getViews().createContainerView(b, "Containers-B", "Description"); + + Recommendation recommendation = new ContainerViewsForMultipleSoftwareSystemsInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); + assertEquals("Container views exist for 2 software systems. It is recommended that a workspace includes container views for a single software system only.", recommendation.getDescription()); + } + + @Test + public void run_SingleSoftwareSystem() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + workspace.getViews().createContainerView(a, "Containers-A", "Description"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + + Recommendation recommendation = new ContainerViewsForMultipleSoftwareSystemsInspection(workspace).run(); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java new file mode 100644 index 000000000..2d5c9b40c --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java @@ -0,0 +1,33 @@ +package com.structurizr.assistant.view; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class EmptyViewsInspectionTests { + + @Test + public void run_WhenThereAreNoViews() { + Workspace workspace = new Workspace("Name", "Description"); + + Recommendation recommendation = new EmptyViewsInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.views.empty", recommendation.getType()); + assertEquals("Add some views to the workspace.", recommendation.getDescription()); + } + + @Test + public void run_WhenThereAreViews() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createSystemLandscapeView("key", "Description"); + + Recommendation recommendation = new EmptyViewsInspection(workspace).run(); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java new file mode 100644 index 000000000..48ec21171 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java @@ -0,0 +1,40 @@ +package com.structurizr.assistant.view; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class SystemContextViewsForMultipleSoftwareSystemsInspectionTests { + + @Test + public void run_MultipleSoftwareSystems() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + workspace.getViews().createSystemContextView(a, "SystemContext-A", "Description"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + workspace.getViews().createSystemContextView(b, "SystemContext-B", "Description"); + + Recommendation recommendation = new SystemContextViewsForMultipleSoftwareSystemsInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); + assertEquals("System context views exist for 2 software systems. It is recommended that a workspace includes system context views for a single software system only.", recommendation.getDescription()); + } + + @Test + public void run_SingleSoftwareSystem() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + workspace.getViews().createSystemContextView(a, "SystemContext-A", "Description"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + + Recommendation recommendation = new SystemContextViewsForMultipleSoftwareSystemsInspection(workspace).run(); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java new file mode 100644 index 000000000..d48e93c77 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java @@ -0,0 +1,34 @@ +package com.structurizr.assistant.workspace; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Priority; +import com.structurizr.assistant.Recommendation; +import com.structurizr.configuration.WorkspaceScope; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class WorkspaceScopeInspectionTests { + + @Test + public void run_WithUnscopedWorkspace() { + Workspace workspace = new Workspace("Name", "Description"); + + Recommendation recommendation = new WorkspaceScopeInspection(workspace).run(); + Assertions.assertEquals(Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); + assertEquals("This workspace has no defined scope. It is recommended that the workspace scope is set to \"Landscape\" or \"SoftwareSystem\".", recommendation.getDescription()); + } + + @Test + public void run_WithScopedWorkspace() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getConfiguration().setScope(WorkspaceScope.SoftwareSystem); + + Recommendation recommendation = new WorkspaceScopeInspection(workspace).run(); + assertNull(recommendation); + } + +} diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java index 29bbb8332..d12adf3a0 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -296,8 +296,6 @@ public void putWorkspace(long workspaceId, Workspace workspace) throws Structuri workspace.setLastModifiedAgent(agent); workspace.setLastModifiedUser(getUser()); - workspace.countAndLogWarnings(); - HttpPut httpPut = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId); StringWriter stringWriter = new StringWriter(); diff --git a/structurizr-core/src/main/java/com/structurizr/Workspace.java b/structurizr-core/src/main/java/com/structurizr/Workspace.java index 8dd320eb7..d48ead6e1 100644 --- a/structurizr-core/src/main/java/com/structurizr/Workspace.java +++ b/structurizr-core/src/main/java/com/structurizr/Workspace.java @@ -163,74 +163,6 @@ public boolean isEmpty() { return model.isEmpty() && viewSet.isEmpty() && documentation.isEmpty(); } - /** - * Counts and logs any warnings within the workspace (e.g. missing element descriptions). - * - * @return the number of warnings - */ - public int countAndLogWarnings() { - final List warnings = new LinkedList<>(); - - // find elements with a missing description - getModel().getElements().stream() - .filter(e -> !(e instanceof SoftwareSystemInstance) && !(e instanceof ContainerInstance) && !(e instanceof DeploymentNode) && !(e instanceof InfrastructureNode)) - .filter(e -> e.getDescription() == null || e.getDescription().trim().length() == 0) - .forEach(e -> warnings.add(e.getCanonicalName() + " is missing a description.")); - - // find containers with a missing technology - getModel().getElements().stream() - .filter(e -> e instanceof Container) - .map(e -> (Container)e) - .filter(c -> c.getTechnology() == null || c.getTechnology().trim().length() == 0) - .forEach(c -> warnings.add(c.getCanonicalName() + " is missing a technology.")); - - // find components with a missing technology - getModel().getElements().stream() - .filter(e -> e instanceof Component) - .map(e -> (Component)e) - .filter(c -> c.getTechnology() == null || c.getTechnology().trim().length() == 0) - .forEach(c -> warnings.add(c.getCanonicalName() + " is missing a technology.")); - - // find component relationships with a missing description - for (Relationship relationship : getModel().getRelationships()) { - if (relationship.getSource() instanceof Component && relationship.getDestination() instanceof Component && - relationship.getSource().getParent().equals(relationship.getDestination().getParent())) { - // ignore component-component relationships inside the same container because these are - // often identified using reflection and won't have a description - // (i.e. let's not flood the user with warnings) - } else { - if (relationship.getDescription() == null || relationship.getDescription().trim().length() == 0) { - warnings.add("The relationship between " + relationship.getSource().getCanonicalName() + " and " + relationship.getDestination().getCanonicalName() + " is missing a description."); - } - } - } - - // diagram keys have not been specified - this is only applicable to - // workspaces created with the early versions of Structurizr for Java - getViews().getSystemLandscapeViews().stream() - .filter(v -> v.getKey() == null) - .forEach(v -> warnings.add("System Landscape view \"" + v.getName() + "\": Missing key")); - getViews().getSystemContextViews().stream() - .filter(v -> v.getKey() == null) - .forEach(v -> warnings.add("System Context view \"" + v.getName() + "\": Missing key")); - getViews().getContainerViews().stream() - .filter(v -> v.getKey() == null) - .forEach(v -> warnings.add("Container view \"" + v.getName() + "\": Missing key")); - getViews().getComponentViews().stream() - .filter(v -> v.getKey() == null) - .forEach(v -> warnings.add("Component view \"" + v.getName() + "\": Missing key")); - getViews().getDynamicViews().stream() - .filter(v -> v.getKey() == null) - .forEach(v -> warnings.add("Dynamic view \"" + v.getName() + "\": Missing key")); - getViews().getDeploymentViews().stream() - .filter(v -> v.getKey() == null) - .forEach(v -> warnings.add("Deployment view \"" + v.getName() + "\": Missing key")); - - warnings.forEach(log::warn); - - return warnings.size(); - } - /** * Trims the workspace by removing all unused elements. */ diff --git a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java index 4a5230338..d0f5f979e 100644 --- a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java @@ -1,10 +1,8 @@ package com.structurizr; -import com.structurizr.configuration.WorkspaceScope; import com.structurizr.documentation.Decision; import com.structurizr.documentation.Format; import com.structurizr.model.*; -import com.structurizr.view.SystemContextView; import com.structurizr.view.SystemLandscapeView; import org.junit.jupiter.api.Test; @@ -46,24 +44,6 @@ void isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { assertFalse(workspace.isEmpty()); } - @Test - void countAndLogWarnings() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1", null); - SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2", " "); - Container container1 = softwareSystem1.addContainer("Name", "Description", null); - Container container2 = softwareSystem2.addContainer("Name", "Description", " "); - container1.uses(container2, null, null); - container2.uses(container1, " ", " "); - - Component component1A = container1.addComponent("A", null, null); - Component component1B = container1.addComponent("B", "", ""); - component1A.uses(component1B, null); - component1B.uses(component1A, ""); - - assertEquals(10, workspace.countAndLogWarnings()); - } - @Test void hydrate_DoesNotCrash() { Workspace workspace = new Workspace("Name", "Description"); From ad9db0f900fe3eb075677710d08552d68e8d8a7e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 9 Jan 2024 17:41:24 +0000 Subject: [PATCH 142/418] Missing assertion. --- .../src/test/java/com/structurizr/WorkspaceTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java index d0f5f979e..b5b741da1 100644 --- a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java @@ -253,6 +253,7 @@ void trim_WhenAllElementsAreUnused() { workspace.trim(); assertEquals(0, workspace.getModel().getElements().size()); + assertEquals(0, workspace.getModel().getRelationships().size()); } @Test From abdc7ec3bdeeb3775f13385704bbc2b4b4e7cc25 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 10 Jan 2024 08:34:31 +0000 Subject: [PATCH 143/418] Removes the deprecated "location" and "enterprise" concept. --- changelog.md | 1 + .../java/com/structurizr/model/Location.java | 8 - .../java/com/structurizr/model/Model.java | 74 +- .../java/com/structurizr/model/Person.java | 8 +- .../com/structurizr/model/SoftwareSystem.java | 8 +- .../com/structurizr/view/ComponentView.java | 13 +- .../com/structurizr/view/ContainerView.java | 13 +- .../com/structurizr/view/DynamicView.java | 13 +- .../structurizr/view/SystemContextView.java | 12 +- .../structurizr/view/SystemLandscapeView.java | 12 +- .../com/structurizr/view/Terminology.java | 2 +- .../java/com/structurizr/view/ViewSet.java | 4 - .../com/structurizr/model/PersonTests.java | 7 - .../view/SystemContextViewTests.java | 8 - .../view/SystemLandscapeViewTests.java | 8 - .../structurizr/dsl/ComponentViewParser.java | 1 - .../structurizr/dsl/ContainerViewParser.java | 1 - .../structurizr/dsl/DynamicViewParser.java | 2 - .../structurizr/dsl/EnterpriseDslContext.java | 23 - .../com/structurizr/dsl/EnterpriseParser.java | 30 - .../com/structurizr/dsl/ModelDslContext.java | 10 - .../com/structurizr/dsl/PersonParser.java | 5 - .../structurizr/dsl/SoftwareSystemParser.java | 5 - .../structurizr/dsl/StructurizrDslParser.java | 18 +- .../structurizr/dsl/StructurizrDslTokens.java | 1 - .../dsl/SystemContextViewParser.java | 5 +- .../dsl/SystemLandscapeViewParser.java | 5 +- .../dsl/TerminologyDslContext.java | 1 - .../structurizr/dsl/TerminologyParser.java | 9 - .../dsl/ComponentViewParserTests.java | 3 - .../dsl/ContainerViewParserTests.java | 3 - .../java/com/structurizr/dsl/DslTests.java | 16 - .../dsl/EnterpriseParserTests.java | 63 - .../structurizr/dsl/ModelDslContextTests.java | 77 - .../structurizr/dsl/PersonParserTests.java | 20 +- .../dsl/SoftwareSystemParserTests.java | 20 +- .../dsl/TerminologyParserTests.java | 17 - .../src/test/resources/dsl/big-bank-plc.dsl | 2 +- .../model/people-and-software-systems.dsl | 2 +- .../src/test/resources/dsl/groups-nested.dsl | 8 - .../src/test/resources/dsl/test.dsl | 3 +- .../export/AbstractDiagramExporter.java | 68 +- .../export/plantuml/C4PlantUMLExporter.java | 16 +- .../export/dot/36141-Components.dot | 48 +- .../export/dot/36141-Containers.dot | 34 +- .../dot/36141-DevelopmentDeployment.dot | 42 +- .../export/dot/36141-LiveDeployment.dot | 58 +- .../structurizr/export/dot/36141-SignIn.dot | 22 +- .../export/dot/36141-SystemContext.dot | 25 +- .../export/dot/36141-SystemLandscape.dot | 39 +- .../export/dot/groups-Components.dot | 4 +- .../export/dot/groups-Containers.dot | 4 +- .../export/dot/groups-SystemLandscape.dot | 41 +- .../export/ilograph/36141.ilograph | 428 +- .../export/mermaid/36141-Components.mmd | 68 +- .../export/mermaid/36141-Containers.mmd | 46 +- .../mermaid/36141-DevelopmentDeployment.mmd | 48 +- .../export/mermaid/36141-LiveDeployment.mmd | 72 +- .../export/mermaid/36141-SignIn-sequence.mmd | 20 +- .../export/mermaid/36141-SignIn.mmd | 32 +- .../export/mermaid/36141-SystemContext.mmd | 25 +- .../export/mermaid/36141-SystemLandscape.mmd | 40 +- .../export/mermaid/groups-Components.mmd | 2 +- .../export/mermaid/groups-Containers.mmd | 2 +- .../export/mermaid/groups-SystemLandscape.mmd | 24 +- ...ructurizrPlantUMLDiagramExporterTests.java | 6 +- .../plantuml/c4plantuml/36141-Components.puml | 12 +- .../plantuml/c4plantuml/36141-Containers.puml | 18 +- .../36141-DevelopmentDeployment.puml | 26 +- .../c4plantuml/36141-LiveDeployment.puml | 32 +- .../c4plantuml/36141-SignIn-sequence.puml | 4 +- .../plantuml/c4plantuml/36141-SignIn.puml | 6 +- .../c4plantuml/36141-SystemContext.puml | 14 +- .../c4plantuml/36141-SystemLandscape.puml | 19 +- .../c4plantuml/groups-Components.puml | 4 +- .../c4plantuml/groups-Containers.puml | 4 +- .../c4plantuml/groups-SystemLandscape.puml | 21 +- .../structurizr/36141-Components.puml | 12 +- .../structurizr/36141-Containers.puml | 12 +- .../36141-DevelopmentDeployment.puml | 14 +- .../structurizr/36141-LiveDeployment.puml | 16 +- .../plantuml/structurizr/36141-SignIn.puml | 6 +- .../structurizr/36141-SystemContext.puml | 13 +- .../structurizr/36141-SystemLandscape.puml | 19 +- .../structurizr/groups-Components.puml | 2 +- .../structurizr/groups-Containers.puml | 2 +- .../structurizr/groups-SystemLandscape.puml | 29 +- .../src/test/resources/groups.dsl | 59 + .../src/test/resources/groups.json | 142 +- .../structurizr-36141-workspace.json | 3549 ++++++++--------- .../graphviz/DOTExporterTests.java | 91 +- .../structurizr/graphviz/SVGReaderTests.java | 4 +- 92 files changed, 2495 insertions(+), 3390 deletions(-) delete mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java delete mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java delete mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java delete mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java create mode 100644 structurizr-export/src/test/resources/groups.dsl diff --git a/changelog.md b/changelog.md index cc90bf20c..7e95c9fb2 100644 --- a/changelog.md +++ b/changelog.md @@ -3,4 +3,5 @@ ## 2.0.0 (unreleased) - structurizr-core: Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). +- structurizr-core: Removes the deprecated location and enterprise concepts from `Model`. diff --git a/structurizr-core/src/main/java/com/structurizr/model/Location.java b/structurizr-core/src/main/java/com/structurizr/model/Location.java index e216c7f14..29b0193bc 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Location.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Location.java @@ -1,13 +1,5 @@ package com.structurizr.model; -/** - * Represents the location of an element with regards to a specific viewpoint. - * - * Diagram renderers may use this information in a different way, but generally it will be used to mark - * an element as being outside of the enterprise boundary. For example, "our customers are external to our enterprise": - * - https://github.com/structurizr/examples/blob/main/java/src/main/java/com/structurizr/example/bigbankplc/BigBankPlc.java#L36 - * - https://structurizr.com/share/28201/diagrams#SystemLandscape - */ public enum Location { Internal, diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 73ac3a65d..866faf7d0 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -33,23 +33,13 @@ public final class Model implements PropertyHolder { Model() { } - /** - * Gets the enterprise associated with this model. - * - * @return an Enterprise instance, or null if one has not been set - */ + @Deprecated public Enterprise getEnterprise() { return enterprise; } - /** - * Sets the enterprise associated with this model. - * This is typically used in conjunction with {@link Location}. - * - * @param enterprise an Enterprise instance - */ @Deprecated - public void setEnterprise(Enterprise enterprise) { + void setEnterprise(Enterprise enterprise) { this.enterprise = enterprise; } @@ -75,36 +65,6 @@ public SoftwareSystem addSoftwareSystem(@Nonnull String name) { public SoftwareSystem addSoftwareSystem(@Nonnull String name, @Nullable String description) { if (getSoftwareSystemWithName(name) == null) { SoftwareSystem softwareSystem = new SoftwareSystem(); - softwareSystem.setLocation(Location.Unspecified); - softwareSystem.setName(name); - softwareSystem.setDescription(description); - - softwareSystems.add(softwareSystem); - - softwareSystem.setId(idGenerator.generateId(softwareSystem)); - addElementToInternalStructures(softwareSystem); - - return softwareSystem; - } else { - throw new IllegalArgumentException("A top-level element named '" + name + "' already exists."); - } - } - - /** - * Creates a software system and adds it to the model. - * - * @param location the location of the software system (e.g. internal, external, etc) - * @param name the name of the software system - * @param description a short description of the software system - * @return the SoftwareSystem instance created and added to the model (or null) - * @throws IllegalArgumentException if a software system with the same name already exists - */ - @Nonnull - @Deprecated - public SoftwareSystem addSoftwareSystem(@Nullable Location location, @Nonnull String name, @Nullable String description) { - if (getSoftwareSystemWithName(name) == null) { - SoftwareSystem softwareSystem = new SoftwareSystem(); - softwareSystem.setLocation(location); softwareSystem.setName(name); softwareSystem.setDescription(description); @@ -143,36 +103,6 @@ public Person addPerson(@Nonnull String name) { public Person addPerson(@Nonnull String name, @Nullable String description) { if (getPersonWithName(name) == null) { Person person = new Person(); - person.setLocation(Location.Unspecified); - person.setName(name); - person.setDescription(description); - - people.add(person); - - person.setId(idGenerator.generateId(person)); - addElementToInternalStructures(person); - - return person; - } else { - throw new IllegalArgumentException("A top-level element named '" + name + "' already exists."); - } - } - - /** - * Creates a person and adds it to the model. - * - * @param location the location of the person (e.g. internal, external, etc) - * @param name the name of the person (e.g. "Admin User" or "Bob the Business User") - * @param description a short description of the person - * @return the Person instance created and added to the model (or null) - * @throws IllegalArgumentException if a person with the same name already exists - */ - @Nonnull - @Deprecated - public Person addPerson(Location location, @Nonnull String name, @Nullable String description) { - if (getPersonWithName(name) == null) { - Person person = new Person(); - person.setLocation(location); person.setName(name); person.setDescription(description); diff --git a/structurizr-core/src/main/java/com/structurizr/model/Person.java b/structurizr-core/src/main/java/com/structurizr/model/Person.java index 858463bae..dd9916897 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Person.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Person.java @@ -23,17 +23,13 @@ public Element getParent() { Person() { } - /** - * Gets the location of this person. - * - * @return a Location - */ + @Deprecated public Location getLocation() { return location; } @Deprecated - public void setLocation(Location location) { + void setLocation(Location location) { if (location != null) { this.location = location; } else { diff --git a/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java index 6b0135397..74cf5d85d 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java @@ -36,11 +36,7 @@ public Element getParent() { SoftwareSystem() { } - /** - * Gets the location of this software system. - * - * @return a Location instance - */ + @Deprecated public Location getLocation() { return location; } @@ -51,7 +47,7 @@ public Location getLocation() { * @param location a Location instance */ @Deprecated - public void setLocation(Location location) { + void setLocation(Location location) { if (location != null) { this.location = location; } else { diff --git a/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java index f3eb65b7a..d03756b37 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java @@ -294,22 +294,13 @@ protected boolean canBeRemoved(Element element) { return true; } - /** - * Determines whether container boundaries should be visible for "external" components (those outside the container in scope). - * - * @return true if external container boundaries are visible, false otherwise - */ + @Deprecated public boolean getExternalContainerBoundariesVisible() { return externalContainerBoundariesVisible; } - /** - * Sets whether container boundaries should be visible for "external" components (those outside the container in scope). - * - * @param externalContainerBoundariesVisible true if external container boundaries should be visible, false otherwise - */ @Deprecated - public void setExternalSoftwareSystemBoundariesVisible(boolean externalContainerBoundariesVisible) { + void setExternalSoftwareSystemBoundariesVisible(boolean externalContainerBoundariesVisible) { this.externalContainerBoundariesVisible = externalContainerBoundariesVisible; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java index 3536b96fd..b4d02fb9a 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java @@ -200,22 +200,13 @@ protected boolean canBeRemoved(Element element) { return true; } - /** - * Determines whether software system boundaries should be visible for "external" containers (those outside the software system in scope). - * - * @return true if external software system boundaries are visible, false otherwise - */ + @Deprecated public boolean getExternalSoftwareSystemBoundariesVisible() { return externalSoftwareSystemBoundariesVisible; } - /** - * Sets whether software system boundaries should be visible for "external" containers (those outside the software system in scope). - * - * @param externalSoftwareSystemBoundariesVisible true if external software system boundaries should be visible, false otherwise - */ @Deprecated - public void setExternalSoftwareSystemBoundariesVisible(boolean externalSoftwareSystemBoundariesVisible) { + void setExternalSoftwareSystemBoundariesVisible(boolean externalSoftwareSystemBoundariesVisible) { this.externalSoftwareSystemBoundariesVisible = externalSoftwareSystemBoundariesVisible; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java b/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java index 79331d3ce..04d0fbdfb 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java @@ -372,22 +372,13 @@ private boolean isNumeric(String str) { } } - /** - * Determines whether software system/container boundaries should be visible for "external" containers/components (those outside the element in scope). - * - * @return true if external boundaries are visible, false otherwise - */ + @Deprecated public boolean getExternalBoundariesVisible() { return externalBoundariesVisible; } - /** - * Sets whether software system/container boundaries should be visible for "external" containers/components (those outside the element in scope). - * - * @param externalBoundariesVisible true if external boundaries should be visible, false otherwise - */ @Deprecated - public void setExternalBoundariesVisible(boolean externalBoundariesVisible) { + void setExternalBoundariesVisible(boolean externalBoundariesVisible) { this.externalBoundariesVisible = externalBoundariesVisible; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java index a96595596..bef228fd8 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java @@ -72,23 +72,13 @@ public void addNearestNeighbours(@Nonnull Element element) { } } - /** - * Determines whether the enterprise boundary (to differentiate "internal" elements from "external" elements") should be visible on the resulting diagram. - * - * @return true if the enterprise boundary is visible, false otherwise - */ @Deprecated public boolean isEnterpriseBoundaryVisible() { return enterpriseBoundaryVisible; } - /** - * Sets whether the enterprise boundary (to differentiate "internal" elements from "external" elements") should be visible on the resulting diagram. - * - * @param enterpriseBoundaryVisible true if the enterprise boundary should be visible, false otherwise - */ @Deprecated - public void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { + void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { this.enterpriseBoundaryVisible = enterpriseBoundaryVisible; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java index d040911fe..ce719e07f 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java @@ -88,23 +88,13 @@ public void addNearestNeighbours(@Nonnull Element element) { } } - /** - * Determines whether the enterprise boundary (to differentiate "internal" elements from "external" elements") should be visible on the resulting diagram. - * - * @return true if the enterprise boundary is visible, false otherwise - */ @Deprecated public boolean isEnterpriseBoundaryVisible() { return enterpriseBoundaryVisible; } - /** - * Sets whether the enterprise boundary (to differentiate "internal" elements from "external" elements") should be visible on the resulting diagram. - * - * @param enterpriseBoundaryVisible true if the enterprise boundary should be visible, false otherwise - */ @Deprecated - public void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { + void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { this.enterpriseBoundaryVisible = enterpriseBoundaryVisible; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/Terminology.java b/structurizr-core/src/main/java/com/structurizr/view/Terminology.java index f9c0fbc2b..b8daf3e1e 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Terminology.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Terminology.java @@ -41,7 +41,7 @@ public String getEnterprise() { } @Deprecated - public void setEnterprise(String enterprise) { + void setEnterprise(String enterprise) { this.enterprise = enterprise; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java index 79559dd62..f33690f93 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -854,7 +854,6 @@ public void createDefaultViews() { SystemLandscapeView systemLandscapeView = createSystemLandscapeView(generateViewKey(SYSTEM_LANDSCAPE_VIEW_TYPE), ""); systemLandscapeView.addDefaultElements(); systemLandscapeView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); - systemLandscapeView.setEnterpriseBoundaryVisible(true); if (!model.getSoftwareSystems().isEmpty()) { List softwareSystems = new ArrayList<>(model.getSoftwareSystems()); @@ -866,7 +865,6 @@ public void createDefaultViews() { SystemContextView systemContextView = createSystemContextView(softwareSystem, systemContextViewKey, ""); systemContextView.addDefaultElements(); systemContextView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); - systemContextView.setEnterpriseBoundaryVisible(true); if (softwareSystem.getContainers().size() > 0) { List containers = new ArrayList<>(softwareSystem.getContainers()); @@ -876,7 +874,6 @@ public void createDefaultViews() { ContainerView containerView = createContainerView(softwareSystem, containerViewKey, ""); containerView.addDefaultElements(); containerView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); - containerView.setExternalSoftwareSystemBoundariesVisible(true); for (Container container : containers) { if (container.getComponents().size() > 0) { @@ -884,7 +881,6 @@ public void createDefaultViews() { ComponentView componentView = createComponentView(container, componentViewKey, ""); componentView.addDefaultElements(); componentView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); - componentView.setExternalSoftwareSystemBoundariesVisible(true); } } } diff --git a/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java b/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java index 4a7460364..63fd8a6ee 100644 --- a/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java +++ b/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java @@ -87,11 +87,4 @@ void interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractio assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); } - @Test - void setLocation_SetsTheLocationToUnspecified_WhenNullIsPassed() { - Person person = model.addPerson("Person", "Description"); - person.setLocation(null); - assertEquals(Location.Unspecified, person.getLocation()); - } - } \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java index 9de221b2f..87f5a631a 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java @@ -272,14 +272,6 @@ void addPersonWithoutRelationships_DoesNotAddRelationships() { assertEquals(0, view.getRelationships().size()); } - @Test - void isEnterpriseBoundaryVisible() { - assertTrue(view.isEnterpriseBoundaryVisible()); // default is true - - view.setEnterpriseBoundaryVisible(false); - assertFalse(view.isEnterpriseBoundaryVisible()); - } - @Test void addDefaultElements() { CustomElement element = model.addCustomElement("Custom"); diff --git a/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java index dade9b288..274afed64 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java @@ -82,14 +82,6 @@ void addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSyst assertTrue(view.getElements().contains(new ElementView(person))); } - @Test - void isEnterpriseBoundaryVisible() { - assertTrue(view.isEnterpriseBoundaryVisible()); // default is true - - view.setEnterpriseBoundaryVisible(false); - assertFalse(view.isEnterpriseBoundaryVisible()); - } - @Test void addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { try { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java index 3acd88669..b0e1207df 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java @@ -54,7 +54,6 @@ ComponentView parse(DslContext context, Tokens tokens) { } ComponentView view = workspace.getViews().createComponentView(container, key, description); - view.setExternalSoftwareSystemBoundariesVisible(true); return view; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java index b1033eb43..60ea0e7d7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java @@ -54,7 +54,6 @@ ContainerView parse(DslContext context, Tokens tokens) { } ContainerView view = workspace.getViews().createContainerView(softwareSystem, key, description); - view.setExternalSoftwareSystemBoundariesVisible(true); return view; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java index 78b23103e..461dee1fe 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java @@ -81,8 +81,6 @@ DynamicView parse(DslContext context, Tokens tokens) { } } - view.setExternalBoundariesVisible(true); - return view; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java deleted file mode 100644 index 3fe12598e..000000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseDslContext.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.structurizr.dsl; - -final class EnterpriseDslContext extends GroupableDslContext { - - EnterpriseDslContext() { - super(); - } - - EnterpriseDslContext(ElementGroup group) { - super(group); - } - - @Override - protected String[] getPermittedTokens() { - return new String[] { - StructurizrDslTokens.GROUP_TOKEN, - StructurizrDslTokens.PERSON_TOKEN, - StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, - StructurizrDslTokens.RELATIONSHIP_TOKEN - }; - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java deleted file mode 100644 index 54cbd3bfb..000000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/EnterpriseParser.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.Workspace; -import com.structurizr.model.Enterprise; - -final class EnterpriseParser extends AbstractParser { - - private static final String GRAMMAR = "enterprise [name]"; - - private static final int NAME_INDEX = 1; - - void parse(DslContext context, Tokens tokens) { - Workspace workspace = context.getWorkspace(); - - if (tokens.hasMoreThan(NAME_INDEX)) { - throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); - } else if (tokens.includes(NAME_INDEX)) { - String name = tokens.get(1); - - if (workspace.getModel().getEnterprise() == null) { - workspace.getModel().setEnterprise(new Enterprise(name)); - } else if (!name.equals(workspace.getModel().getEnterprise().getName())) { - throw new RuntimeException("The name of the enterprise has already been set"); - } - } else { - // do nothing ... this will just create an EnterpriseDslContext, so that people and software systems can be marked as "internal" - } - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java index e803d4fea..f03fbb426 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java @@ -12,19 +12,9 @@ final class ModelDslContext extends GroupableDslContext { super(group); } - @Override - void end() { - // the location is only set to internal when created inside an "enterprise" block, let's assume the rest are external if an enterprise has been specified - if (getWorkspace().getModel().getEnterprise() != null) { - getWorkspace().getModel().getPeople().stream().filter(p -> p.getLocation() != Location.Internal).forEach(p -> p.setLocation(Location.External)); - getWorkspace().getModel().getSoftwareSystems().stream().filter(ss -> ss.getLocation() != Location.Internal).forEach(ss -> ss.setLocation(Location.External)); - } - } - @Override protected String[] getPermittedTokens() { return new String[] { - StructurizrDslTokens.ENTERPRISE_TOKEN, StructurizrDslTokens.GROUP_TOKEN, StructurizrDslTokens.PERSON_TOKEN, StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java index 9ec33bfa6..c2103af51 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Location; import com.structurizr.model.Person; final class PersonParser extends AbstractParser { @@ -44,10 +43,6 @@ Person parse(GroupableDslContext context, Tokens tokens) { person.addTags(tags.split(",")); } - if (context instanceof EnterpriseDslContext) { - person.setLocation(Location.Internal); - } - if (context.hasGroup()) { person.setGroup(context.getGroup().getName()); context.getGroup().addElement(person); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java index 9e3214bbd..46cc0c9c4 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Location; import com.structurizr.model.SoftwareSystem; final class SoftwareSystemParser extends AbstractParser { @@ -44,10 +43,6 @@ SoftwareSystem parse(GroupableDslContext context, Tokens tokens) { softwareSystem.addTags(tags.split(",")); } - if (context instanceof EnterpriseDslContext) { - softwareSystem.setLocation(Location.Internal); - } - if (context.hasGroup()) { softwareSystem.setGroup(context.getGroup().getName()); context.getGroup().addElement(softwareSystem); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 17fbedb4d..db957889f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -305,7 +305,7 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio } else if (inContext(ExternalScriptDslContext.class)) { new ScriptParser().parseParameter(getContext(ExternalScriptDslContext.class), tokens); - } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(EnterpriseDslContext.class) || inContext(CustomElementDslContext.class) || inContext(PersonDslContext.class) || inContext(SoftwareSystemDslContext.class) || inContext(ContainerDslContext.class) || inContext(ComponentDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(DeploymentNodeDslContext.class) || inContext(InfrastructureNodeDslContext.class) || inContext(SoftwareSystemInstanceDslContext.class) || inContext(ContainerInstanceDslContext.class))) { + } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(CustomElementDslContext.class) || inContext(PersonDslContext.class) || inContext(SoftwareSystemDslContext.class) || inContext(ContainerDslContext.class) || inContext(ComponentDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(DeploymentNodeDslContext.class) || inContext(InfrastructureNodeDslContext.class) || inContext(SoftwareSystemInstanceDslContext.class) || inContext(ContainerInstanceDslContext.class))) { Relationship relationship = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { @@ -367,7 +367,7 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio registerIdentifier(identifier, customElement); - } else if (PERSON_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(EnterpriseDslContext.class))) { + } else if (PERSON_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { Person person = new PersonParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { @@ -376,7 +376,7 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio registerIdentifier(identifier, person); - } else if (SOFTWARE_SYSTEM_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(EnterpriseDslContext.class))) { + } else if (SOFTWARE_SYSTEM_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { @@ -408,11 +408,6 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio startContext(new ModelDslContext(group)); registerIdentifier(identifier, group); - } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(EnterpriseDslContext.class)) { - ElementGroup group = new GroupParser().parse(getContext(EnterpriseDslContext.class), tokens); - - startContext(new EnterpriseDslContext(group)); - registerIdentifier(identifier, group); } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { ElementGroup group = new GroupParser().parse(getContext(SoftwareSystemDslContext.class), tokens); @@ -618,10 +613,6 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio } else if (RELATIONSHIP_STYLE_ROUTING_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { new RelationshipStyleParser().parseRouting(getContext(RelationshipStyleDslContext.class), tokens); - } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { - new EnterpriseParser().parse(getContext(), tokens.withoutContextStartToken()); - startContext(new EnterpriseDslContext()); - } else if (DEPLOYMENT_ENVIRONMENT_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { String environment = new DeploymentEnvironmentParser().parse(tokens.withoutContextStartToken()); @@ -815,9 +806,6 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio } else if (TERMINOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { startContext(new TerminologyDslContext()); - } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { - new TerminologyParser().parseEnterprise(getContext(), tokens); - } else if (PERSON_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { new TerminologyParser().parsePerson(getContext(), tokens); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 946185e21..acae127aa 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -27,7 +27,6 @@ class StructurizrDslTokens { static final String SCOPE_TOKEN = "scope"; static final String MODEL_TOKEN = "model"; static final String VIEWS_TOKEN = "views"; - static final String ENTERPRISE_TOKEN = "enterprise"; static final String DEPLOYMENT_ENVIRONMENT_TOKEN = "deploymentEnvironment"; static final String DEPLOYMENT_GROUP_TOKEN = "deploymentGroup"; static final String DEPLOYMENT_NODE_TOKEN = "deploymentNode"; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java index 4dc004a95..d4a1330f0 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java @@ -53,10 +53,7 @@ SystemContextView parse(DslContext context, Tokens tokens) { description = tokens.get(DESCRIPTION_INDEX); } - SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, key, description); - view.setEnterpriseBoundaryVisible(true); - - return view; + return workspace.getViews().createSystemContextView(softwareSystem, key, description); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java index e03919f72..344f2a3d3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java @@ -34,10 +34,7 @@ SystemLandscapeView parse(DslContext context, Tokens tokens) { description = tokens.get(DESCRIPTION_INDEX); } - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView(key, description); - view.setEnterpriseBoundaryVisible(true); - - return view; + return workspace.getViews().createSystemLandscapeView(key, description); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java index 27e69e8fb..be42d7490 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java @@ -5,7 +5,6 @@ final class TerminologyDslContext extends DslContext { @Override protected String[] getPermittedTokens() { return new String[] { - StructurizrDslTokens.ENTERPRISE_TOKEN, StructurizrDslTokens.PERSON_TOKEN, StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, StructurizrDslTokens.CONTAINER_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java index b189f5d86..d95e2409c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java @@ -4,15 +4,6 @@ final class TerminologyParser extends AbstractParser { private final static int TERM_INDEX = 1; - void parseEnterprise(DslContext context, Tokens tokens) { - // enterprise - if (!tokens.includes(TERM_INDEX)) { - throw new RuntimeException("Expected: enterprise "); - } - - context.getWorkspace().getViews().getConfiguration().getTerminology().setEnterprise(tokens.get(TERM_INDEX)); - } - void parsePerson(DslContext context, Tokens tokens) { // person if (!tokens.includes(TERM_INDEX)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java index a67d1d6d9..aac6950e9 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java @@ -73,7 +73,6 @@ void test_parse_CreatesAComponentView() { assertEquals(1, views.size()); assertEquals("Component-001", views.get(0).getKey()); assertEquals("", views.get(0).getDescription()); - assertTrue(views.get(0).getExternalContainerBoundariesVisible()); } @Test @@ -89,7 +88,6 @@ void test_parse_CreatesAComponentViewWithAKey() { assertEquals(1, views.size()); assertEquals("key", views.get(0).getKey()); assertEquals("", views.get(0).getDescription()); - assertTrue(views.get(0).getExternalContainerBoundariesVisible()); } @Test @@ -105,7 +103,6 @@ void test_parse_CreatesAComponentViewWithAKeyAndDescription() { assertEquals(1, views.size()); assertEquals("key", views.get(0).getKey()); assertEquals("Description", views.get(0).getDescription()); - assertTrue(views.get(0).getExternalContainerBoundariesVisible()); } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java index 3044d5900..5d24c2a9d 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java @@ -73,7 +73,6 @@ void test_parse_CreatesAContainerView() { assertEquals(1, views.size()); assertEquals("Container-001", views.get(0).getKey()); assertEquals("", views.get(0).getDescription()); - assertTrue(views.get(0).getExternalSoftwareSystemBoundariesVisible()); } @Test @@ -89,7 +88,6 @@ void test_parse_CreatesAContainerViewWithAKey() { assertEquals(1, views.size()); assertEquals("key", views.get(0).getKey()); assertEquals("", views.get(0).getDescription()); - assertTrue(views.get(0).getExternalSoftwareSystemBoundariesVisible()); } @Test @@ -105,7 +103,6 @@ void test_parse_CreatesAContainerViewWithAKeyAndDescription() { assertEquals(1, views.size()); assertEquals("key", views.get(0).getKey()); assertEquals("Description", views.get(0).getDescription()); - assertTrue(views.get(0).getExternalSoftwareSystemBoundariesVisible()); } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index b8f2002b8..ed76f3eff 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -24,7 +24,6 @@ void test_test() throws Exception { parser.parse(new File("src/test/resources/dsl/test.dsl")); assertFalse(parser.getWorkspace().isEmpty()); - assertEquals("Organisation - Group", parser.getWorkspace().getModel().getEnterprise().getName()); } @Test @@ -147,10 +146,6 @@ void test_bigbankplc() throws Exception { Workspace workspace = parser.getWorkspace(); - assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Customer Service Staff").getLocation()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); - assertEquals(51, workspace.getModel().getElements().size()); assertEquals(3, workspace.getModel().getPeople().size()); assertEquals(4, workspace.getModel().getSoftwareSystems().size()); @@ -208,10 +203,6 @@ void test_bigbankplc_systemlandscape() throws Exception { Workspace workspace = parser.getWorkspace(); - assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Customer Service Staff").getLocation()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); - assertEquals(7, workspace.getModel().getElements().size()); assertEquals(3, workspace.getModel().getPeople().size()); assertEquals(4, workspace.getModel().getSoftwareSystems().size()); @@ -241,10 +232,6 @@ void test_bigbankplc_internetbankingsystem() throws Exception { Workspace workspace = parser.getWorkspace(); - assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Customer Service Staff").getLocation()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); - assertEquals(51, workspace.getModel().getElements().size()); assertEquals(3, workspace.getModel().getPeople().size()); assertEquals(4, workspace.getModel().getSoftwareSystems().size()); @@ -603,9 +590,6 @@ void test_nested_groups() throws Exception { SoftwareSystem c = parser.getWorkspace().getModel().getSoftwareSystemWithName("C"); assertEquals("Organisation", c.getGroup()); - - SoftwareSystem d = parser.getWorkspace().getModel().getSoftwareSystemWithName("D"); - assertEquals("Department A/Team 1", d.getGroup()); } @Test diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java deleted file mode 100644 index fa28b7cec..000000000 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/EnterpriseParserTests.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.model.Enterprise; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class EnterpriseParserTests extends AbstractTests { - - private EnterpriseParser parser = new EnterpriseParser(); - - @Test - void test_parse_SetsTheEnterpriseName_WhenItHasNotBeenSet() { - assertNull(workspace.getModel().getEnterprise()); - parser.parse(context(), tokens("enterprise", "New Name")); - assertEquals("New Name", workspace.getModel().getEnterprise().getName()); - } - - @Test - void test_parse_ThrowsAnException_WhenTheEnterpriseNameHasAlreadyBeenSet() { - workspace.getModel().setEnterprise(new Enterprise("My Enterprise")); - try { - parser.parse(context(), tokens("enterprise", "name")); - fail(); - } catch (Exception e) { - assertEquals("The name of the enterprise has already been set", e.getMessage()); - } - } - - @Test - void test_parse_DoesNothing_WhenANameIsSpecifiedButIsTheSameAsTheExistingEnterpriseName() { - workspace.getModel().setEnterprise(new Enterprise("My Enterprise")); - parser.parse(context(), tokens("My Enterprise")); - - assertEquals("My Enterprise", workspace.getModel().getEnterprise().getName()); - } - - @Test - void test_parse_DoesNothing_WhenNoNameIsSpecified() { - parser.parse(context(), tokens("enterprise")); - - assertNull(workspace.getModel().getEnterprise()); - } - - @Test - void test_parse_DoesNothing_WhenNoNameIsSpecifiedAndTheEnterpriseNameHasAlreadyBeenSet() { - workspace.getModel().setEnterprise(new Enterprise("My Enterprise")); - parser.parse(context(), tokens("enterprise")); - - assertEquals("My Enterprise", workspace.getModel().getEnterprise().getName()); - } - - @Test - void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { - try { - parser.parse(context(), tokens("enterprise", "name", "extra")); - fail(); - } catch (Exception e) { - assertEquals("Too many tokens, expected: enterprise [name]", e.getMessage()); - } - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java deleted file mode 100644 index 08e976738..000000000 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelDslContextTests.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.model.Enterprise; -import com.structurizr.model.Location; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class ModelDslContextTests extends AbstractTests { - - @Test - void end_DoesNothing_WhenNoPeopleAreMarkedAsInternal() { - ModelDslContext context = new ModelDslContext(); - context.setWorkspace(workspace); - - Person user1 = workspace.getModel().addPerson("Name 1"); - Person user2 = workspace.getModel().addPerson("Name 2"); - assertEquals(Location.Unspecified, user1.getLocation()); - assertEquals(Location.Unspecified, user2.getLocation()); - - context.end(); - assertEquals(Location.Unspecified, user1.getLocation()); - assertEquals(Location.Unspecified, user2.getLocation()); - } - - @Test - void end_MarksAllOtherPeopleAsExternal_WhenSomePeopleAreMarkedAsInternal() { - ModelDslContext context = new ModelDslContext(); - context.setWorkspace(workspace); - workspace.getModel().setEnterprise(new Enterprise("Name")); - - Person user1 = workspace.getModel().addPerson("Name 1"); - Person user2 = workspace.getModel().addPerson("Name 2"); - user2.setLocation(Location.Internal); - assertEquals(Location.Unspecified, user1.getLocation()); - assertEquals(Location.Internal, user2.getLocation()); - - context.end(); - assertEquals(Location.External, user1.getLocation()); - assertEquals(Location.Internal, user2.getLocation()); - } - - @Test - void end_DoesNothing_WhenNoSoftwareSystemsAreMarkedAsInternal() { - ModelDslContext context = new ModelDslContext(); - context.setWorkspace(workspace); - - SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Name 1"); - SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Name 2"); - assertEquals(Location.Unspecified, softwareSystem1.getLocation()); - assertEquals(Location.Unspecified, softwareSystem2.getLocation()); - - context.end(); - assertEquals(Location.Unspecified, softwareSystem1.getLocation()); - assertEquals(Location.Unspecified, softwareSystem2.getLocation()); - } - - @Test - void end_MarksAllOtherSoftwareSystemsAsExternal_WhenSomeSoftwareSystemsAreMarkedAsInternal() { - ModelDslContext context = new ModelDslContext(); - context.setWorkspace(workspace); - workspace.getModel().setEnterprise(new Enterprise("Name")); - - SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Name 1"); - SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Name 2"); - softwareSystem1.setLocation(Location.Internal); - assertEquals(Location.Internal, softwareSystem1.getLocation()); - assertEquals(Location.Unspecified, softwareSystem2.getLocation()); - - context.end(); - assertEquals(Location.Internal, softwareSystem1.getLocation()); - assertEquals(Location.External, softwareSystem2.getLocation()); - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java index f24c1c20d..ed6454623 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Location; import com.structurizr.model.Person; import org.junit.jupiter.api.Test; @@ -8,7 +7,7 @@ class PersonParserTests extends AbstractTests { - private PersonParser parser = new PersonParser(); + private final PersonParser parser = new PersonParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @@ -38,7 +37,6 @@ void test_parse_CreatesAPerson() { Person user = model.getPersonWithName("User"); assertNotNull(user); assertEquals("", user.getDescription()); - assertEquals(Location.Unspecified, user.getLocation()); assertEquals("Element,Person", user.getTags()); } @@ -50,7 +48,6 @@ void test_parse_CreatesAPersonWithADescription() { Person user = model.getPersonWithName("User"); assertNotNull(user); assertEquals("Description", user.getDescription()); - assertEquals(Location.Unspecified, user.getLocation()); assertEquals("Element,Person", user.getTags()); } @@ -62,22 +59,7 @@ void test_parse_CreatesAPersonWithADescriptionAndTags() { Person user = model.getPersonWithName("User"); assertNotNull(user); assertEquals("Description", user.getDescription()); - assertEquals(Location.Unspecified, user.getLocation()); assertEquals("Element,Person,Tag 1,Tag 2", user.getTags()); } - @Test - void test_parse_CreatesAnInternalPerson() { - EnterpriseDslContext context = new EnterpriseDslContext(); - context.setWorkspace(workspace); - parser.parse(context, tokens("person", "User")); - - assertEquals(1, model.getElements().size()); - Person user = model.getPersonWithName("User"); - assertNotNull(user); - assertEquals("", user.getDescription()); - assertEquals(Location.Internal, user.getLocation()); - assertEquals("Element,Person", user.getTags()); - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java index adbc2dfa8..d11724335 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Location; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; @@ -8,7 +7,7 @@ class SoftwareSystemParserTests extends AbstractTests { - private SoftwareSystemParser parser = new SoftwareSystemParser(); + private final SoftwareSystemParser parser = new SoftwareSystemParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @@ -38,7 +37,6 @@ void test_parse_CreatesASoftwareSystem() { SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); assertNotNull(softwareSystem); assertEquals("", softwareSystem.getDescription()); - assertEquals(Location.Unspecified, softwareSystem.getLocation()); assertEquals("Element,Software System", softwareSystem.getTags()); } @@ -50,7 +48,6 @@ void test_parse_CreatesASoftwareSystemWithADescription() { SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); assertNotNull(softwareSystem); assertEquals("Description", softwareSystem.getDescription()); - assertEquals(Location.Unspecified, softwareSystem.getLocation()); assertEquals("Element,Software System", softwareSystem.getTags()); } @@ -62,22 +59,7 @@ void test_parse_CreatesASoftwareSystemWithADescriptionAndTags() { SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); assertNotNull(softwareSystem); assertEquals("Description", softwareSystem.getDescription()); - assertEquals(Location.Unspecified, softwareSystem.getLocation()); assertEquals("Element,Software System,Tag 1,Tag 2", softwareSystem.getTags()); } - @Test - void test_parse_CreatesAnInternalSoftwareSystem() { - EnterpriseDslContext context = new EnterpriseDslContext(); - context.setWorkspace(workspace); - parser.parse(context, tokens("softwareSystem", "Name")); - - assertEquals(1, model.getElements().size()); - SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); - assertNotNull(softwareSystem); - assertEquals("", softwareSystem.getDescription()); - assertEquals(Location.Internal, softwareSystem.getLocation()); - assertEquals("Element,Software System", softwareSystem.getTags()); - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java index dbff12115..5d4592627 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java @@ -9,23 +9,6 @@ class TerminologyParserTests extends AbstractTests { private TerminologyParser parser = new TerminologyParser(); - @Test - void test_parseEnterprise_ThrowsAnException_WhenNoTermIsSpecified() { - try { - parser.parseEnterprise(context(), tokens("enterprise")); - fail(); - } catch (Exception e) { - assertEquals("Expected: enterprise ", e.getMessage()); - } - } - - @Test - void test_parseEnterprise_SetsTheTerm_WhenOneIsSpecified() { - parser.parseEnterprise(context(), tokens("enterprise", "TERM")); - - assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getEnterprise()); - } - @Test void test_parsePerson_ThrowsAnException_WhenNoTermIsSpecified() { try { diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl index 2e6783160..6cca65aed 100644 --- a/structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc.dsl @@ -9,7 +9,7 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea model { customer = person "Personal Banking Customer" "A customer of the bank, with personal bank accounts." "Customer" - enterprise "Big Bank plc" { + group "Big Bank plc" { supportStaff = person "Customer Service Staff" "Customer service staff within the bank." "Bank Staff" backoffice = person "Back Office Staff" "Administration and support staff within the bank." "Bank Staff" diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl index b4ff925b0..d5d051be7 100644 --- a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/model/people-and-software-systems.dsl @@ -1,6 +1,6 @@ customer = person "Personal Banking Customer" "A customer of the bank, with personal bank accounts." "Customer" -enterprise "Big Bank plc" { +group "Big Bank plc" { supportStaff = person "Customer Service Staff" "Customer service staff within the bank." "Bank Staff" backoffice = person "Back Office Staff" "Administration and support staff within the bank." "Bank Staff" diff --git a/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl b/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl index 4e98e49a3..47ac91617 100644 --- a/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl +++ b/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl @@ -27,14 +27,6 @@ workspace { c = softwareSystem "C" } - - enterprise "Enterprise" { - group "Department A" { - group "Team 1" { - d = softwareSystem "D" - } - } - } } views { diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index cab02b522..4748da87f 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -39,7 +39,7 @@ workspace "Name" "Description" { } } - enterprise "${ORGANISATION_NAME} - ${GROUP_NAME}" { + group "${ORGANISATION_NAME} - ${GROUP_NAME}" { softwareSystem = softwareSystem "Software System" "Description" "Tag" { webApplication = container "Web Application" "Description" "Technology" "Tag" { homePageController = component "HomePageController" "Description" "Spring MVC Controller" "Tag" { @@ -317,7 +317,6 @@ workspace "Name" "Description" { } terminology { - enterprise "Enterprise" person "Person" softwareSystem "Software System" container "Container" diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java index 9c2202bd8..b06c62f5a 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -130,7 +130,21 @@ public Diagram export(SystemLandscapeView view) { private Diagram export(SystemLandscapeView view, Integer animationStep) { this.frame = animationStep; - return export(view, view.isEnterpriseBoundaryVisible()); + + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + List elements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + elements.add((GroupableElement)elementView.getElement()); + } + writeElements(view, elements, writer); + + writer.writeLine(); + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); } public Diagram export(SystemContextView view) { @@ -149,59 +163,15 @@ public Diagram export(SystemContextView view) { private Diagram export(SystemContextView view, Integer animationStep) { this.frame = animationStep; - return export(view, view.isEnterpriseBoundaryVisible()); - } - private Diagram export(ModelView view, boolean enterpriseBoundaryIsVisible) { IndentingWriter writer = new IndentingWriter(); writeHeader(view, writer); - boolean showEnterpriseBoundary = - enterpriseBoundaryIsVisible && - (view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof Person && ((Person)e).getLocation() == Location.Internal) || - view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof SoftwareSystem && ((SoftwareSystem)e).getLocation() == Location.Internal)); - - if (showEnterpriseBoundary) { - String enterpriseName = "Enterprise"; - if (view.getModel().getEnterprise() != null) { - enterpriseName = view.getModel().getEnterprise().getName(); - } - - startEnterpriseBoundary(view, enterpriseName, writer); - - List elementsInsideEnterpriseBoundary = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() == Location.Internal) { - elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); - } - if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() == Location.Internal) { - elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); - } - } - writeElements(view, elementsInsideEnterpriseBoundary, writer); - - endEnterpriseBoundary(view, writer); - - List elementsOutsideEnterpriseBoundary = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() != Location.Internal) { - elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); - } - if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() != Location.Internal) { - elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); - } - if (elementView.getElement() instanceof CustomElement) { - elementsOutsideEnterpriseBoundary.add((CustomElement)elementView.getElement()); - } - } - writeElements(view, elementsOutsideEnterpriseBoundary, writer); - } else { - List elements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - elements.add((GroupableElement)elementView.getElement()); - } - writeElements(view, elements, writer); + List elements = new ArrayList<>(); + for (ElementView elementView : view.getElements()) { + elements.add((GroupableElement)elementView.getElement()); } + writeElements(view, elements, writer); writer.writeLine(); writeRelationships(view, writer); diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index bf67727da..fa74211fe 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -493,27 +493,19 @@ protected void writeElement(ModelView view, Element element, IndentingWriter wri if (element instanceof Person) { Person person = (Person)element; - String location = ""; - if (person.getLocation() == Location.External) { - location = "_Ext"; - } // Person(alias, label, ?descr, ?sprite, ?tags, ?link, ?type) writer.writeLine( - String.format("Person%s(%s, \"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", - location, id, name, description, tagsOf(view, elementToWrite), url) + String.format("Person(%s, \"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", + id, name, description, tagsOf(view, elementToWrite), url) ); } else if (element instanceof SoftwareSystem) { SoftwareSystem softwareSystem = (SoftwareSystem)element; - String location = ""; - if (softwareSystem.getLocation() == Location.External) { - location = "_Ext"; - } // System(alias, label, ?descr, ?sprite, ?tags, ?link, ?type) writer.writeLine( - String.format("System%s(%s, \"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", - location, id, name, description, tagsOf(view, elementToWrite), url) + String.format("System(%s, \"%s\", $descr=\"%s\", $tags=\"%s\", $link=\"%s\")", + id, name, description, tagsOf(view, elementToWrite), url) ); } else if (element instanceof Container) { Container container = (Container)element; diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot index 76c5ff5e9..b2869d886 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot @@ -6,12 +6,12 @@ digraph { label=<
Internet Banking System - API Application - Components
The component diagram for the API Application.> 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 17 [id=17,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 6 [id=6,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 18 [id=18,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 21 [id=21,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - subgraph cluster_20 { + subgraph cluster_11 { margin=25 label=<
API Application

[Container: Java and Spring MVC]> labelloc=b @@ -19,25 +19,25 @@ digraph { fontcolor="#444444" fillcolor="#444444" - 29 [id=29,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 30 [id=30,shape=rect, label=<Accounts Summary
Controller

[Component: Spring MVC Rest Controller]

Provides customers with a
summary of their bank
accounts.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 31 [id=31,shape=rect, label=<Reset Password
Controller

[Component: Spring MVC Rest Controller]

Allows users to reset their
passwords with a single use
URL.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 32 [id=32,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 33 [id=33,shape=rect, label=<Mainframe Banking
System Facade

[Component: Spring Bean]

A facade onto the mainframe
banking system.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 34 [id=34,shape=rect, label=<E-mail Component
[Component: Spring Bean]

Sends e-mails to users.>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 13 [id=13,shape=rect, label=<Accounts Summary
Controller

[Component: Spring MVC Rest Controller]

Provides customers with a
summary of their bank
accounts.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 14 [id=14,shape=rect, label=<Reset Password
Controller

[Component: Spring MVC Rest Controller]

Allows users to reset their
passwords with a single use
URL.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 16 [id=16,shape=rect, label=<Mainframe Banking
System Facade

[Component: Spring Bean]

A facade onto the mainframe
banking system.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 17 [id=17,shape=rect, label=<E-mail Component
[Component: Spring Bean]

Sends e-mails to users.>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] } - 17 -> 29 [id=35, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 17 -> 31 [id=37, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 17 -> 30 [id=38, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 18 -> 29 [id=39, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 18 -> 31 [id=41, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 18 -> 30 [id=42, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 29 -> 32 [id=43, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 30 -> 33 [id=44, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 31 -> 32 [id=45, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 31 -> 34 [id=46, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 32 -> 21 [id=47, label=<Reads from and writes to
[JDBC]>, style="dashed", color="#707070", fontcolor="#707070"] - 33 -> 4 [id=48, label=<Uses
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 34 -> 6 [id=49, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] + 8 -> 12 [id=32, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 8 -> 13 [id=34, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 8 -> 14 [id=35, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 9 -> 12 [id=36, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 9 -> 13 [id=38, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 9 -> 14 [id=39, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 12 -> 15 [id=40, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] + 13 -> 16 [id=41, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] + 14 -> 15 [id=42, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] + 14 -> 17 [id=43, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] + 15 -> 18 [id=44, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] + 16 -> 4 [id=46, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 17 -> 5 [id=48, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot index c4548fdc9..a0deff955 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot @@ -7,9 +7,9 @@ digraph { 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 6 [id=6,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - subgraph cluster_2 { + subgraph cluster_7 { margin=25 label=<
Internet Banking System

[Software System]> labelloc=b @@ -17,21 +17,21 @@ digraph { fontcolor="#444444" fillcolor="#444444" - 17 [id=17,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 18 [id=18,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 19 [id=19,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 20 [id=20,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 21 [id=21,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 10 [id=10,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 11 [id=11,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } - 1 -> 19 [id=22, label=<Visits bigbank.com/ib
using

[HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 17 [id=23, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 18 [id=24, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 19 -> 17 [id=25, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] - 20 -> 21 [id=26, label=<Reads from and writes to
[JDBC]>, style="dashed", color="#707070", fontcolor="#707070"] - 20 -> 4 [id=27, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 20 -> 6 [id=28, label=<Sends e-mail using
[SMTP]>, style="dashed", color="#707070", fontcolor="#707070"] - 17 -> 20 [id=36, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 18 -> 20 [id=40, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 6 -> 1 [id=8, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] + 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 10 [id=28, label=<Visits bigbank.com/ib
using

[HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 8 [id=29, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 9 [id=30, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] + 10 -> 8 [id=31, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] + 8 -> 11 [id=33, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 9 -> 11 [id=37, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 11 -> 18 [id=45, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] + 11 -> 4 [id=47, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 11 -> 5 [id=49, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot index 0b86e2c80..784eaeae4 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot @@ -14,6 +14,17 @@ digraph { fillcolor="#ffffff" subgraph cluster_51 { + margin=25 + label=<Web Browser
[Deployment Node: Chrome, Firefox, Safari, or Edge]> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 52 [id=52,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + subgraph cluster_53 { margin=25 label=<Docker Container - Web Server
[Deployment Node: Docker]> labelloc=b @@ -21,7 +32,7 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - subgraph cluster_52 { + subgraph cluster_54 { margin=25 label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> labelloc=b @@ -29,8 +40,8 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - 53 [id=53,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 54 [id=54,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 55 [id=55,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 57 [id=57,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } } @@ -56,20 +67,9 @@ digraph { } - subgraph cluster_63 { - margin=25 - label=<Web Browser
[Deployment Node: Chrome, Firefox, Safari, or Edge]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 64 [id=64,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - } - subgraph cluster_55 { + subgraph cluster_63 { margin=25 label=<Big Bank plc
[Deployment Node: Big Bank plc data center]> labelloc=b @@ -77,7 +77,7 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - subgraph cluster_56 { + subgraph cluster_64 { margin=25 label=<bigbank-dev001
[Deployment Node]> labelloc=b @@ -85,13 +85,13 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - 57 [id=57,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 65 [id=65,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] } } - 54 -> 57 [id=58, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 54 -> 61 [id=62, label=<Reads from and writes to
[JDBC]>, style="dashed", color="#707070", fontcolor="#707070"] - 64 -> 54 [id=65, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 53 -> 64 [id=66, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] + 55 -> 52 [id=56, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] + 52 -> 57 [id=58, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 57 -> 61 [id=62, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] + 57 -> 65 [id=66, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot index 841b0dc42..5ac04100c 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot @@ -46,17 +46,6 @@ digraph { fillcolor="#ffffff" subgraph cluster_73 { - margin=25 - label=<bigbank-prod001
[Deployment Node]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 74 [id=74,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - } - - subgraph cluster_75 { margin=25 label=<bigbank-web***
[Deployment Node: Ubuntu 16.04 LTS]> labelloc=b @@ -64,7 +53,7 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - subgraph cluster_76 { + subgraph cluster_74 { margin=25 label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> labelloc=b @@ -72,12 +61,12 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - 77 [id=77,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 75 [id=75,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } } - subgraph cluster_79 { + subgraph cluster_77 { margin=25 label=<bigbank-api***
[Deployment Node: Ubuntu 16.04 LTS]> labelloc=b @@ -85,7 +74,7 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - subgraph cluster_80 { + subgraph cluster_78 { margin=25 label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> labelloc=b @@ -93,12 +82,12 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - 81 [id=81,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 79 [id=79,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } } - subgraph cluster_85 { + subgraph cluster_82 { margin=25 label=<bigbank-db01
[Deployment Node: Ubuntu 16.04 LTS]> labelloc=b @@ -106,7 +95,7 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - subgraph cluster_86 { + subgraph cluster_83 { margin=25 label=<Oracle - Primary
[Deployment Node: Oracle 12c]> labelloc=b @@ -114,12 +103,12 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - 87 [id=87,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 84 [id=84,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } } - subgraph cluster_89 { + subgraph cluster_86 { margin=25 label=<bigbank-db02
[Deployment Node: Ubuntu 16.04 LTS]> labelloc=b @@ -127,7 +116,7 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - subgraph cluster_90 { + subgraph cluster_87 { margin=25 label=<Oracle - Secondary
[Deployment Node: Oracle 12c]> labelloc=b @@ -135,18 +124,29 @@ digraph { fontcolor="#000000" fillcolor="#ffffff" - 91 [id=91,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 88 [id=88,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } } + subgraph cluster_90 { + margin=25 + label=<bigbank-prod001
[Deployment Node]> + labelloc=b + color="#888888" + fontcolor="#000000" + fillcolor="#ffffff" + + 91 [id=91,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + } + } - 77 -> 71 [id=78, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] - 68 -> 81 [id=82, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 71 -> 81 [id=83, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 81 -> 74 [id=84, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 81 -> 87 [id=88, label=<Reads from and writes to
[JDBC]>, style="dashed", color="#707070", fontcolor="#707070"] - 81 -> 91 [id=92, label=<Reads from and writes to
[JDBC]>, style="dashed", color="#707070", fontcolor="#707070"] - 87 -> 91 [id=93, label=<Replicates data to>, style="dashed", color="#707070", fontcolor="#707070",ltail=cluster_86,lhead=cluster_90] + 75 -> 71 [id=76, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] + 68 -> 79 [id=80, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 71 -> 79 [id=81, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 79 -> 84 [id=85, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] + 79 -> 88 [id=89, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] + 79 -> 91 [id=92, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 84 -> 88 [id=93, label=<Replicates data to>, style="dashed", color="#707070", fontcolor="#707070",ltail=cluster_83,lhead=cluster_87] } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot index bcf42801d..25109d81f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot @@ -5,7 +5,7 @@ digraph { edge [fontname="Arial"] label=<
API Application - Dynamic
Summarises how the sign in feature works in the single-page application.> - subgraph cluster_20 { + subgraph cluster_11 { margin=25 label=<
API Application

[Container: Java and Spring MVC]> labelloc=b @@ -13,17 +13,17 @@ digraph { fontcolor="#444444" fillcolor="#444444" - 29 [id=29,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 32 [id=32,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] } - 17 [id=17,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 21 [id=21,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 17 -> 29 [id=35, label=<1. Submits credentials to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 29 -> 32 [id=43, label=<2. Validates credentials
using
>, style="dashed", color="#707070", fontcolor="#707070"] - 32 -> 21 [id=47, label=<3. select * from users
where username = ?

[JDBC]>, style="dashed", color="#707070", fontcolor="#707070"] - 21 -> 32 [id=47, label=<4. Returns user data to
[JDBC]>, style="dashed", color="#707070", fontcolor="#707070"] - 32 -> 29 [id=43, label=<5. Returns true if the
hashed password matches
>, style="dashed", color="#707070", fontcolor="#707070"] - 29 -> 17 [id=35, label=<6. Sends back an
authentication token to

[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 8 -> 12 [id=32, label=<1. Submits credentials to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] + 12 -> 15 [id=40, label=<2. Validates credentials
using
>, style="dashed", color="#707070", fontcolor="#707070"] + 15 -> 18 [id=44, label=<3. select * from users
where username = ?

[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] + 18 -> 15 [id=44, label=<4. Returns user data to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] + 15 -> 12 [id=40, label=<5. Returns true if the
hashed password matches
>, style="dashed", color="#707070", fontcolor="#707070"] + 12 -> 8 [id=32, label=<6. Sends back an
authentication token to

[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot index 5f1280e06..73780f17e 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot @@ -5,13 +5,24 @@ digraph { edge [fontname="Arial"] label=<
Internet Banking System - System Context
The system context diagram for the Internet Banking System.> + subgraph "cluster_group_Big Bank plc" { + margin=25 + label=<
Big Bank plc
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 7 [id=7,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] + } + 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] - 2 [id=2,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] - 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 6 [id=6,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 1 -> 2 [id=3, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 2 -> 4 [id=5, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#707070", fontcolor="#707070"] - 2 -> 6 [id=7, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] - 6 -> 1 [id=8, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 7 [id=19, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] + 7 -> 4 [id=20, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#707070", fontcolor="#707070"] + 7 -> 5 [id=21, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] + 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot index b82be15da..2b691cc0d 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot @@ -3,33 +3,34 @@ digraph { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
System Landscape
The system landscape diagram for Big Bank plc.> + label=<
System Landscape> - subgraph cluster_enterprise { + subgraph "cluster_group_Big Bank plc" { margin=25 - label=<
Big Bank plc

[Enterprise]> + label=<
Big Bank plc
> labelloc=b - color="#444444" - fontcolor="#444444" + color="#cccccc" + fontcolor="#cccccc" fillcolor="#ffffff" + style="dashed" - 12 [id=12,shape=rect, label=<Customer Service
Staff

[Person]

Customer service staff within
the bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 15 [id=15,shape=rect, label=<Back Office Staff
[Person]

Administration and support
staff within the bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 2 [id=2,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] + 2 [id=2,shape=rect, label=<Customer Service
Staff

[Person]

Customer service staff within
the bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 3 [id=3,shape=rect, label=<Back Office Staff
[Person]

Administration and support
staff within the bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 6 [id=6,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 9 [id=9,shape=rect, label=<ATM
[Software System]

Allows customers to withdraw
cash.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 6 [id=6,shape=rect, label=<ATM
[Software System]

Allows customers to withdraw
cash.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 7 [id=7,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] } 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] - 9 -> 4 [id=10, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 9 [id=11, label=<Withdraws cash using>, style="dashed", color="#707070", fontcolor="#707070"] - 12 -> 4 [id=13, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 12 [id=14, label=<Asks questions to
[Telephone]>, style="dashed", color="#707070", fontcolor="#707070"] - 15 -> 4 [id=16, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 2 [id=3, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 2 -> 4 [id=5, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#707070", fontcolor="#707070"] - 2 -> 6 [id=7, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] - 6 -> 1 [id=8, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 7 [id=19, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] + 7 -> 4 [id=20, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#707070", fontcolor="#707070"] + 7 -> 5 [id=21, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] + 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 2 [id=23, label=<Asks questions to
[Telephone]>, style="dashed", color="#707070", fontcolor="#707070"] + 2 -> 4 [id=24, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] + 1 -> 6 [id=25, label=<Withdraws cash using>, style="dashed", color="#707070", fontcolor="#707070"] + 6 -> 4 [id=26, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] + 3 -> 4 [id=27, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot index efa20b99e..cc868ffe8 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot @@ -15,9 +15,9 @@ digraph { fontcolor="#444444" fillcolor="#444444" - subgraph "cluster_group_Group 4" { + subgraph "cluster_group_Group 5" { margin=25 - label=<
Group 4
> + label=<
Group 5
> labelloc=b color="#cccccc" fontcolor="#cccccc" diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot index fcddeeed4..013ecfbf2 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot @@ -15,9 +15,9 @@ digraph { fontcolor="#444444" fillcolor="#444444" - subgraph "cluster_group_Group 3" { + subgraph "cluster_group_Group 4" { margin=25 - label=<
Group 3
> + label=<
Group 4
> labelloc=b color="#cccccc" fontcolor="#cccccc" diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot index 5daee1e84..5147526da 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot @@ -5,39 +5,40 @@ digraph { edge [fontname="Arial"] label=<
System Landscape> - subgraph cluster_enterprise { + subgraph "cluster_group_Group 1" { margin=25 - label=<
Enterprise

[Enterprise]> + label=<
Group 1
> labelloc=b - color="#444444" - fontcolor="#444444" + color="#cccccc" + fontcolor="#cccccc" fillcolor="#ffffff" + style="dashed" - subgraph "cluster_group_Group 2" { - margin=25 - label=<
Group 2
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 4 [id=4,shape=rect, label=<D
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + 2 [id=2,shape=rect, label=<B
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] } - subgraph "cluster_group_Group 1" { + subgraph "cluster_group_Group 2" { margin=25 - label=<
Group 1
> + label=<
Group 2
> labelloc=b color="#cccccc" fontcolor="#cccccc" fillcolor="#ffffff" style="dashed" - 2 [id=2,shape=rect, label=<B
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + subgraph "cluster_group_Group 3" { + margin=25 + label=<
Group 3
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 4 [id=4,shape=rect, label=<D
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] + } + } 1 [id=1,shape=rect, label=<A
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph index 2452b8dbc..31f0a6f7e 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph @@ -7,21 +7,42 @@ resources: backgroundColor: "#08427b" color: "#ffffff" - - id: "12" + - id: "2" name: "Customer Service Staff" subtitle: "[Person]" description: "Customer service staff within the bank." backgroundColor: "#999999" color: "#ffffff" - - id: "15" + - id: "3" name: "Back Office Staff" subtitle: "[Person]" description: "Administration and support staff within the bank." backgroundColor: "#999999" color: "#ffffff" - - id: "2" + - id: "4" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "5" + name: "E-mail System" + subtitle: "[Software System]" + description: "The internal Microsoft Exchange e-mail system." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "6" + name: "ATM" + subtitle: "[Software System]" + description: "Allows customers to withdraw cash." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "7" name: "Internet Banking System" subtitle: "[Software System]" description: "Allows customers to view information about their bank accounts, and make payments." @@ -29,28 +50,14 @@ resources: color: "#ffffff" children: - - id: "17" - name: "Single-Page Application" - subtitle: "[Container: JavaScript and Angular]" - description: "Provides all of the Internet banking functionality to customers via their web browser." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "18" - name: "Mobile App" - subtitle: "[Container: Xamarin]" - description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "19" + - id: "10" name: "Web Application" subtitle: "[Container: Java and Spring MVC]" description: "Delivers the static content and the Internet banking single page application." backgroundColor: "#438dd5" color: "#ffffff" - - id: "20" + - id: "11" name: "API Application" subtitle: "[Container: Java and Spring MVC]" description: "Provides Internet banking functionality via a JSON/HTTPS API." @@ -58,108 +65,112 @@ resources: color: "#ffffff" children: - - id: "29" + - id: "12" name: "Sign In Controller" subtitle: "[Component: Spring MVC Rest Controller]" description: "Allows users to sign in to the Internet Banking System." backgroundColor: "#85bbf0" color: "#000000" - - id: "30" + - id: "13" name: "Accounts Summary Controller" subtitle: "[Component: Spring MVC Rest Controller]" description: "Provides customers with a summary of their bank accounts." backgroundColor: "#85bbf0" color: "#000000" - - id: "31" + - id: "14" name: "Reset Password Controller" subtitle: "[Component: Spring MVC Rest Controller]" description: "Allows users to reset their passwords with a single use URL." backgroundColor: "#85bbf0" color: "#000000" - - id: "32" + - id: "15" name: "Security Component" subtitle: "[Component: Spring Bean]" description: "Provides functionality related to signing in, changing passwords, etc." backgroundColor: "#85bbf0" color: "#000000" - - id: "33" + - id: "16" name: "Mainframe Banking System Facade" subtitle: "[Component: Spring Bean]" description: "A facade onto the mainframe banking system." backgroundColor: "#85bbf0" color: "#000000" - - id: "34" + - id: "17" name: "E-mail Component" subtitle: "[Component: Spring Bean]" description: "Sends e-mails to users." backgroundColor: "#85bbf0" color: "#000000" - - id: "21" + - id: "18" name: "Database" subtitle: "[Container: Oracle Database Schema]" description: "Stores user registration information, hashed authentication credentials, access logs, etc." backgroundColor: "#438dd5" color: "#ffffff" - - id: "4" - name: "Mainframe Banking System" - subtitle: "[Software System]" - description: "Stores all of the core banking information about customers, accounts, transactions, etc." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "6" - name: "E-mail System" - subtitle: "[Software System]" - description: "The internal Microsoft Exchange e-mail system." - backgroundColor: "#999999" - color: "#ffffff" + - id: "8" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" - - id: "9" - name: "ATM" - subtitle: "[Software System]" - description: "Allows customers to withdraw cash." - backgroundColor: "#999999" - color: "#ffffff" + - id: "9" + name: "Mobile App" + subtitle: "[Container: Xamarin]" + description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." + backgroundColor: "#438dd5" + color: "#ffffff" - id: "50" name: "Developer Laptop" subtitle: "[Deployment Node: Microsoft Windows 10 or Apple macOS]" - description: "A developer laptop." backgroundColor: "#ffffff" color: "#000000" children: - id: "51" + name: "Web Browser" + subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "52" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "53" name: "Docker Container - Web Server" subtitle: "[Deployment Node: Docker]" - description: "A Docker container." backgroundColor: "#ffffff" color: "#000000" children: - - id: "52" + - id: "54" name: "Apache Tomcat" subtitle: "[Deployment Node: Apache Tomcat 8.x]" - description: "An open source Java EE web server." backgroundColor: "#ffffff" color: "#000000" children: - - id: "53" + - id: "55" name: "Web Application" subtitle: "[Container: Java and Spring MVC]" description: "Delivers the static content and the Internet banking single page application." backgroundColor: "#438dd5" color: "#ffffff" - - id: "54" + - id: "57" name: "API Application" subtitle: "[Container: Java and Spring MVC]" description: "Provides Internet banking functionality via a JSON/HTTPS API." @@ -169,7 +180,6 @@ resources: - id: "59" name: "Docker Container - Database Server" subtitle: "[Deployment Node: Docker]" - description: "A Docker container." backgroundColor: "#ffffff" color: "#000000" @@ -177,7 +187,6 @@ resources: - id: "60" name: "Database Server" subtitle: "[Deployment Node: Oracle 12c]" - description: "A development database." backgroundColor: "#ffffff" color: "#000000" @@ -189,35 +198,21 @@ resources: backgroundColor: "#438dd5" color: "#ffffff" - - id: "63" - name: "Web Browser" - subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "64" - name: "Single-Page Application" - subtitle: "[Container: JavaScript and Angular]" - description: "Provides all of the Internet banking functionality to customers via their web browser." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "55" + - id: "63" name: "Big Bank plc" subtitle: "[Deployment Node: Big Bank plc data center]" backgroundColor: "#ffffff" color: "#000000" children: - - id: "56" + - id: "64" name: "bigbank-dev001" subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#000000" children: - - id: "57" + - id: "65" name: "Mainframe Banking System" subtitle: "[Software System]" description: "Stores all of the core banking information about customers, accounts, transactions, etc." @@ -267,365 +262,356 @@ resources: children: - id: "73" - name: "bigbank-prod001" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "74" - name: "Mainframe Banking System" - subtitle: "[Software System]" - description: "Stores all of the core banking information about customers, accounts, transactions, etc." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "75" name: "bigbank-web***" subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - description: "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs." backgroundColor: "#ffffff" color: "#000000" children: - - id: "76" + - id: "74" name: "Apache Tomcat" subtitle: "[Deployment Node: Apache Tomcat 8.x]" - description: "An open source Java EE web server." backgroundColor: "#ffffff" color: "#000000" children: - - id: "77" + - id: "75" name: "Web Application" subtitle: "[Container: Java and Spring MVC]" description: "Delivers the static content and the Internet banking single page application." backgroundColor: "#438dd5" color: "#ffffff" - - id: "79" + - id: "77" name: "bigbank-api***" subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - description: "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs." backgroundColor: "#ffffff" color: "#000000" children: - - id: "80" + - id: "78" name: "Apache Tomcat" subtitle: "[Deployment Node: Apache Tomcat 8.x]" - description: "An open source Java EE web server." backgroundColor: "#ffffff" color: "#000000" children: - - id: "81" + - id: "79" name: "API Application" subtitle: "[Container: Java and Spring MVC]" description: "Provides Internet banking functionality via a JSON/HTTPS API." backgroundColor: "#438dd5" color: "#ffffff" - - id: "85" + - id: "82" name: "bigbank-db01" subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - description: "The primary database server." backgroundColor: "#ffffff" color: "#000000" children: - - id: "86" + - id: "83" name: "Oracle - Primary" subtitle: "[Deployment Node: Oracle 12c]" - description: "The primary, live database server." backgroundColor: "#ffffff" color: "#000000" children: - - id: "87" + - id: "84" name: "Database" subtitle: "[Container: Oracle Database Schema]" description: "Stores user registration information, hashed authentication credentials, access logs, etc." backgroundColor: "#438dd5" color: "#ffffff" - - id: "89" + - id: "86" name: "bigbank-db02" subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - description: "The secondary database server." backgroundColor: "#ffffff" color: "#000000" children: - - id: "90" + - id: "87" name: "Oracle - Secondary" subtitle: "[Deployment Node: Oracle 12c]" - description: "A secondary, standby database server, used for failover purposes only." backgroundColor: "#ffffff" color: "#000000" children: - - id: "91" + - id: "88" name: "Database" subtitle: "[Container: Oracle Database Schema]" description: "Stores user registration information, hashed authentication credentials, access logs, etc." backgroundColor: "#438dd5" color: "#ffffff" + - id: "90" + name: "bigbank-prod001" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#000000" + + children: + - id: "91" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + perspectives: - name: Static Structure relations: - from: "1" - to: "9" - label: "Withdraws cash using" + to: "7" + label: "Views account balances, and makes payments using" color: "#707070" - from: "1" - to: "12" + to: "2" label: "Asks questions to" description: "Telephone" color: "#707070" - from: "1" - to: "2" - label: "Views account balances, and makes payments using" + to: "6" + label: "Withdraws cash using" color: "#707070" - - from: "12" + - from: "2" to: "4" label: "Uses" color: "#707070" - - from: "15" + - from: "3" to: "4" label: "Uses" color: "#707070" - - from: "2" - to: "4" - label: "Gets account information from, and makes payments using" - color: "#707070" - - - from: "2" - to: "6" - label: "Sends e-mail using" - color: "#707070" - - - from: "6" + - from: "5" to: "1" label: "Sends e-mails to" color: "#707070" - - from: "9" + - from: "6" to: "4" label: "Uses" color: "#707070" + - from: "7" + to: "4" + label: "Gets account information from, and makes payments using" + color: "#707070" + + - from: "7" + to: "5" + label: "Sends e-mail using" + color: "#707070" + - from: "1" - to: "19" + to: "10" label: "Visits bigbank.com/ib using" description: "HTTPS" color: "#707070" - from: "1" - to: "17" + to: "8" label: "Views account balances, and makes payments using" color: "#707070" - from: "1" - to: "18" + to: "9" label: "Views account balances, and makes payments using" color: "#707070" - - from: "17" - to: "20" + - from: "10" + to: "8" + label: "Delivers to the customer's web browser" + color: "#707070" + + - from: "11" + to: "18" + label: "Reads from and writes to" + description: "SQL/TCP" + color: "#707070" + + - from: "11" + to: "4" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#707070" + + - from: "11" + to: "5" + label: "Sends e-mail using" + color: "#707070" + + - from: "8" + to: "11" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "18" - to: "20" + - from: "9" + to: "11" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "19" + - from: "12" + to: "15" + label: "Uses" + color: "#707070" + + - from: "13" + to: "16" + label: "Uses" + color: "#707070" + + - from: "14" + to: "15" + label: "Uses" + color: "#707070" + + - from: "14" to: "17" - label: "Delivers to the customer's web browser" + label: "Uses" color: "#707070" - - from: "20" - to: "21" + - from: "15" + to: "18" label: "Reads from and writes to" - description: "JDBC" + description: "SQL/TCP" color: "#707070" - - from: "20" + - from: "16" to: "4" label: "Makes API calls to" description: "XML/HTTPS" color: "#707070" - - from: "20" - to: "6" + - from: "17" + to: "5" label: "Sends e-mail using" - description: "SMTP" color: "#707070" - - from: "17" - to: "29" + - from: "8" + to: "12" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "17" - to: "31" + - from: "8" + to: "13" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "17" - to: "30" + - from: "8" + to: "14" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "18" - to: "29" + - from: "9" + to: "12" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "18" - to: "31" + - from: "9" + to: "13" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "18" - to: "30" + - from: "9" + to: "14" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "29" - to: "32" - label: "Uses" - color: "#707070" - - - from: "30" - to: "33" - label: "Uses" - color: "#707070" - - - from: "31" - to: "32" - label: "Uses" - color: "#707070" - - - from: "31" - to: "34" - label: "Uses" - color: "#707070" - - - from: "32" - to: "21" - label: "Reads from and writes to" - description: "JDBC" - color: "#707070" - - - from: "33" - to: "4" - label: "Uses" - description: "XML/HTTPS" - color: "#707070" - - - from: "34" - to: "6" - label: "Sends e-mail using" - color: "#707070" - - name: Dynamic - API Application - Dynamic sequence: - start: "17" + start: "8" steps: - - to: "29" + - to: "12" label: "1. Submits credentials to" description: "JSON/HTTPS" color: "#707070" - - to: "32" + - to: "15" label: "2. Validates credentials using" color: "#707070" - - to: "21" + - to: "18" label: "3. select * from users where username = ?" - description: "JDBC" + description: "SQL/TCP" color: "#707070" - - to: "32" + - to: "15" label: "4. Returns user data to" - description: "JDBC" + description: "SQL/TCP" color: "#707070" - - to: "29" + - to: "12" label: "5. Returns true if the hashed password matches" color: "#707070" - - to: "17" + - to: "8" label: "6. Sends back an authentication token to" description: "JSON/HTTPS" color: "#707070" - name: Deployment - Development relations: - - from: "53" - to: "64" - label: "Delivers to the customer's web browser" - color: "#707070" - - from: "54" + - from: "52" to: "57" label: "Makes API calls to" - description: "XML/HTTPS" + description: "JSON/HTTPS" color: "#707070" - - from: "54" + - from: "55" + to: "52" + label: "Delivers to the customer's web browser" + color: "#707070" + - from: "57" to: "61" label: "Reads from and writes to" - description: "JDBC" + description: "SQL/TCP" color: "#707070" - - from: "64" - to: "54" + - from: "57" + to: "65" label: "Makes API calls to" - description: "JSON/HTTPS" + description: "XML/HTTPS" color: "#707070" - name: Deployment - Live relations: - from: "68" - to: "81" + to: "79" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - from: "71" - to: "81" + to: "79" label: "Makes API calls to" description: "JSON/HTTPS" color: "#707070" - - from: "77" + - from: "75" to: "71" label: "Delivers to the customer's web browser" color: "#707070" - - from: "81" - to: "74" - label: "Makes API calls to" - description: "XML/HTTPS" - color: "#707070" - - from: "81" - to: "87" + - from: "79" + to: "84" label: "Reads from and writes to" - description: "JDBC" + description: "SQL/TCP" color: "#707070" - - from: "81" - to: "91" + - from: "79" + to: "88" label: "Reads from and writes to" - description: "JDBC" + description: "SQL/TCP" color: "#707070" + - from: "79" + to: "91" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#707070" \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd index dcae0ac7f..8a96174a1 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd @@ -6,43 +6,43 @@ graph TB 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 17["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 17 fill:#438dd5,stroke:#2e6295,color:#ffffff - 6["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] - style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff - 18["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] + 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] + style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff + 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff - 21[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 21 fill:#438dd5,stroke:#2e6295,color:#ffffff + 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] + style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff + 9["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] + style 9 fill:#438dd5,stroke:#2e6295,color:#ffffff - subgraph 20 [API Application] - style 20 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 11 [API Application] + style 11 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 29["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] - style 29 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 30["
Accounts Summary Controller
[Component: Spring MVC Rest Controller]
Provides customers with a
summary of their bank
accounts.
"] - style 30 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 31["
Reset Password Controller
[Component: Spring MVC Rest Controller]
Allows users to reset their
passwords with a single use
URL.
"] - style 31 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 32["
Security Component
[Component: Spring Bean]
Provides functionality
related to signing in,
changing passwords, etc.
"] - style 32 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 33["
Mainframe Banking System Facade
[Component: Spring Bean]
A facade onto the mainframe
banking system.
"] - style 33 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 34["
E-mail Component
[Component: Spring Bean]
Sends e-mails to users.
"] - style 34 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 12["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] + style 12 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 13["
Accounts Summary Controller
[Component: Spring MVC Rest Controller]
Provides customers with a
summary of their bank
accounts.
"] + style 13 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 14["
Reset Password Controller
[Component: Spring MVC Rest Controller]
Allows users to reset their
passwords with a single use
URL.
"] + style 14 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 15["
Security Component
[Component: Spring Bean]
Provides functionality
related to signing in,
changing passwords, etc.
"] + style 15 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 16["
Mainframe Banking System Facade
[Component: Spring Bean]
A facade onto the mainframe
banking system.
"] + style 16 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 17["
E-mail Component
[Component: Spring Bean]
Sends e-mails to users.
"] + style 17 fill:#85bbf0,stroke:#5d82a8,color:#000000 end - 17-. "
Makes API calls to
[JSON/HTTPS]
" .->29 - 17-. "
Makes API calls to
[JSON/HTTPS]
" .->31 - 17-. "
Makes API calls to
[JSON/HTTPS]
" .->30 - 18-. "
Makes API calls to
[JSON/HTTPS]
" .->29 - 18-. "
Makes API calls to
[JSON/HTTPS]
" .->31 - 18-. "
Makes API calls to
[JSON/HTTPS]
" .->30 - 29-. "
Uses
" .->32 - 30-. "
Uses
" .->33 - 31-. "
Uses
" .->32 - 31-. "
Uses
" .->34 - 32-. "
Reads from and writes to
[JDBC]
" .->21 - 33-. "
Uses
[XML/HTTPS]
" .->4 - 34-. "
Sends e-mail using
" .->6 + 8-. "
Makes API calls to
[JSON/HTTPS]
" .->12 + 8-. "
Makes API calls to
[JSON/HTTPS]
" .->13 + 8-. "
Makes API calls to
[JSON/HTTPS]
" .->14 + 9-. "
Makes API calls to
[JSON/HTTPS]
" .->12 + 9-. "
Makes API calls to
[JSON/HTTPS]
" .->13 + 9-. "
Makes API calls to
[JSON/HTTPS]
" .->14 + 12-. "
Uses
" .->15 + 13-. "
Uses
" .->16 + 14-. "
Uses
" .->15 + 14-. "
Uses
" .->17 + 15-. "
Reads from and writes to
[SQL/TCP]
" .->18 + 16-. "
Makes API calls to
[XML/HTTPS]
" .->4 + 17-. "
Sends e-mail using
" .->5 end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd index 4cda955bd..86d53a05a 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd @@ -8,32 +8,32 @@ graph TB style 1 fill:#08427b,stroke:#052e56,color:#ffffff 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 6["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] - style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff + 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] + style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff - subgraph 2 [Internet Banking System] - style 2 fill:#ffffff,stroke:#0b4884,color:#0b4884 + subgraph 7 [Internet Banking System] + style 7 fill:#ffffff,stroke:#0b4884,color:#0b4884 - 17["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 17 fill:#438dd5,stroke:#2e6295,color:#ffffff - 18["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] + 10["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] + style 10 fill:#438dd5,stroke:#2e6295,color:#ffffff + 11["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] + style 11 fill:#438dd5,stroke:#2e6295,color:#ffffff + 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff - 19["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] - style 19 fill:#438dd5,stroke:#2e6295,color:#ffffff - 20["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] - style 20 fill:#438dd5,stroke:#2e6295,color:#ffffff - 21[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 21 fill:#438dd5,stroke:#2e6295,color:#ffffff + 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] + style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff + 9["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] + style 9 fill:#438dd5,stroke:#2e6295,color:#ffffff end - 1-. "
Visits bigbank.com/ib using
[HTTPS]
" .->19 - 1-. "
Views account balances, and
makes payments using
" .->17 - 1-. "
Views account balances, and
makes payments using
" .->18 - 19-. "
Delivers to the customer's
web browser
" .->17 - 20-. "
Reads from and writes to
[JDBC]
" .->21 - 20-. "
Makes API calls to
[XML/HTTPS]
" .->4 - 20-. "
Sends e-mail using
[SMTP]
" .->6 - 17-. "
Makes API calls to
[JSON/HTTPS]
" .->20 - 18-. "
Makes API calls to
[JSON/HTTPS]
" .->20 - 6-. "
Sends e-mails to
" .->1 + 5-. "
Sends e-mails to
" .->1 + 1-. "
Visits bigbank.com/ib using
[HTTPS]
" .->10 + 1-. "
Views account balances, and
makes payments using
" .->8 + 1-. "
Views account balances, and
makes payments using
" .->9 + 10-. "
Delivers to the customer's
web browser
" .->8 + 8-. "
Makes API calls to
[JSON/HTTPS]
" .->11 + 9-. "
Makes API calls to
[JSON/HTTPS]
" .->11 + 11-. "
Reads from and writes to
[SQL/TCP]
" .->18 + 11-. "
Makes API calls to
[XML/HTTPS]
" .->4 + 11-. "
Sends e-mail using
" .->5 end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd index 14b3194e6..b933bd4fb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd @@ -7,16 +7,23 @@ graph TB subgraph 50 [Developer Laptop] style 50 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 51 [Docker Container - Web Server] + subgraph 51 [Web Browser] style 51 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 52 [Apache Tomcat] - style 52 fill:#ffffff,stroke:#888888,color:#000000 + 52["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] + style 52 fill:#438dd5,stroke:#2e6295,color:#ffffff + end + + subgraph 53 [Docker Container - Web Server] + style 53 fill:#ffffff,stroke:#888888,color:#000000 + + subgraph 54 [Apache Tomcat] + style 54 fill:#ffffff,stroke:#888888,color:#000000 - 53["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] - style 53 fill:#438dd5,stroke:#2e6295,color:#ffffff - 54["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] - style 54 fill:#438dd5,stroke:#2e6295,color:#ffffff + 55["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] + style 55 fill:#438dd5,stroke:#2e6295,color:#ffffff + 57["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] + style 57 fill:#438dd5,stroke:#2e6295,color:#ffffff end end @@ -33,29 +40,22 @@ graph TB end - subgraph 63 [Web Browser] - style 63 fill:#ffffff,stroke:#888888,color:#000000 - - 64["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 64 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - end - subgraph 55 [Big Bank plc] - style 55 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 63 [Big Bank plc] + style 63 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 56 [bigbank-dev001] - style 56 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 64 [bigbank-dev001] + style 64 fill:#ffffff,stroke:#888888,color:#000000 - 57["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 57 fill:#999999,stroke:#6b6b6b,color:#ffffff + 65["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] + style 65 fill:#999999,stroke:#6b6b6b,color:#ffffff end end - 54-. "
Makes API calls to
[XML/HTTPS]
" .->57 - 54-. "
Reads from and writes to
[JDBC]
" .->61 - 64-. "
Makes API calls to
[JSON/HTTPS]
" .->54 - 53-. "
Delivers to the customer's
web browser
" .->64 + 55-. "
Delivers to the customer's
web browser
" .->52 + 52-. "
Makes API calls to
[JSON/HTTPS]
" .->57 + 57-. "
Reads from and writes to
[SQL/TCP]
" .->61 + 57-. "
Makes API calls to
[XML/HTTPS]
" .->65 end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd index 73b4fb23c..e4a56d3be 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd @@ -26,67 +26,67 @@ graph TB subgraph 72 [Big Bank plc] style 72 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 73 [bigbank-prod001] + subgraph 73 [bigbank-web***] style 73 fill:#ffffff,stroke:#888888,color:#000000 - 74["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 74 fill:#999999,stroke:#6b6b6b,color:#ffffff - end - - subgraph 75 [bigbank-web***] - style 75 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 76 [Apache Tomcat] - style 76 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 74 [Apache Tomcat] + style 74 fill:#ffffff,stroke:#888888,color:#000000 - 77["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] - style 77 fill:#438dd5,stroke:#2e6295,color:#ffffff + 75["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] + style 75 fill:#438dd5,stroke:#2e6295,color:#ffffff end end - subgraph 79 [bigbank-api***] - style 79 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 77 [bigbank-api***] + style 77 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 80 [Apache Tomcat] - style 80 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 78 [Apache Tomcat] + style 78 fill:#ffffff,stroke:#888888,color:#000000 - 81["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] - style 81 fill:#438dd5,stroke:#2e6295,color:#ffffff + 79["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] + style 79 fill:#438dd5,stroke:#2e6295,color:#ffffff end end - subgraph 85 [bigbank-db01] - style 85 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 82 [bigbank-db01] + style 82 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 86 [Oracle - Primary] - style 86 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 83 [Oracle - Primary] + style 83 fill:#ffffff,stroke:#888888,color:#000000 - 87[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 87 fill:#438dd5,stroke:#2e6295,color:#ffffff + 84[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] + style 84 fill:#438dd5,stroke:#2e6295,color:#ffffff end end - subgraph 89 [bigbank-db02] - style 89 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 86 [bigbank-db02] + style 86 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 90 [Oracle - Secondary] - style 90 fill:#ffffff,stroke:#888888,color:#000000 + subgraph 87 [Oracle - Secondary] + style 87 fill:#ffffff,stroke:#888888,color:#000000 - 91[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 91 fill:#438dd5,stroke:#2e6295,color:#ffffff + 88[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] + style 88 fill:#438dd5,stroke:#2e6295,color:#ffffff end end + subgraph 90 [bigbank-prod001] + style 90 fill:#ffffff,stroke:#888888,color:#000000 + + 91["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] + style 91 fill:#999999,stroke:#6b6b6b,color:#ffffff + end + end - 77-. "
Delivers to the customer's
web browser
" .->71 - 68-. "
Makes API calls to
[JSON/HTTPS]
" .->81 - 71-. "
Makes API calls to
[JSON/HTTPS]
" .->81 - 81-. "
Makes API calls to
[XML/HTTPS]
" .->74 - 81-. "
Reads from and writes to
[JDBC]
" .->87 - 81-. "
Reads from and writes to
[JDBC]
" .->91 + 75-. "
Delivers to the customer's
web browser
" .->71 + 68-. "
Makes API calls to
[JSON/HTTPS]
" .->79 + 71-. "
Makes API calls to
[JSON/HTTPS]
" .->79 + 79-. "
Reads from and writes to
[SQL/TCP]
" .->84 + 79-. "
Reads from and writes to
[SQL/TCP]
" .->88 + 79-. "
Makes API calls to
[XML/HTTPS]
" .->91 end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd index 655ff882c..f25511e2f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd @@ -1,13 +1,13 @@ sequenceDiagram - participant 17 as Single-Page Application
[Container: JavaScript and Angular] - participant 29 as Sign In Controller
[Component: Spring MVC Rest Controller] - participant 32 as Security Component
[Component: Spring Bean] - participant 21 as Database
[Container: Oracle Database Schema] + participant 8 as Single-Page Application
[Container: JavaScript and Angular] + participant 12 as Sign In Controller
[Component: Spring MVC Rest Controller] + participant 15 as Security Component
[Component: Spring Bean] + participant 18 as Database
[Container: Oracle Database Schema] - 17->>29: Submits credentials to
[JSON/HTTPS] - 29->>32: Validates credentials using - 32->>21: select * from users where username = ?
[JDBC] - 21-->>32: Returns user data to
[JDBC] - 32-->>29: Returns true if the hashed password matches - 29-->>17: Sends back an authentication token to
[JSON/HTTPS] \ No newline at end of file + 8->>12: Submits credentials to
[JSON/HTTPS] + 12->>15: Validates credentials using + 15->>18: select * from users where username = ?
[SQL/TCP] + 18-->>15: Returns user data to
[SQL/TCP] + 15-->>12: Returns true if the hashed password matches + 12-->>8: Sends back an authentication token to
[JSON/HTTPS] \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd index ad523b076..ffccd9df7 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd @@ -4,24 +4,24 @@ graph TB subgraph diagram ["API Application - Dynamic"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 20 [API Application] - style 20 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 11 [API Application] + style 11 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 29["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] - style 29 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 32["
Security Component
[Component: Spring Bean]
Provides functionality
related to signing in,
changing passwords, etc.
"] - style 32 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 12["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] + style 12 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 15["
Security Component
[Component: Spring Bean]
Provides functionality
related to signing in,
changing passwords, etc.
"] + style 15 fill:#85bbf0,stroke:#5d82a8,color:#000000 end - 17["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 17 fill:#438dd5,stroke:#2e6295,color:#ffffff - 21[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 21 fill:#438dd5,stroke:#2e6295,color:#ffffff + 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] + style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff + 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] + style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff - 17-. "
1. Submits credentials to
[JSON/HTTPS]
" .->29 - 29-. "
2. Validates credentials
using
" .->32 - 32-. "
3. select * from users where
username = ?
[JDBC]
" .->21 - 21-. "
4. Returns user data to
[JDBC]
" .->32 - 32-. "
5. Returns true if the hashed
password matches
" .->29 - 29-. "
6. Sends back an
authentication token to
[JSON/HTTPS]
" .->17 + 8-. "
1. Submits credentials to
[JSON/HTTPS]
" .->12 + 12-. "
2. Validates credentials
using
" .->15 + 15-. "
3. select * from users where
username = ?
[SQL/TCP]
" .->18 + 18-. "
4. Returns user data to
[SQL/TCP]
" .->15 + 15-. "
5. Returns true if the hashed
password matches
" .->12 + 12-. "
6. Sends back an
authentication token to
[JSON/HTTPS]
" .->8 end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd index 184335671..66a0a966a 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd @@ -4,17 +4,22 @@ graph TB subgraph diagram ["Internet Banking System - System Context"] style diagram fill:#ffffff,stroke:#ffffff + subgraph group1 [Big Bank plc] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] + style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff + 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] + style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff + 7["
Internet Banking System
[Software System]
Allows customers to view
information about their bank
accounts, and make payments.
"] + style 7 fill:#1168bd,stroke:#0b4884,color:#ffffff + end + 1["
Personal Banking Customer
[Person]
A customer of the bank, with
personal bank accounts.
"] style 1 fill:#08427b,stroke:#052e56,color:#ffffff - 2["
Internet Banking System
[Software System]
Allows customers to view
information about their bank
accounts, and make payments.
"] - style 2 fill:#1168bd,stroke:#0b4884,color:#ffffff - 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 6["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] - style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff - 1-. "
Views account balances, and
makes payments using
" .->2 - 2-. "
Gets account information
from, and makes payments
using
" .->4 - 2-. "
Sends e-mail using
" .->6 - 6-. "
Sends e-mails to
" .->1 + 1-. "
Views account balances, and
makes payments using
" .->7 + 7-. "
Gets account information
from, and makes payments
using
" .->4 + 7-. "
Sends e-mail using
" .->5 + 5-. "
Sends e-mails to
" .->1 end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd index 9e382989d..f763af979 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd @@ -4,33 +4,33 @@ graph TB subgraph diagram ["System Landscape"] style diagram fill:#ffffff,stroke:#ffffff - subgraph enterprise [Big Bank plc] - style enterprise fill:#ffffff,stroke:#444444,color:#444444 + subgraph group1 [Big Bank plc] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - 12["
Customer Service Staff
[Person]
Customer service staff within
the bank.
"] - style 12 fill:#999999,stroke:#6b6b6b,color:#ffffff - 15["
Back Office Staff
[Person]
Administration and support
staff within the bank.
"] - style 15 fill:#999999,stroke:#6b6b6b,color:#ffffff - 2["
Internet Banking System
[Software System]
Allows customers to view
information about their bank
accounts, and make payments.
"] - style 2 fill:#1168bd,stroke:#0b4884,color:#ffffff + 2["
Customer Service Staff
[Person]
Customer service staff within
the bank.
"] + style 2 fill:#999999,stroke:#6b6b6b,color:#ffffff + 3["
Back Office Staff
[Person]
Administration and support
staff within the bank.
"] + style 3 fill:#999999,stroke:#6b6b6b,color:#ffffff 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 6["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] + 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] + style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff + 6["
ATM
[Software System]
Allows customers to withdraw
cash.
"] style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff - 9["
ATM
[Software System]
Allows customers to withdraw
cash.
"] - style 9 fill:#999999,stroke:#6b6b6b,color:#ffffff + 7["
Internet Banking System
[Software System]
Allows customers to view
information about their bank
accounts, and make payments.
"] + style 7 fill:#1168bd,stroke:#0b4884,color:#ffffff end 1["
Personal Banking Customer
[Person]
A customer of the bank, with
personal bank accounts.
"] style 1 fill:#08427b,stroke:#052e56,color:#ffffff - 9-. "
Uses
" .->4 - 1-. "
Withdraws cash using
" .->9 - 12-. "
Uses
" .->4 - 1-. "
Asks questions to
[Telephone]
" .->12 - 15-. "
Uses
" .->4 - 1-. "
Views account balances, and
makes payments using
" .->2 - 2-. "
Gets account information
from, and makes payments
using
" .->4 - 2-. "
Sends e-mail using
" .->6 - 6-. "
Sends e-mails to
" .->1 + 1-. "
Views account balances, and
makes payments using
" .->7 + 7-. "
Gets account information
from, and makes payments
using
" .->4 + 7-. "
Sends e-mail using
" .->5 + 5-. "
Sends e-mails to
" .->1 + 1-. "
Asks questions to
[Telephone]
" .->2 + 2-. "
Uses
" .->4 + 1-. "
Withdraws cash using
" .->6 + 6-. "
Uses
" .->4 + 3-. "
Uses
" .->4 end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd index 79c389eca..8a379ec14 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd @@ -10,7 +10,7 @@ graph TB subgraph 6 [F] style 6 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - subgraph group1 [Group 4] + subgraph group1 [Group 5] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 8["
H
[Component]
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd index 3c5abba9d..c11fc9560 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd @@ -10,7 +10,7 @@ graph TB subgraph 4 [D] style 4 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - subgraph group1 [Group 3] + subgraph group1 [Group 4] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 6["
F
[Container]
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd index 5ca147d80..24cb88486 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd @@ -4,25 +4,25 @@ graph TB subgraph diagram ["System Landscape"] style diagram fill:#ffffff,stroke:#ffffff - subgraph enterprise [Enterprise] - style enterprise fill:#ffffff,stroke:#444444,color:#444444 + subgraph group1 [Group 1] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - subgraph group1 [Group 2] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + 2["
B
[Software System]
"] + style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end - 4["
D
[Software System]
"] - style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end + subgraph group2 [Group 2] + style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 3["
C
[Software System]
"] style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end + subgraph group3 [Group 3] + style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - subgraph group2 [Group 1] - style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + 4["
D
[Software System]
"] + style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end - 2["
B
[Software System]
"] - style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 end 1["
A
[Software System]
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 0acf637cb..4effe64c2 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -28,7 +28,7 @@ public void test_BigBankPlcExample() throws Exception { Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml")); assertEquals(expected, diagram.getDefinition()); - assertEquals(3, diagram.getFrames().size()); + assertEquals(0, diagram.getFrames().size()); //assertEquals("", diagram.getLegend().getDefinition()); @@ -55,12 +55,12 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml")); assertEquals(expected, diagram.getDefinition()); - assertEquals(4, diagram.getFrames().size()); + assertEquals(3, diagram.getFrames().size()); diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml")); assertEquals(expected, diagram.getDefinition()); - assertEquals(6, diagram.getFrames().size()); + assertEquals(5, diagram.getFrames().size()); // and the sequence diagram version workspace.getViews().getConfiguration().addProperty(exporter.PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "true"); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml index 47509b6c7..3ae85ef6e 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml @@ -20,10 +20,10 @@ AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="Solid", $borderThickness="1") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") -Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") -Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") +Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") +Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") @@ -35,17 +35,17 @@ Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Applica } 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.SinglePageApplication, InternetBankingSystem.APIApplication.ResetPasswordController, "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.MobileApp, InternetBankingSystem.APIApplication.ResetPasswordController, "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.SecurityComponent, InternetBankingSystem.Database, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.EmailComponent, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") SHOW_LEGEND(true) diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml index 9badc190c..70127ca7a 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml @@ -11,36 +11,36 @@ top to bottom direction AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Person", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="Solid", $borderThickness="1") -Person_Ext(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person", $link="") +Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") System_Boundary("InternetBankingSystem_boundary", "Internet Banking System", $tags="Software System") { - Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") - Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") Container(InternetBankingSystem.WebApplication, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") Container(InternetBankingSystem.APIApplication, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") } +Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") Rel(PersonalBankingCustomer, InternetBankingSystem.WebApplication, "Visits bigbank.com/ib using", $techn="HTTPS", $tags="Relationship", $link="") Rel(PersonalBankingCustomer, InternetBankingSystem.SinglePageApplication, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") Rel(PersonalBankingCustomer, InternetBankingSystem.MobileApp, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") Rel(InternetBankingSystem.WebApplication, InternetBankingSystem.SinglePageApplication, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication, InternetBankingSystem.Database, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication, EmailSystem, "Sends e-mail using", $techn="SMTP", $tags="Relationship", $link="") Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") Rel(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication, InternetBankingSystem.Database, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") SHOW_LEGEND(true) @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml index d3e9a8d69..f3e11defa 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml @@ -12,31 +12,31 @@ top to bottom direction AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -Deployment_Node(Development.DeveloperLaptop, "Developer Laptop", $type="Microsoft Windows 10 or Apple macOS", $descr="A developer laptop.", $tags="Element", $link="") { - Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer, "Docker Container - Web Server", $type="Docker", $descr="A Docker container.", $tags="Element", $link="") { - Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="An open source Java EE web server.", $tags="Element", $link="") { +Deployment_Node(Development.DeveloperLaptop, "Developer Laptop", $type="Microsoft Windows 10 or Apple macOS", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { + Container(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + } + + Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer, "Docker Container - Web Server", $type="Docker", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") } } - Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer, "Docker Container - Database Server", $type="Docker", $descr="A Docker container.", $tags="Element", $link="") { - Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer, "Database Server", $type="Oracle 12c", $descr="A development database.", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer, "Docker Container - Database Server", $type="Docker", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer, "Database Server", $type="Oracle 12c", $descr="", $tags="Element", $link="") { ContainerDb(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") } } - Deployment_Node(Development.DeveloperLaptop.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { - Container(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") - } - } Deployment_Node(Development.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { @@ -46,10 +46,10 @@ Deployment_Node(Development.BigBankplc, "Big Bank plc", $type="Big Bank plc data } -Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") -Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="") -Rel(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") +Rel(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") +Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") SHOW_LEGEND(true) @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml index 6a33ec429..d3b2df46c 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml @@ -13,12 +13,10 @@ AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#888888", $fontColor AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database,Failover", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddRelTag("Failover", $textColor="#707070", $lineColor="#707070", $lineStyle = "") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") Deployment_Node(Live.Customersmobiledevice, "Customer's mobile device", $type="Apple iOS or Android", $descr="", $tags="Element", $link="") { @@ -33,46 +31,46 @@ Deployment_Node(Live.Customerscomputer, "Customer's computer", $type="Microsoft } Deployment_Node(Live.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankprod001, "bigbank-prod001", $type="", $descr="", $tags="Element", $link="") { - System(Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") - } - - Deployment_Node(Live.BigBankplc.bigbankweb, "bigbank-web*** (x4)", $type="Ubuntu 16.04 LTS", $descr="A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankweb.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="An open source Java EE web server.", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankweb, "bigbank-web*** (x4)", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankweb.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { Container(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") } } - Deployment_Node(Live.BigBankplc.bigbankapi, "bigbank-api*** (x8)", $type="Ubuntu 16.04 LTS", $descr="A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankapi.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="An open source Java EE web server.", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankapi, "bigbank-api*** (x8)", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankapi.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { Container(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") } } - Deployment_Node(Live.BigBankplc.bigbankdb01, "bigbank-db01", $type="Ubuntu 16.04 LTS", $descr="The primary database server.", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankdb01.OraclePrimary, "Oracle - Primary", $type="Oracle 12c", $descr="The primary, live database server.", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankdb01, "bigbank-db01", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankdb01.OraclePrimary, "Oracle - Primary", $type="Oracle 12c", $descr="", $tags="Element", $link="") { ContainerDb(Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") } } - Deployment_Node(Live.BigBankplc.bigbankdb02, "bigbank-db02", $type="Ubuntu 16.04 LTS", $descr="The secondary database server.", $tags="Failover", $link="") { - Deployment_Node(Live.BigBankplc.bigbankdb02.OracleSecondary, "Oracle - Secondary", $type="Oracle 12c", $descr="A secondary, standby database server, used for failover purposes only.", $tags="Failover", $link="") { - ContainerDb(Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database,Failover", $link="") + Deployment_Node(Live.BigBankplc.bigbankdb02, "bigbank-db02", $type="Ubuntu 16.04 LTS", $descr="", $tags="Failover", $link="") { + Deployment_Node(Live.BigBankplc.bigbankdb02.OracleSecondary, "Oracle - Secondary", $type="Oracle 12c", $descr="", $tags="Failover", $link="") { + ContainerDb(Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") } } + Deployment_Node(Live.BigBankplc.bigbankprod001, "bigbank-prod001", $type="", $descr="", $tags="Element", $link="") { + System(Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + } + } Rel(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, Live.Customerscomputer.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") Rel(Live.Customersmobiledevice.MobileApp_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") Rel(Live.Customerscomputer.WebBrowser.SinglePageApplication_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") +Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") +Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") -Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="") -Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2, "Reads from and writes to", $techn="JDBC", $tags="Failover", $link="") Rel(Live.BigBankplc.bigbankdb01.OraclePrimary, Live.BigBankplc.bigbankdb02.OracleSecondary, "Replicates data to", $techn="", $tags="Relationship", $link="") SHOW_LEGEND(true) diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml index 57f8c67a1..3ee74b0c7 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml @@ -17,8 +17,8 @@ ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "Validates credentials using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "select * from users where username = ?", $techn="JDBC", $tags="Relationship", $link="") -Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "Returns user data to", $techn="JDBC", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") +Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml index ee3f343b4..1789c9fa5 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml @@ -22,13 +22,13 @@ Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Applica Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") } -Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") +Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1. Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2. Validates credentials using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3. select * from users where username = ?", $techn="JDBC", $tags="Relationship", $link="") -Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4. Returns user data to", $techn="JDBC", $tags="Relationship", $link="") +Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3. select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") +Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4. Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "5. Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "6. Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml index e4b9cce22..eb64d4554 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml @@ -9,14 +9,18 @@ top to bottom direction AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Person", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -Person_Ext(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person", $link="") -System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") -System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") -System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") +AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { + System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") +} + +Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml index 8f4464816..6a4972739 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml @@ -10,30 +10,31 @@ top to bottom direction AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Person", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -Enterprise_Boundary(enterprise, "Big Bank plc") { +AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { Person(CustomerServiceStaff, "Customer Service Staff", $descr="Customer service staff within the bank.", $tags="Person,Bank Staff", $link="") Person(BackOfficeStaff, "Back Office Staff", $descr="Administration and support staff within the bank.", $tags="Person,Bank Staff", $link="") - System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") System(ATM, "ATM", $descr="Allows customers to withdraw cash.", $tags="Software System,Existing System", $link="") + System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") } -Person_Ext(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person", $link="") +Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") -Rel(ATM, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") -Rel(PersonalBankingCustomer, ATM, "Withdraws cash using", $techn="", $tags="Relationship", $link="") -Rel(CustomerServiceStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") -Rel(PersonalBankingCustomer, CustomerServiceStaff, "Asks questions to", $techn="Telephone", $tags="Relationship", $link="") -Rel(BackOfficeStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") Rel(InternetBankingSystem, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") +Rel(PersonalBankingCustomer, CustomerServiceStaff, "Asks questions to", $techn="Telephone", $tags="Relationship", $link="") +Rel(CustomerServiceStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") +Rel(PersonalBankingCustomer, ATM, "Withdraws cash using", $techn="", $tags="Relationship", $link="") +Rel(ATM, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") +Rel(BackOfficeStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") SHOW_LEGEND(true) @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml index 5707e4f47..b09edaf21 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml @@ -11,8 +11,8 @@ top to bottom direction System(C, "C", $descr="", $tags="", $link="") Container_Boundary("D.F_boundary", "F", $tags="") { - AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") - Boundary(group_1, "Group 4", $tags="Group 4") { + AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_1, "Group 5", $tags="Group 5") { Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml index 42af97606..2b7569d6b 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml @@ -11,8 +11,8 @@ top to bottom direction System(C, "C", $descr="", $tags="", $link="") System_Boundary("D_boundary", "D", $tags="") { - AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") - Boundary(group_1, "Group 3", $tags="Group 3") { + AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_1, "Group 4", $tags="Group 4") { Container(D.F, "F", $techn="", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml index add8abb27..a1620f49f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml @@ -7,21 +7,22 @@ top to bottom direction !include !include -Enterprise_Boundary(enterprise, "Enterprise") { - AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") - Boundary(group_1, "Group 2", $tags="Group 2") { - System(D, "D", $descr="", $tags="", $link="") - } +AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_1, "Group 1", $tags="Group 1") { + System(B, "B", $descr="", $tags="", $link="") +} +AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +Boundary(group_2, "Group 2", $tags="Group 2") { System(C, "C", $descr="", $tags="", $link="") -} + AddBoundaryTag("Group 2/Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + Boundary(group_3, "Group 3", $tags="Group 2/Group 3") { + System(D, "D", $descr="", $tags="", $link="") + } -AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") -Boundary(group_2, "Group 1", $tags="Group 1") { - System_Ext(B, "B", $descr="", $tags="", $link="") } -System_Ext(A, "A", $descr="", $tags="", $link="") +System(A, "A", $descr="", $tags="", $link="") Rel(B, C, "", $techn="", $tags="", $link="") Rel(C, D, "", $techn="", $tags="", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml index edc664115..149271a67 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml @@ -86,10 +86,10 @@ skinparam rectangle<> { } rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem -rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem -rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database +rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication +rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp rectangle "API Application\n[Container: Java and Spring MVC]" <> { rectangle "==Sign In Controller\n[Component: Spring MVC Rest Controller]\n\nAllows users to sign in to the Internet Banking System." <> as InternetBankingSystem.APIApplication.SignInController @@ -101,16 +101,16 @@ rectangle "API Application\n[Container: Java and Spring MVC]" << } InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "Makes API calls to\n[JSON/HTTPS]" InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.AccountsSummaryController : "Makes API calls to\n[JSON/HTTPS]" +InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "Makes API calls to\n[JSON/HTTPS]" InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "Makes API calls to\n[JSON/HTTPS]" InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.AccountsSummaryController : "Makes API calls to\n[JSON/HTTPS]" +InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "Makes API calls to\n[JSON/HTTPS]" InternetBankingSystem.APIApplication.SignInController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "Uses" InternetBankingSystem.APIApplication.AccountsSummaryController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.MainframeBankingSystemFacade : "Uses" InternetBankingSystem.APIApplication.ResetPasswordController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "Uses" InternetBankingSystem.APIApplication.ResetPasswordController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.EmailComponent : "Uses" -InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "Reads from and writes to\n[JDBC]" -InternetBankingSystem.APIApplication.MainframeBankingSystemFacade .[#707070,thickness=2].> MainframeBankingSystem : "Uses\n[XML/HTTPS]" +InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "Reads from and writes to\n[SQL/TCP]" +InternetBankingSystem.APIApplication.MainframeBankingSystemFacade .[#707070,thickness=2].> MainframeBankingSystem : "Makes API calls to\n[XML/HTTPS]" InternetBankingSystem.APIApplication.EmailComponent .[#707070,thickness=2].> EmailSystem : "Sends e-mail using" @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml index 300b0d9e8..40963b9e9 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml @@ -72,21 +72,21 @@ rectangle "==Mainframe Banking System\n[Software System]\n\nStor rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem rectangle "Internet Banking System\n[Software System]" <> { - rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication - rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp rectangle "==Web Application\n[Container: Java and Spring MVC]\n\nDelivers the static content and the Internet banking single page application." <> as InternetBankingSystem.WebApplication rectangle "==API Application\n[Container: Java and Spring MVC]\n\nProvides Internet banking functionality via a JSON/HTTPS API." <> as InternetBankingSystem.APIApplication database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database + rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication + rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp } +EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "Sends e-mails to" PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.WebApplication : "Visits bigbank.com/ib using\n[HTTPS]" PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.SinglePageApplication : "Views account balances, and makes payments using" PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.MobileApp : "Views account balances, and makes payments using" InternetBankingSystem.WebApplication .[#707070,thickness=2].> InternetBankingSystem.SinglePageApplication : "Delivers to the customer's web browser" -InternetBankingSystem.APIApplication .[#707070,thickness=2].> InternetBankingSystem.Database : "Reads from and writes to\n[JDBC]" -InternetBankingSystem.APIApplication .[#707070,thickness=2].> MainframeBankingSystem : "Makes API calls to\n[XML/HTTPS]" -InternetBankingSystem.APIApplication .[#707070,thickness=2].> EmailSystem : "Sends e-mail using\n[SMTP]" InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication : "Makes API calls to\n[JSON/HTTPS]" InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication : "Makes API calls to\n[JSON/HTTPS]" -EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "Sends e-mails to" +InternetBankingSystem.APIApplication .[#707070,thickness=2].> InternetBankingSystem.Database : "Reads from and writes to\n[SQL/TCP]" +InternetBankingSystem.APIApplication .[#707070,thickness=2].> MainframeBankingSystem : "Makes API calls to\n[XML/HTTPS]" +InternetBankingSystem.APIApplication .[#707070,thickness=2].> EmailSystem : "Sends e-mail using" @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml index e35d6fa8c..59d7c4b30 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml @@ -93,6 +93,10 @@ skinparam rectangle<> { } rectangle "Developer Laptop\n[Deployment Node: Microsoft Windows 10 or Apple macOS]" <> as Development.DeveloperLaptop { + rectangle "Web Browser\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Development.DeveloperLaptop.WebBrowser { + rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 + } + rectangle "Docker Container - Web Server\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerWebServer { rectangle "Apache Tomcat\n[Deployment Node: Apache Tomcat 8.x]" <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat { rectangle "==Web Application\n[Container: Java and Spring MVC]\n\nDelivers the static content and the Internet banking single page application." <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 @@ -108,10 +112,6 @@ rectangle "Developer Laptop\n[Deployment Node: Microsoft Windows 10 or } - rectangle "Web Browser\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Development.DeveloperLaptop.WebBrowser { - rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 - } - } rectangle "Big Bank plc\n[Deployment Node: Big Bank plc data center]" <> as Development.BigBankplc { @@ -121,8 +121,8 @@ rectangle "Big Bank plc\n[Deployment Node: Big Bank plc data center]
Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 : "Makes API calls to\n[XML/HTTPS]" -Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 : "Reads from and writes to\n[JDBC]" -Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 : "Makes API calls to\n[JSON/HTTPS]" Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 : "Delivers to the customer's web browser" +Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 : "Makes API calls to\n[JSON/HTTPS]" +Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 : "Reads from and writes to\n[SQL/TCP]" +Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 : "Makes API calls to\n[XML/HTTPS]" @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml index 72427ad33..b7611a0b1 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml @@ -49,7 +49,7 @@ skinparam rectangle<> { BorderColor #888888 shadowing false } -skinparam database<> { +skinparam database<> { BackgroundColor #438dd5 FontColor #ffffff BorderColor #2e6295 @@ -146,10 +146,6 @@ rectangle "Customer's computer\n[Deployment Node: Microsoft Windows or } rectangle "Big Bank plc\n[Deployment Node: Big Bank plc data center]" <> as Live.BigBankplc { - rectangle "bigbank-prod001\n[Deployment Node]" <> as Live.BigBankplc.bigbankprod001 { - rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 - } - rectangle "bigbank-web*** (x4)\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankweb { rectangle "Apache Tomcat\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankweb.ApacheTomcat { rectangle "==Web Application\n[Container: Java and Spring MVC]\n\nDelivers the static content and the Internet banking single page application." <> as Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 @@ -173,18 +169,22 @@ rectangle "Big Bank plc\n[Deployment Node: Big Bank plc data center]
[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb02 { rectangle "Oracle - Secondary\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb02.OracleSecondary { - database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2 + database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 } } + rectangle "bigbank-prod001\n[Deployment Node]" <> as Live.BigBankplc.bigbankprod001 { + rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 + } + } Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 .[#707070,thickness=2].> Live.Customerscomputer.WebBrowser.SinglePageApplication_1 : "Delivers to the customer's web browser" Live.Customersmobiledevice.MobileApp_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 : "Makes API calls to\n[JSON/HTTPS]" Live.Customerscomputer.WebBrowser.SinglePageApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 : "Makes API calls to\n[JSON/HTTPS]" +Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 : "Reads from and writes to\n[SQL/TCP]" +Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 : "Reads from and writes to\n[SQL/TCP]" Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 : "Makes API calls to\n[XML/HTTPS]" -Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 : "Reads from and writes to\n[JDBC]" -Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb02.OracleSecondary.Database_2 : "Reads from and writes to\n[JDBC]" Live.BigBankplc.bigbankdb01.OraclePrimary .[#707070,thickness=2].> Live.BigBankplc.bigbankdb02.OracleSecondary : "Replicates data to" @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml index 687ce9d7e..aad19e957 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml @@ -48,13 +48,13 @@ rectangle "API Application\n[Container: Java and Spring MVC]" << rectangle "==Security Component\n[Component: Spring Bean]\n\nProvides functionality related to signing in, changing passwords, etc." <> as InternetBankingSystem.APIApplication.SecurityComponent } -rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database +rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "1. Submits credentials to\n[JSON/HTTPS]" InternetBankingSystem.APIApplication.SignInController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "2. Validates credentials using" -InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "3. select * from users where username = ?\n[JDBC]" -InternetBankingSystem.APIApplication.SecurityComponent <.[#707070,thickness=2]. InternetBankingSystem.Database : "4. Returns user data to\n[JDBC]" +InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "3. select * from users where username = ?\n[SQL/TCP]" +InternetBankingSystem.APIApplication.SecurityComponent <.[#707070,thickness=2]. InternetBankingSystem.Database : "4. Returns user data to\n[SQL/TCP]" InternetBankingSystem.APIApplication.SignInController <.[#707070,thickness=2]. InternetBankingSystem.APIApplication.SecurityComponent : "5. Returns true if the hashed password matches" InternetBankingSystem.SinglePageApplication <.[#707070,thickness=2]. InternetBankingSystem.APIApplication.SignInController : "6. Sends back an authentication token to\n[JSON/HTTPS]" @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml index 36302bdb3..d4f946a23 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml @@ -38,10 +38,17 @@ skinparam person<> { shadowing false } +rectangle "Big Bank plc" <> { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem + rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem + rectangle "==Internet Banking System\n[Software System]\n\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem +} + person "==Personal Banking Customer\n[Person]\n\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer -rectangle "==Internet Banking System\n[Software System]\n\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem -rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem -rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem : "Views account balances, and makes payments using" InternetBankingSystem .[#707070,thickness=2].> MainframeBankingSystem : "Gets account information from, and makes payments using" diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml index 511b1a16c..b042bb6a5 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml @@ -56,27 +56,28 @@ skinparam person<> { shadowing false } -rectangle "Big Bank plc" <> { - skinparam RectangleBorderColor<> #444444 - skinparam RectangleFontColor<> #444444 +rectangle "Big Bank plc" <> { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed person "==Customer Service Staff\n[Person]\n\nCustomer service staff within the bank." <> as CustomerServiceStaff person "==Back Office Staff\n[Person]\n\nAdministration and support staff within the bank." <> as BackOfficeStaff - rectangle "==Internet Banking System\n[Software System]\n\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem rectangle "==ATM\n[Software System]\n\nAllows customers to withdraw cash." <> as ATM + rectangle "==Internet Banking System\n[Software System]\n\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem } person "==Personal Banking Customer\n[Person]\n\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer -ATM .[#707070,thickness=2].> MainframeBankingSystem : "Uses" -PersonalBankingCustomer .[#707070,thickness=2].> ATM : "Withdraws cash using" -CustomerServiceStaff .[#707070,thickness=2].> MainframeBankingSystem : "Uses" -PersonalBankingCustomer .[#707070,thickness=2].> CustomerServiceStaff : "Asks questions to\n[Telephone]" -BackOfficeStaff .[#707070,thickness=2].> MainframeBankingSystem : "Uses" PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem : "Views account balances, and makes payments using" InternetBankingSystem .[#707070,thickness=2].> MainframeBankingSystem : "Gets account information from, and makes payments using" InternetBankingSystem .[#707070,thickness=2].> EmailSystem : "Sends e-mail using" EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "Sends e-mails to" +PersonalBankingCustomer .[#707070,thickness=2].> CustomerServiceStaff : "Asks questions to\n[Telephone]" +CustomerServiceStaff .[#707070,thickness=2].> MainframeBankingSystem : "Uses" +PersonalBankingCustomer .[#707070,thickness=2].> ATM : "Withdraws cash using" +ATM .[#707070,thickness=2].> MainframeBankingSystem : "Uses" +BackOfficeStaff .[#707070,thickness=2].> MainframeBankingSystem : "Uses" @enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml index 0cc6d9bd4..04af47b24 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml @@ -40,7 +40,7 @@ skinparam rectangle<> { rectangle "==C\n[Software System]" <> as C rectangle "F\n[Container]" <> { - rectangle "Group 4" <> { + rectangle "Group 5" <> { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml index fd5a74bf5..f7d532695 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml @@ -40,7 +40,7 @@ skinparam rectangle<> { rectangle "==C\n[Software System]" <> as C rectangle "D\n[Software System]" <> { - rectangle "Group 3" <> { + rectangle "Group 4" <> { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml index 094f24a31..c967286be 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml @@ -38,27 +38,28 @@ skinparam rectangle<> { shadowing false } -rectangle "Enterprise" <> { - skinparam RectangleBorderColor<> #444444 - skinparam RectangleFontColor<> #444444 +rectangle "Group 1" <> { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed - rectangle "Group 2" <> { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==D\n[Software System]" <> as D - } - - rectangle "==C\n[Software System]" <> as C + rectangle "==B\n[Software System]" <> as B } -rectangle "Group 1" <> { +rectangle "Group 2" <> { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed - rectangle "==B\n[Software System]" <> as B + rectangle "==C\n[Software System]" <> as C + rectangle "Group 3" <> { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "==D\n[Software System]" <> as D + } + } rectangle "==A\n[Software System]" <
> as A diff --git a/structurizr-export/src/test/resources/groups.dsl b/structurizr-export/src/test/resources/groups.dsl new file mode 100644 index 000000000..c2f39a5bc --- /dev/null +++ b/structurizr-export/src/test/resources/groups.dsl @@ -0,0 +1,59 @@ +workspace { + + model { + properties { + structurizr.groupSeparator / + } + + a = softwareSystem "A" + + group "Group 1" { + b = softwareSystem "B" + } + + group "Group 2" { + c = softwareSystem "C" + + group "Group 3" { + d = softwareSystem "D" { + e = container "E" + + group "Group 4" { + f = container "F" { + g = component "G" + + group "Group 5" { + h = component "H" + } + } + } + } + } + } + + a -> b + b -> c + c -> e + c -> g + c -> h + + } + + views { + systemlandscape "SystemLandscape" { + include * + autolayout + } + + container d "Containers" { + include * + autolayout + } + + component f "Components" { + include * + autolayout + } + } + +} diff --git a/structurizr-export/src/test/resources/groups.json b/structurizr-export/src/test/resources/groups.json index 62c3303b8..9ffc544f4 100644 --- a/structurizr-export/src/test/resources/groups.json +++ b/structurizr-export/src/test/resources/groups.json @@ -3,110 +3,165 @@ "name" : "Name", "description" : "Description", "properties" : { - "structurizr.dsl" : "d29ya3NwYWNlIHsKCiAgICBtb2RlbCB7CiAgICAgICAgYSA9IHNvZnR3YXJlU3lzdGVtICJBIgogICAgICAgIAogICAgICAgIGdyb3VwICJHcm91cCAxIiB7CiAgICAgICAgICAgIGIgPSBzb2Z0d2FyZVN5c3RlbSAiQiIKICAgICAgICB9CgogICAgICAgIGVudGVycHJpc2UgIkVudGVycHJpc2UiIHsKICAgICAgICAgICAgYyA9IHNvZnR3YXJlU3lzdGVtICJDIgoKICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDIiIHsKICAgICAgICAgICAgICAgIGQgPSBzb2Z0d2FyZVN5c3RlbSAiRCIgewogICAgICAgICAgICAgICAgICAgIGUgPSBjb250YWluZXIgIkUiCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDMiIHsKICAgICAgICAgICAgICAgICAgICAgICAgZiA9IGNvbnRhaW5lciAiRiIgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgZyA9IGNvbXBvbmVudCAiRyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDQiIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoID0gY29tcG9uZW50ICJIIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIAogICAgICAgIGEgLT4gYgogICAgICAgIGIgLT4gYwogICAgICAgIGMgLT4gZQogICAgICAgIGMgLT4gZwogICAgICAgIGMgLT4gaAoKICAgIH0KICAgIAogICAgdmlld3MgewogICAgICAgIHN5c3RlbWxhbmRzY2FwZSAiU3lzdGVtTGFuZHNjYXBlIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhdXRvbGF5b3V0CiAgICAgICAgfQogICAgICAgIAogICAgICAgIGNvbnRhaW5lciBkICJDb250YWluZXJzIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhdXRvbGF5b3V0CiAgICAgICAgfQoKICAgICAgICBjb21wb25lbnQgZiAiQ29tcG9uZW50cyIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYXV0b2xheW91dAogICAgICAgIH0KICAgIH0KCn0K" + "structurizr.dsl" : "d29ya3NwYWNlIHsKCiAgICBtb2RlbCB7CiAgICAgICAgcHJvcGVydGllcyB7CiAgICAgICAgICAgIHN0cnVjdHVyaXpyLmdyb3VwU2VwYXJhdG9yIC8KICAgICAgICB9CgogICAgICAgIGEgPSBzb2Z0d2FyZVN5c3RlbSAiQSIKCiAgICAgICAgZ3JvdXAgIkdyb3VwIDEiIHsKICAgICAgICAgICAgYiA9IHNvZnR3YXJlU3lzdGVtICJCIgogICAgICAgIH0KCiAgICAgICAgZ3JvdXAgIkdyb3VwIDIiIHsKICAgICAgICAgICAgYyA9IHNvZnR3YXJlU3lzdGVtICJDIgoKICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDMiIHsKICAgICAgICAgICAgICAgIGQgPSBzb2Z0d2FyZVN5c3RlbSAiRCIgewogICAgICAgICAgICAgICAgICAgIGUgPSBjb250YWluZXIgIkUiCgogICAgICAgICAgICAgICAgICAgIGdyb3VwICJHcm91cCA0IiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGYgPSBjb250YWluZXIgIkYiIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGcgPSBjb21wb25lbnQgIkciCgogICAgICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDUiIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoID0gY29tcG9uZW50ICJIIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICBhIC0+IGIKICAgICAgICBiIC0+IGMKICAgICAgICBjIC0+IGUKICAgICAgICBjIC0+IGcKICAgICAgICBjIC0+IGgKCiAgICB9CgogICAgdmlld3MgewogICAgICAgIHN5c3RlbWxhbmRzY2FwZSAiU3lzdGVtTGFuZHNjYXBlIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhdXRvbGF5b3V0CiAgICAgICAgfQoKICAgICAgICBjb250YWluZXIgZCAiQ29udGFpbmVycyIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYXV0b2xheW91dAogICAgICAgIH0KCiAgICAgICAgY29tcG9uZW50IGYgIkNvbXBvbmVudHMiIHsKICAgICAgICAgICAgaW5jbHVkZSAqCiAgICAgICAgICAgIGF1dG9sYXlvdXQKICAgICAgICB9CiAgICB9Cgp9Cg==" }, "configuration" : { }, "model" : { - "enterprise" : { - "name" : "Enterprise" - }, "softwareSystems" : [ { "id" : "1", "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "a" + }, "name" : "A", "relationships" : [ { "id" : "9", "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "370d771e-c1ab-4243-b9cd-2325145b20bc" + }, "sourceId" : "1", "destinationId" : "2" } ], - "location" : "External" + "location" : "Unspecified", + "documentation" : { } }, { "id" : "2", "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "b" + }, "name" : "B", "relationships" : [ { "id" : "10", "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "923c8280-f475-422a-9fef-2b69220bc8e6" + }, "sourceId" : "2", "destinationId" : "3" } ], "group" : "Group 1", - "location" : "External" + "location" : "Unspecified", + "documentation" : { } }, { "id" : "3", "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "c" + }, "name" : "C", "relationships" : [ { "id" : "11", "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "cd5c9209-d4f5-4ffa-a323-dc0d5612cdb8" + }, "sourceId" : "3", "destinationId" : "5" }, { "id" : "12", - "tags" : "Relationship", "sourceId" : "3", - "destinationId" : "4" + "destinationId" : "4", + "linkedRelationshipId" : "11" }, { "id" : "13", "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "0f23b6e5-9276-4468-b3fb-9381f53c8cd7" + }, "sourceId" : "3", "destinationId" : "7" }, { "id" : "14", - "tags" : "Relationship", "sourceId" : "3", - "destinationId" : "6" + "destinationId" : "6", + "linkedRelationshipId" : "13" }, { "id" : "15", "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "b0b99a2e-129b-4f75-81c0-de9c67916229" + }, "sourceId" : "3", "destinationId" : "8" } ], - "location" : "Internal" + "group" : "Group 2", + "location" : "Unspecified", + "documentation" : { } }, { "id" : "4", "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "d" + }, "name" : "D", - "group" : "Group 2", - "location" : "Internal", + "group" : "Group 2/Group 3", + "location" : "Unspecified", "containers" : [ { - "id" : "5", - "tags" : "Element,Container", - "name" : "E" - }, { "id" : "6", "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "f" + }, "name" : "F", - "group" : "Group 3", + "group" : "Group 4", "components" : [ { "id" : "8", "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "h" + }, "name" : "H", - "group" : "Group 4", - "size" : 0 + "group" : "Group 5", + "documentation" : { } }, { "id" : "7", "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "g" + }, "name" : "G", - "size" : 0 - } ] - } ] - } ] + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "5", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "e" + }, + "name" : "E", + "documentation" : { } + } ], + "documentation" : { } + } ], + "properties" : { + "structurizr.groupSeparator" : "/" + } }, "documentation" : { }, "views" : { "systemLandscapeViews" : [ { "key" : "SystemLandscape", + "order" : 1, "automaticLayout" : { "implementation" : "Graphviz", "rankDirection" : "TopBottom", "rankSeparation" : 300, "nodeSeparation" : 300, "edgeSeparation" : 0, - "vertices" : false + "vertices" : false, + "applied" : false }, "enterpriseBoundaryVisible" : true, + "relationships" : [ { + "id" : "12" + }, { + "id" : "9" + }, { + "id" : "10" + } ], "elements" : [ { "id" : "1", "x" : 0, @@ -123,27 +178,27 @@ "id" : "4", "x" : 0, "y" : 0 - } ], - "relationships" : [ { - "id" : "12" - }, { - "id" : "9" - }, { - "id" : "10" } ] } ], "containerViews" : [ { - "softwareSystemId" : "4", "key" : "Containers", + "order" : 2, + "softwareSystemId" : "4", "automaticLayout" : { "implementation" : "Graphviz", "rankDirection" : "TopBottom", "rankSeparation" : 300, "nodeSeparation" : 300, "edgeSeparation" : 0, - "vertices" : false + "vertices" : false, + "applied" : false }, "externalSoftwareSystemBoundariesVisible" : true, + "relationships" : [ { + "id" : "14" + }, { + "id" : "11" + } ], "elements" : [ { "id" : "3", "x" : 0, @@ -156,25 +211,27 @@ "id" : "6", "x" : 0, "y" : 0 - } ], - "relationships" : [ { - "id" : "14" - }, { - "id" : "11" } ] } ], "componentViews" : [ { "key" : "Components", + "order" : 3, "automaticLayout" : { "implementation" : "Graphviz", "rankDirection" : "TopBottom", "rankSeparation" : 300, "nodeSeparation" : 300, "edgeSeparation" : 0, - "vertices" : false + "vertices" : false, + "applied" : false }, "containerId" : "6", "externalContainerBoundariesVisible" : true, + "relationships" : [ { + "id" : "15" + }, { + "id" : "13" + } ], "elements" : [ { "id" : "3", "x" : 0, @@ -187,11 +244,6 @@ "id" : "8", "x" : 0, "y" : 0 - } ], - "relationships" : [ { - "id" : "15" - }, { - "id" : "13" } ] } ], "configuration" : { diff --git a/structurizr-export/src/test/resources/structurizr-36141-workspace.json b/structurizr-export/src/test/resources/structurizr-36141-workspace.json index 0a52ae906..d0162591f 100644 --- a/structurizr-export/src/test/resources/structurizr-36141-workspace.json +++ b/structurizr-export/src/test/resources/structurizr-36141-workspace.json @@ -1,1999 +1,1590 @@ { - "id": 36141, - "name": "Big Bank plc", - "description": "This is an example workspace to illustrate the key features of Structurizr, based around a fictional online banking system.", - "model": { - "enterprise": { - "name": "Big Bank plc" + "id" : 0, + "name" : "Big Bank plc", + "description" : "This is an example workspace to illustrate the key features of Structurizr, via the DSL, based around a fictional online banking system.", + "properties" : { + "structurizr.dsl" : "LyoKICogVGhpcyBpcyBhIGNvbWJpbmVkIHZlcnNpb24gb2YgdGhlIGZvbGxvd2luZyB3b3Jrc3BhY2VzLCB3aXRoIGF1dG9tYXRpYyBsYXlvdXQgZW5hYmxlZDoKICoKICogLSAiQmlnIEJhbmsgcGxjIC0gU3lzdGVtIExhbmRzY2FwZSIgKGh0dHBzOi8vc3RydWN0dXJpenIuY29tL3NoYXJlLzI4MjAxLykKICogLSAiQmlnIEJhbmsgcGxjIC0gSW50ZXJuZXQgQmFua2luZyBTeXN0ZW0iIChodHRwczovL3N0cnVjdHVyaXpyLmNvbS9zaGFyZS8zNjE0MS8pCiovCndvcmtzcGFjZSAiQmlnIEJhbmsgcGxjIiAiVGhpcyBpcyBhbiBleGFtcGxlIHdvcmtzcGFjZSB0byBpbGx1c3RyYXRlIHRoZSBrZXkgZmVhdHVyZXMgb2YgU3RydWN0dXJpenIsIHZpYSB0aGUgRFNMLCBiYXNlZCBhcm91bmQgYSBmaWN0aW9uYWwgb25saW5lIGJhbmtpbmcgc3lzdGVtLiIgewoKICAgIG1vZGVsIHsKICAgICAgICBjdXN0b21lciA9IHBlcnNvbiAiUGVyc29uYWwgQmFua2luZyBDdXN0b21lciIgIkEgY3VzdG9tZXIgb2YgdGhlIGJhbmssIHdpdGggcGVyc29uYWwgYmFuayBhY2NvdW50cy4iICJDdXN0b21lciIKCiAgICAgICAgZ3JvdXAgIkJpZyBCYW5rIHBsYyIgewogICAgICAgICAgICBzdXBwb3J0U3RhZmYgPSBwZXJzb24gIkN1c3RvbWVyIFNlcnZpY2UgU3RhZmYiICJDdXN0b21lciBzZXJ2aWNlIHN0YWZmIHdpdGhpbiB0aGUgYmFuay4iICJCYW5rIFN0YWZmIgogICAgICAgICAgICBiYWNrb2ZmaWNlID0gcGVyc29uICJCYWNrIE9mZmljZSBTdGFmZiIgIkFkbWluaXN0cmF0aW9uIGFuZCBzdXBwb3J0IHN0YWZmIHdpdGhpbiB0aGUgYmFuay4iICJCYW5rIFN0YWZmIgoKICAgICAgICAgICAgbWFpbmZyYW1lID0gc29mdHdhcmVzeXN0ZW0gIk1haW5mcmFtZSBCYW5raW5nIFN5c3RlbSIgIlN0b3JlcyBhbGwgb2YgdGhlIGNvcmUgYmFua2luZyBpbmZvcm1hdGlvbiBhYm91dCBjdXN0b21lcnMsIGFjY291bnRzLCB0cmFuc2FjdGlvbnMsIGV0Yy4iICJFeGlzdGluZyBTeXN0ZW0iCiAgICAgICAgICAgIGVtYWlsID0gc29mdHdhcmVzeXN0ZW0gIkUtbWFpbCBTeXN0ZW0iICJUaGUgaW50ZXJuYWwgTWljcm9zb2Z0IEV4Y2hhbmdlIGUtbWFpbCBzeXN0ZW0uIiAiRXhpc3RpbmcgU3lzdGVtIgogICAgICAgICAgICBhdG0gPSBzb2Z0d2FyZXN5c3RlbSAiQVRNIiAiQWxsb3dzIGN1c3RvbWVycyB0byB3aXRoZHJhdyBjYXNoLiIgIkV4aXN0aW5nIFN5c3RlbSIKCiAgICAgICAgICAgIGludGVybmV0QmFua2luZ1N5c3RlbSA9IHNvZnR3YXJlc3lzdGVtICJJbnRlcm5ldCBCYW5raW5nIFN5c3RlbSIgIkFsbG93cyBjdXN0b21lcnMgdG8gdmlldyBpbmZvcm1hdGlvbiBhYm91dCB0aGVpciBiYW5rIGFjY291bnRzLCBhbmQgbWFrZSBwYXltZW50cy4iIHsKICAgICAgICAgICAgICAgIHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiA9IGNvbnRhaW5lciAiU2luZ2xlLVBhZ2UgQXBwbGljYXRpb24iICJQcm92aWRlcyBhbGwgb2YgdGhlIEludGVybmV0IGJhbmtpbmcgZnVuY3Rpb25hbGl0eSB0byBjdXN0b21lcnMgdmlhIHRoZWlyIHdlYiBicm93c2VyLiIgIkphdmFTY3JpcHQgYW5kIEFuZ3VsYXIiICJXZWIgQnJvd3NlciIKICAgICAgICAgICAgICAgIG1vYmlsZUFwcCA9IGNvbnRhaW5lciAiTW9iaWxlIEFwcCIgIlByb3ZpZGVzIGEgbGltaXRlZCBzdWJzZXQgb2YgdGhlIEludGVybmV0IGJhbmtpbmcgZnVuY3Rpb25hbGl0eSB0byBjdXN0b21lcnMgdmlhIHRoZWlyIG1vYmlsZSBkZXZpY2UuIiAiWGFtYXJpbiIgIk1vYmlsZSBBcHAiCiAgICAgICAgICAgICAgICB3ZWJBcHBsaWNhdGlvbiA9IGNvbnRhaW5lciAiV2ViIEFwcGxpY2F0aW9uIiAiRGVsaXZlcnMgdGhlIHN0YXRpYyBjb250ZW50IGFuZCB0aGUgSW50ZXJuZXQgYmFua2luZyBzaW5nbGUgcGFnZSBhcHBsaWNhdGlvbi4iICJKYXZhIGFuZCBTcHJpbmcgTVZDIgogICAgICAgICAgICAgICAgYXBpQXBwbGljYXRpb24gPSBjb250YWluZXIgIkFQSSBBcHBsaWNhdGlvbiIgIlByb3ZpZGVzIEludGVybmV0IGJhbmtpbmcgZnVuY3Rpb25hbGl0eSB2aWEgYSBKU09OL0hUVFBTIEFQSS4iICJKYXZhIGFuZCBTcHJpbmcgTVZDIiB7CiAgICAgICAgICAgICAgICAgICAgc2lnbmluQ29udHJvbGxlciA9IGNvbXBvbmVudCAiU2lnbiBJbiBDb250cm9sbGVyIiAiQWxsb3dzIHVzZXJzIHRvIHNpZ24gaW4gdG8gdGhlIEludGVybmV0IEJhbmtpbmcgU3lzdGVtLiIgIlNwcmluZyBNVkMgUmVzdCBDb250cm9sbGVyIgogICAgICAgICAgICAgICAgICAgIGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgPSBjb21wb25lbnQgIkFjY291bnRzIFN1bW1hcnkgQ29udHJvbGxlciIgIlByb3ZpZGVzIGN1c3RvbWVycyB3aXRoIGEgc3VtbWFyeSBvZiB0aGVpciBiYW5rIGFjY291bnRzLiIgIlNwcmluZyBNVkMgUmVzdCBDb250cm9sbGVyIgogICAgICAgICAgICAgICAgICAgIHJlc2V0UGFzc3dvcmRDb250cm9sbGVyID0gY29tcG9uZW50ICJSZXNldCBQYXNzd29yZCBDb250cm9sbGVyIiAiQWxsb3dzIHVzZXJzIHRvIHJlc2V0IHRoZWlyIHBhc3N3b3JkcyB3aXRoIGEgc2luZ2xlIHVzZSBVUkwuIiAiU3ByaW5nIE1WQyBSZXN0IENvbnRyb2xsZXIiCiAgICAgICAgICAgICAgICAgICAgc2VjdXJpdHlDb21wb25lbnQgPSBjb21wb25lbnQgIlNlY3VyaXR5IENvbXBvbmVudCIgIlByb3ZpZGVzIGZ1bmN0aW9uYWxpdHkgcmVsYXRlZCB0byBzaWduaW5nIGluLCBjaGFuZ2luZyBwYXNzd29yZHMsIGV0Yy4iICJTcHJpbmcgQmVhbiIKICAgICAgICAgICAgICAgICAgICBtYWluZnJhbWVCYW5raW5nU3lzdGVtRmFjYWRlID0gY29tcG9uZW50ICJNYWluZnJhbWUgQmFua2luZyBTeXN0ZW0gRmFjYWRlIiAiQSBmYWNhZGUgb250byB0aGUgbWFpbmZyYW1lIGJhbmtpbmcgc3lzdGVtLiIgIlNwcmluZyBCZWFuIgogICAgICAgICAgICAgICAgICAgIGVtYWlsQ29tcG9uZW50ID0gY29tcG9uZW50ICJFLW1haWwgQ29tcG9uZW50IiAiU2VuZHMgZS1tYWlscyB0byB1c2Vycy4iICJTcHJpbmcgQmVhbiIKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGRhdGFiYXNlID0gY29udGFpbmVyICJEYXRhYmFzZSIgIlN0b3JlcyB1c2VyIHJlZ2lzdHJhdGlvbiBpbmZvcm1hdGlvbiwgaGFzaGVkIGF1dGhlbnRpY2F0aW9uIGNyZWRlbnRpYWxzLCBhY2Nlc3MgbG9ncywgZXRjLiIgIk9yYWNsZSBEYXRhYmFzZSBTY2hlbWEiICJEYXRhYmFzZSIKICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgIyByZWxhdGlvbnNoaXBzIGJldHdlZW4gcGVvcGxlIGFuZCBzb2Z0d2FyZSBzeXN0ZW1zCiAgICAgICAgY3VzdG9tZXIgLT4gaW50ZXJuZXRCYW5raW5nU3lzdGVtICJWaWV3cyBhY2NvdW50IGJhbGFuY2VzLCBhbmQgbWFrZXMgcGF5bWVudHMgdXNpbmciCiAgICAgICAgaW50ZXJuZXRCYW5raW5nU3lzdGVtIC0+IG1haW5mcmFtZSAiR2V0cyBhY2NvdW50IGluZm9ybWF0aW9uIGZyb20sIGFuZCBtYWtlcyBwYXltZW50cyB1c2luZyIKICAgICAgICBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0gLT4gZW1haWwgIlNlbmRzIGUtbWFpbCB1c2luZyIKICAgICAgICBlbWFpbCAtPiBjdXN0b21lciAiU2VuZHMgZS1tYWlscyB0byIKICAgICAgICBjdXN0b21lciAtPiBzdXBwb3J0U3RhZmYgIkFza3MgcXVlc3Rpb25zIHRvIiAiVGVsZXBob25lIgogICAgICAgIHN1cHBvcnRTdGFmZiAtPiBtYWluZnJhbWUgIlVzZXMiCiAgICAgICAgY3VzdG9tZXIgLT4gYXRtICJXaXRoZHJhd3MgY2FzaCB1c2luZyIKICAgICAgICBhdG0gLT4gbWFpbmZyYW1lICJVc2VzIgogICAgICAgIGJhY2tvZmZpY2UgLT4gbWFpbmZyYW1lICJVc2VzIgoKICAgICAgICAjIHJlbGF0aW9uc2hpcHMgdG8vZnJvbSBjb250YWluZXJzCiAgICAgICAgY3VzdG9tZXIgLT4gd2ViQXBwbGljYXRpb24gIlZpc2l0cyBiaWdiYW5rLmNvbS9pYiB1c2luZyIgIkhUVFBTIgogICAgICAgIGN1c3RvbWVyIC0+IHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiAiVmlld3MgYWNjb3VudCBiYWxhbmNlcywgYW5kIG1ha2VzIHBheW1lbnRzIHVzaW5nIgogICAgICAgIGN1c3RvbWVyIC0+IG1vYmlsZUFwcCAiVmlld3MgYWNjb3VudCBiYWxhbmNlcywgYW5kIG1ha2VzIHBheW1lbnRzIHVzaW5nIgogICAgICAgIHdlYkFwcGxpY2F0aW9uIC0+IHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiAiRGVsaXZlcnMgdG8gdGhlIGN1c3RvbWVyJ3Mgd2ViIGJyb3dzZXIiCgogICAgICAgICMgcmVsYXRpb25zaGlwcyB0by9mcm9tIGNvbXBvbmVudHMKICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gc2lnbmluQ29udHJvbGxlciAiTWFrZXMgQVBJIGNhbGxzIHRvIiAiSlNPTi9IVFRQUyIKICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gYWNjb3VudHNTdW1tYXJ5Q29udHJvbGxlciAiTWFrZXMgQVBJIGNhbGxzIHRvIiAiSlNPTi9IVFRQUyIKICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gcmVzZXRQYXNzd29yZENvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgbW9iaWxlQXBwIC0+IHNpZ25pbkNvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgbW9iaWxlQXBwIC0+IGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgbW9iaWxlQXBwIC0+IHJlc2V0UGFzc3dvcmRDb250cm9sbGVyICJNYWtlcyBBUEkgY2FsbHMgdG8iICJKU09OL0hUVFBTIgogICAgICAgIHNpZ25pbkNvbnRyb2xsZXIgLT4gc2VjdXJpdHlDb21wb25lbnQgIlVzZXMiCiAgICAgICAgYWNjb3VudHNTdW1tYXJ5Q29udHJvbGxlciAtPiBtYWluZnJhbWVCYW5raW5nU3lzdGVtRmFjYWRlICJVc2VzIgogICAgICAgIHJlc2V0UGFzc3dvcmRDb250cm9sbGVyIC0+IHNlY3VyaXR5Q29tcG9uZW50ICJVc2VzIgogICAgICAgIHJlc2V0UGFzc3dvcmRDb250cm9sbGVyIC0+IGVtYWlsQ29tcG9uZW50ICJVc2VzIgogICAgICAgIHNlY3VyaXR5Q29tcG9uZW50IC0+IGRhdGFiYXNlICJSZWFkcyBmcm9tIGFuZCB3cml0ZXMgdG8iICJTUUwvVENQIgogICAgICAgIG1haW5mcmFtZUJhbmtpbmdTeXN0ZW1GYWNhZGUgLT4gbWFpbmZyYW1lICJNYWtlcyBBUEkgY2FsbHMgdG8iICJYTUwvSFRUUFMiCiAgICAgICAgZW1haWxDb21wb25lbnQgLT4gZW1haWwgIlNlbmRzIGUtbWFpbCB1c2luZyIKCiAgICAgICAgZGVwbG95bWVudEVudmlyb25tZW50ICJEZXZlbG9wbWVudCIgewogICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiRGV2ZWxvcGVyIExhcHRvcCIgIiIgIk1pY3Jvc29mdCBXaW5kb3dzIDEwIG9yIEFwcGxlIG1hY09TIiB7CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiV2ViIEJyb3dzZXIiICIiICJDaHJvbWUsIEZpcmVmb3gsIFNhZmFyaSwgb3IgRWRnZSIgewogICAgICAgICAgICAgICAgICAgIGRldmVsb3BlclNpbmdsZVBhZ2VBcHBsaWNhdGlvbkluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2Ugc2luZ2xlUGFnZUFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiRG9ja2VyIENvbnRhaW5lciAtIFdlYiBTZXJ2ZXIiICIiICJEb2NrZXIiIHsKICAgICAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQXBhY2hlIFRvbWNhdCIgIiIgIkFwYWNoZSBUb21jYXQgOC54IiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGRldmVsb3BlcldlYkFwcGxpY2F0aW9uSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSB3ZWJBcHBsaWNhdGlvbgogICAgICAgICAgICAgICAgICAgICAgICBkZXZlbG9wZXJBcGlBcHBsaWNhdGlvbkluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgYXBpQXBwbGljYXRpb24KICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiRG9ja2VyIENvbnRhaW5lciAtIERhdGFiYXNlIFNlcnZlciIgIiIgIkRvY2tlciIgewogICAgICAgICAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJEYXRhYmFzZSBTZXJ2ZXIiICIiICJPcmFjbGUgMTJjIiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGRldmVsb3BlckRhdGFiYXNlSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSBkYXRhYmFzZQogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQmlnIEJhbmsgcGxjIiAiIiAiQmlnIEJhbmsgcGxjIGRhdGEgY2VudGVyIiAiIiB7CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiYmlnYmFuay1kZXYwMDEiICIiICIiICIiIHsKICAgICAgICAgICAgICAgICAgICBzb2Z0d2FyZVN5c3RlbUluc3RhbmNlIG1haW5mcmFtZQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CgogICAgICAgIH0KCiAgICAgICAgZGVwbG95bWVudEVudmlyb25tZW50ICJMaXZlIiB7CiAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJDdXN0b21lcidzIG1vYmlsZSBkZXZpY2UiICIiICJBcHBsZSBpT1Mgb3IgQW5kcm9pZCIgewogICAgICAgICAgICAgICAgbGl2ZU1vYmlsZUFwcEluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgbW9iaWxlQXBwCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkN1c3RvbWVyJ3MgY29tcHV0ZXIiICIiICJNaWNyb3NvZnQgV2luZG93cyBvciBBcHBsZSBtYWNPUyIgewogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIldlYiBCcm93c2VyIiAiIiAiQ2hyb21lLCBGaXJlZm94LCBTYWZhcmksIG9yIEVkZ2UiIHsKICAgICAgICAgICAgICAgICAgICBsaXZlU2luZ2xlUGFnZUFwcGxpY2F0aW9uSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSBzaW5nbGVQYWdlQXBwbGljYXRpb24KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQoKICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkJpZyBCYW5rIHBsYyIgIiIgIkJpZyBCYW5rIHBsYyBkYXRhIGNlbnRlciIgewogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2Jhbmstd2ViKioqIiAiIiAiVWJ1bnR1IDE2LjA0IExUUyIgIiIgNCB7CiAgICAgICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkFwYWNoZSBUb21jYXQiICIiICJBcGFjaGUgVG9tY2F0IDgueCIgewogICAgICAgICAgICAgICAgICAgICAgICBsaXZlV2ViQXBwbGljYXRpb25JbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIHdlYkFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2JhbmstYXBpKioqIiAiIiAiVWJ1bnR1IDE2LjA0IExUUyIgIiIgOCB7CiAgICAgICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkFwYWNoZSBUb21jYXQiICIiICJBcGFjaGUgVG9tY2F0IDgueCIgewogICAgICAgICAgICAgICAgICAgICAgICBsaXZlQXBpQXBwbGljYXRpb25JbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIGFwaUFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJiaWdiYW5rLWRiMDEiICIiICJVYnVudHUgMTYuMDQgTFRTIiB7CiAgICAgICAgICAgICAgICAgICAgcHJpbWFyeURhdGFiYXNlU2VydmVyID0gZGVwbG95bWVudE5vZGUgIk9yYWNsZSAtIFByaW1hcnkiICIiICJPcmFjbGUgMTJjIiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxpdmVQcmltYXJ5RGF0YWJhc2VJbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIGRhdGFiYXNlCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2JhbmstZGIwMiIgIiIgIlVidW50dSAxNi4wNCBMVFMiICJGYWlsb3ZlciIgewogICAgICAgICAgICAgICAgICAgIHNlY29uZGFyeURhdGFiYXNlU2VydmVyID0gZGVwbG95bWVudE5vZGUgIk9yYWNsZSAtIFNlY29uZGFyeSIgIiIgIk9yYWNsZSAxMmMiICJGYWlsb3ZlciIgewogICAgICAgICAgICAgICAgICAgICAgICBsaXZlU2Vjb25kYXJ5RGF0YWJhc2VJbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIGRhdGFiYXNlICJGYWlsb3ZlciIKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiYmlnYmFuay1wcm9kMDAxIiAiIiAiIiAiIiB7CiAgICAgICAgICAgICAgICAgICAgc29mdHdhcmVTeXN0ZW1JbnN0YW5jZSBtYWluZnJhbWUKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQoKICAgICAgICAgICAgcHJpbWFyeURhdGFiYXNlU2VydmVyIC0+IHNlY29uZGFyeURhdGFiYXNlU2VydmVyICJSZXBsaWNhdGVzIGRhdGEgdG8iCiAgICAgICAgfQogICAgfQoKICAgIHZpZXdzIHsKICAgICAgICBzeXN0ZW1sYW5kc2NhcGUgIlN5c3RlbUxhbmRzY2FwZSIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYXV0b0xheW91dAogICAgICAgIH0KCiAgICAgICAgc3lzdGVtY29udGV4dCBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0gIlN5c3RlbUNvbnRleHQiIHsKICAgICAgICAgICAgaW5jbHVkZSAqCiAgICAgICAgICAgIGFuaW1hdGlvbiB7CiAgICAgICAgICAgICAgICBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0KICAgICAgICAgICAgICAgIGN1c3RvbWVyCiAgICAgICAgICAgICAgICBtYWluZnJhbWUKICAgICAgICAgICAgICAgIGVtYWlsCiAgICAgICAgICAgIH0KICAgICAgICAgICAgYXV0b0xheW91dAogICAgICAgICAgICBkZXNjcmlwdGlvbiAiVGhlIHN5c3RlbSBjb250ZXh0IGRpYWdyYW0gZm9yIHRoZSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iCiAgICAgICAgICAgIHByb3BlcnRpZXMgewogICAgICAgICAgICAgICAgc3RydWN0dXJpenIuZ3JvdXBzIGZhbHNlCiAgICAgICAgICAgIH0KICAgICAgICB9CgogICAgICAgIGNvbnRhaW5lciBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0gIkNvbnRhaW5lcnMiIHsKICAgICAgICAgICAgaW5jbHVkZSAqCiAgICAgICAgICAgIGFuaW1hdGlvbiB7CiAgICAgICAgICAgICAgICBjdXN0b21lciBtYWluZnJhbWUgZW1haWwKICAgICAgICAgICAgICAgIHdlYkFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24KICAgICAgICAgICAgICAgIG1vYmlsZUFwcAogICAgICAgICAgICAgICAgYXBpQXBwbGljYXRpb24KICAgICAgICAgICAgICAgIGRhdGFiYXNlCiAgICAgICAgICAgIH0KICAgICAgICAgICAgYXV0b0xheW91dAogICAgICAgICAgICBkZXNjcmlwdGlvbiAiVGhlIGNvbnRhaW5lciBkaWFncmFtIGZvciB0aGUgSW50ZXJuZXQgQmFua2luZyBTeXN0ZW0uIgogICAgICAgIH0KCiAgICAgICAgY29tcG9uZW50IGFwaUFwcGxpY2F0aW9uICJDb21wb25lbnRzIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhbmltYXRpb24gewogICAgICAgICAgICAgICAgc2luZ2xlUGFnZUFwcGxpY2F0aW9uIG1vYmlsZUFwcCBkYXRhYmFzZSBlbWFpbCBtYWluZnJhbWUKICAgICAgICAgICAgICAgIHNpZ25pbkNvbnRyb2xsZXIgc2VjdXJpdHlDb21wb25lbnQKICAgICAgICAgICAgICAgIGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgbWFpbmZyYW1lQmFua2luZ1N5c3RlbUZhY2FkZQogICAgICAgICAgICAgICAgcmVzZXRQYXNzd29yZENvbnRyb2xsZXIgZW1haWxDb21wb25lbnQKICAgICAgICAgICAgfQogICAgICAgICAgICBhdXRvTGF5b3V0CiAgICAgICAgICAgIGRlc2NyaXB0aW9uICJUaGUgY29tcG9uZW50IGRpYWdyYW0gZm9yIHRoZSBBUEkgQXBwbGljYXRpb24uIgogICAgICAgIH0KCiAgICAgICAgaW1hZ2UgbWFpbmZyYW1lQmFua2luZ1N5c3RlbUZhY2FkZSAiTWFpbmZyYW1lQmFua2luZ1N5c3RlbUZhY2FkZSIgewogICAgICAgICAgICBpbWFnZSBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vc3RydWN0dXJpenIvZXhhbXBsZXMvbWFpbi9kc2wvYmlnLWJhbmstcGxjL2ludGVybmV0LWJhbmtpbmctc3lzdGVtL21haW5mcmFtZS1iYW5raW5nLXN5c3RlbS1mYWNhZGUucG5nCiAgICAgICAgICAgIHRpdGxlICJbQ29kZV0gTWFpbmZyYW1lIEJhbmtpbmcgU3lzdGVtIEZhY2FkZSIKICAgICAgICB9CgogICAgICAgIGR5bmFtaWMgYXBpQXBwbGljYXRpb24gIlNpZ25JbiIgIlN1bW1hcmlzZXMgaG93IHRoZSBzaWduIGluIGZlYXR1cmUgd29ya3MgaW4gdGhlIHNpbmdsZS1wYWdlIGFwcGxpY2F0aW9uLiIgewogICAgICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gc2lnbmluQ29udHJvbGxlciAiU3VibWl0cyBjcmVkZW50aWFscyB0byIKICAgICAgICAgICAgc2lnbmluQ29udHJvbGxlciAtPiBzZWN1cml0eUNvbXBvbmVudCAiVmFsaWRhdGVzIGNyZWRlbnRpYWxzIHVzaW5nIgogICAgICAgICAgICBzZWN1cml0eUNvbXBvbmVudCAtPiBkYXRhYmFzZSAic2VsZWN0ICogZnJvbSB1c2VycyB3aGVyZSB1c2VybmFtZSA9ID8iCiAgICAgICAgICAgIGRhdGFiYXNlIC0+IHNlY3VyaXR5Q29tcG9uZW50ICJSZXR1cm5zIHVzZXIgZGF0YSB0byIKICAgICAgICAgICAgc2VjdXJpdHlDb21wb25lbnQgLT4gc2lnbmluQ29udHJvbGxlciAiUmV0dXJucyB0cnVlIGlmIHRoZSBoYXNoZWQgcGFzc3dvcmQgbWF0Y2hlcyIKICAgICAgICAgICAgc2lnbmluQ29udHJvbGxlciAtPiBzaW5nbGVQYWdlQXBwbGljYXRpb24gIlNlbmRzIGJhY2sgYW4gYXV0aGVudGljYXRpb24gdG9rZW4gdG8iCiAgICAgICAgICAgIGF1dG9MYXlvdXQKICAgICAgICAgICAgZGVzY3JpcHRpb24gIlN1bW1hcmlzZXMgaG93IHRoZSBzaWduIGluIGZlYXR1cmUgd29ya3MgaW4gdGhlIHNpbmdsZS1wYWdlIGFwcGxpY2F0aW9uLiIKICAgICAgICB9CgogICAgICAgIGRlcGxveW1lbnQgaW50ZXJuZXRCYW5raW5nU3lzdGVtICJEZXZlbG9wbWVudCIgIkRldmVsb3BtZW50RGVwbG95bWVudCIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYW5pbWF0aW9uIHsKICAgICAgICAgICAgICAgIGRldmVsb3BlclNpbmdsZVBhZ2VBcHBsaWNhdGlvbkluc3RhbmNlCiAgICAgICAgICAgICAgICBkZXZlbG9wZXJXZWJBcHBsaWNhdGlvbkluc3RhbmNlIGRldmVsb3BlckFwaUFwcGxpY2F0aW9uSW5zdGFuY2UKICAgICAgICAgICAgICAgIGRldmVsb3BlckRhdGFiYXNlSW5zdGFuY2UKICAgICAgICAgICAgfQogICAgICAgICAgICBhdXRvTGF5b3V0CiAgICAgICAgICAgIGRlc2NyaXB0aW9uICJBbiBleGFtcGxlIGRldmVsb3BtZW50IGRlcGxveW1lbnQgc2NlbmFyaW8gZm9yIHRoZSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iCiAgICAgICAgfQoKICAgICAgICBkZXBsb3ltZW50IGludGVybmV0QmFua2luZ1N5c3RlbSAiTGl2ZSIgIkxpdmVEZXBsb3ltZW50IiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhbmltYXRpb24gewogICAgICAgICAgICAgICAgbGl2ZVNpbmdsZVBhZ2VBcHBsaWNhdGlvbkluc3RhbmNlCiAgICAgICAgICAgICAgICBsaXZlTW9iaWxlQXBwSW5zdGFuY2UKICAgICAgICAgICAgICAgIGxpdmVXZWJBcHBsaWNhdGlvbkluc3RhbmNlIGxpdmVBcGlBcHBsaWNhdGlvbkluc3RhbmNlCiAgICAgICAgICAgICAgICBsaXZlUHJpbWFyeURhdGFiYXNlSW5zdGFuY2UKICAgICAgICAgICAgICAgIGxpdmVTZWNvbmRhcnlEYXRhYmFzZUluc3RhbmNlCiAgICAgICAgICAgIH0KICAgICAgICAgICAgYXV0b0xheW91dAogICAgICAgICAgICBkZXNjcmlwdGlvbiAiQW4gZXhhbXBsZSBsaXZlIGRlcGxveW1lbnQgc2NlbmFyaW8gZm9yIHRoZSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iCiAgICAgICAgfQoKICAgICAgICBzdHlsZXMgewogICAgICAgICAgICBlbGVtZW50ICJQZXJzb24iIHsKICAgICAgICAgICAgICAgIGNvbG9yICNmZmZmZmYKICAgICAgICAgICAgICAgIGZvbnRTaXplIDIyCiAgICAgICAgICAgICAgICBzaGFwZSBQZXJzb24KICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJDdXN0b21lciIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjMDg0MjdiCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiQmFuayBTdGFmZiIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjOTk5OTk5CiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiU29mdHdhcmUgU3lzdGVtIiB7CiAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kICMxMTY4YmQKICAgICAgICAgICAgICAgIGNvbG9yICNmZmZmZmYKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJFeGlzdGluZyBTeXN0ZW0iIHsKICAgICAgICAgICAgICAgIGJhY2tncm91bmQgIzk5OTk5OQogICAgICAgICAgICAgICAgY29sb3IgI2ZmZmZmZgogICAgICAgICAgICB9CiAgICAgICAgICAgIGVsZW1lbnQgIkNvbnRhaW5lciIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjNDM4ZGQ1CiAgICAgICAgICAgICAgICBjb2xvciAjZmZmZmZmCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiV2ViIEJyb3dzZXIiIHsKICAgICAgICAgICAgICAgIHNoYXBlIFdlYkJyb3dzZXIKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJNb2JpbGUgQXBwIiB7CiAgICAgICAgICAgICAgICBzaGFwZSBNb2JpbGVEZXZpY2VMYW5kc2NhcGUKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJEYXRhYmFzZSIgewogICAgICAgICAgICAgICAgc2hhcGUgQ3lsaW5kZXIKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJDb21wb25lbnQiIHsKICAgICAgICAgICAgICAgIGJhY2tncm91bmQgIzg1YmJmMAogICAgICAgICAgICAgICAgY29sb3IgIzAwMDAwMAogICAgICAgICAgICB9CiAgICAgICAgICAgIGVsZW1lbnQgIkZhaWxvdmVyIiB7CiAgICAgICAgICAgICAgICBvcGFjaXR5IDI1CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9Cn0K" + }, + "configuration" : { }, + "model" : { + "people" : [ { + "id" : "1", + "tags" : "Element,Person,Customer", + "properties" : { + "structurizr.dsl.identifier" : "customer" + }, + "name" : "Personal Banking Customer", + "description" : "A customer of the bank, with personal bank accounts.", + "relationships" : [ { + "id" : "19", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "6d299902-3f78-4a39-a8ca-3a61e3e50f3e" }, - "people": [ - { - "id": "15", - "tags": "Element,Person,Bank Staff", - "name": "Back Office Staff", - "description": "Administration and support staff within the bank.", - "relationships": [ - { - "id": "16", - "tags": "Relationship", - "sourceId": "15", - "destinationId": "4", - "description": "Uses" - } - ], - "location": "Internal" + "sourceId" : "1", + "destinationId" : "7", + "description" : "Views account balances, and makes payments using" + }, { + "id" : "23", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "e61b034b-4617-4f42-8831-b171bdd5f7df" + }, + "sourceId" : "1", + "destinationId" : "2", + "description" : "Asks questions to", + "technology" : "Telephone" + }, { + "id" : "25", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "2ebe5df0-e547-4c9b-9403-a83561d958ea" + }, + "sourceId" : "1", + "destinationId" : "6", + "description" : "Withdraws cash using" + }, { + "id" : "28", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "4a08e84b-acb3-4fc4-a7f1-8ebe621a0f0f" + }, + "sourceId" : "1", + "destinationId" : "10", + "description" : "Visits bigbank.com/ib using", + "technology" : "HTTPS" + }, { + "id" : "29", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "025a49a0-4557-44bb-9724-2d7ea4d7e1ed" + }, + "sourceId" : "1", + "destinationId" : "8", + "description" : "Views account balances, and makes payments using" + }, { + "id" : "30", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "c6295d75-dd8c-494e-8d69-096540a25b4b" + }, + "sourceId" : "1", + "destinationId" : "9", + "description" : "Views account balances, and makes payments using" + } ], + "location" : "Unspecified" + }, { + "id" : "2", + "tags" : "Element,Person,Bank Staff", + "properties" : { + "structurizr.dsl.identifier" : "supportstaff" + }, + "name" : "Customer Service Staff", + "description" : "Customer service staff within the bank.", + "relationships" : [ { + "id" : "24", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "ad399353-807e-4350-ad5f-2359c54844db" + }, + "sourceId" : "2", + "destinationId" : "4", + "description" : "Uses" + } ], + "group" : "Big Bank plc", + "location" : "Unspecified" + }, { + "id" : "3", + "tags" : "Element,Person,Bank Staff", + "properties" : { + "structurizr.dsl.identifier" : "backoffice" + }, + "name" : "Back Office Staff", + "description" : "Administration and support staff within the bank.", + "relationships" : [ { + "id" : "27", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "a4243927-b19d-423f-9f2c-6d84ba3604e3" + }, + "sourceId" : "3", + "destinationId" : "4", + "description" : "Uses" + } ], + "group" : "Big Bank plc", + "location" : "Unspecified" + } ], + "softwareSystems" : [ { + "id" : "4", + "tags" : "Element,Software System,Existing System", + "properties" : { + "structurizr.dsl.identifier" : "mainframe" + }, + "name" : "Mainframe Banking System", + "description" : "Stores all of the core banking information about customers, accounts, transactions, etc.", + "group" : "Big Bank plc", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "5", + "tags" : "Element,Software System,Existing System", + "properties" : { + "structurizr.dsl.identifier" : "email" + }, + "name" : "E-mail System", + "description" : "The internal Microsoft Exchange e-mail system.", + "relationships" : [ { + "id" : "22", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "93b2b770-fec1-40f4-ab14-cec2eb3c75d6" + }, + "sourceId" : "5", + "destinationId" : "1", + "description" : "Sends e-mails to" + } ], + "group" : "Big Bank plc", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "6", + "tags" : "Element,Software System,Existing System", + "properties" : { + "structurizr.dsl.identifier" : "atm" + }, + "name" : "ATM", + "description" : "Allows customers to withdraw cash.", + "relationships" : [ { + "id" : "26", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "e2e730e2-ce51-4154-9847-b8d73823cdc2" + }, + "sourceId" : "6", + "destinationId" : "4", + "description" : "Uses" + } ], + "group" : "Big Bank plc", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "7", + "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "internetbankingsystem" + }, + "name" : "Internet Banking System", + "description" : "Allows customers to view information about their bank accounts, and make payments.", + "relationships" : [ { + "id" : "20", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "bd46add8-7f56-4639-9f1c-1569a681ad92" + }, + "sourceId" : "7", + "destinationId" : "4", + "description" : "Gets account information from, and makes payments using" + }, { + "id" : "21", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "c58d1eb9-d9da-4f37-b9f5-e2035c651abd" + }, + "sourceId" : "7", + "destinationId" : "5", + "description" : "Sends e-mail using" + } ], + "group" : "Big Bank plc", + "location" : "Unspecified", + "containers" : [ { + "id" : "18", + "tags" : "Element,Container,Database", + "properties" : { + "structurizr.dsl.identifier" : "database" + }, + "name" : "Database", + "description" : "Stores user registration information, hashed authentication credentials, access logs, etc.", + "technology" : "Oracle Database Schema", + "documentation" : { } + }, { + "id" : "9", + "tags" : "Element,Container,Mobile App", + "properties" : { + "structurizr.dsl.identifier" : "mobileapp" + }, + "name" : "Mobile App", + "description" : "Provides a limited subset of the Internet banking functionality to customers via their mobile device.", + "relationships" : [ { + "id" : "36", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "5fce96a5-2f6c-41e1-9263-26174a28ced1" + }, + "sourceId" : "9", + "destinationId" : "12", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS" + }, { + "id" : "37", + "sourceId" : "9", + "destinationId" : "11", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS", + "linkedRelationshipId" : "36" + }, { + "id" : "38", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "3ee42ae6-31e2-4644-8d27-0d9f8fde6919" + }, + "sourceId" : "9", + "destinationId" : "13", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS" + }, { + "id" : "39", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "65f4415c-7575-4230-bd32-6d8d754d01ca" + }, + "sourceId" : "9", + "destinationId" : "14", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS" + } ], + "technology" : "Xamarin", + "documentation" : { } + }, { + "id" : "10", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "webapplication" + }, + "name" : "Web Application", + "description" : "Delivers the static content and the Internet banking single page application.", + "relationships" : [ { + "id" : "31", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "792ce67f-78f1-4c65-933b-84eece4334e2" + }, + "sourceId" : "10", + "destinationId" : "8", + "description" : "Delivers to the customer's web browser" + } ], + "technology" : "Java and Spring MVC", + "documentation" : { } + }, { + "id" : "11", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "apiapplication" + }, + "name" : "API Application", + "description" : "Provides Internet banking functionality via a JSON/HTTPS API.", + "relationships" : [ { + "id" : "45", + "sourceId" : "11", + "destinationId" : "18", + "description" : "Reads from and writes to", + "technology" : "SQL/TCP", + "linkedRelationshipId" : "44" + }, { + "id" : "47", + "sourceId" : "11", + "destinationId" : "4", + "description" : "Makes API calls to", + "technology" : "XML/HTTPS", + "linkedRelationshipId" : "46" + }, { + "id" : "49", + "sourceId" : "11", + "destinationId" : "5", + "description" : "Sends e-mail using", + "linkedRelationshipId" : "48" + } ], + "technology" : "Java and Spring MVC", + "components" : [ { + "id" : "16", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "mainframebankingsystemfacade" + }, + "name" : "Mainframe Banking System Facade", + "description" : "A facade onto the mainframe banking system.", + "relationships" : [ { + "id" : "46", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "2f9ddb3a-77a2-433c-9257-49bfe5bb0766" }, - { - "id": "12", - "tags": "Element,Person,Bank Staff", - "name": "Customer Service Staff", - "description": "Customer service staff within the bank.", - "relationships": [ - { - "id": "13", - "tags": "Relationship", - "sourceId": "12", - "destinationId": "4", - "description": "Uses" - } - ], - "location": "Internal" + "sourceId" : "16", + "destinationId" : "4", + "description" : "Makes API calls to", + "technology" : "XML/HTTPS" + } ], + "technology" : "Spring Bean", + "documentation" : { } + }, { + "id" : "12", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "signincontroller" + }, + "name" : "Sign In Controller", + "description" : "Allows users to sign in to the Internet Banking System.", + "relationships" : [ { + "id" : "40", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "2f405114-17b0-4223-aaa1-12f87d1ca8ba" }, - { - "id": "1", - "tags": "Element,Person", - "name": "Personal Banking Customer", - "description": "A customer of the bank, with personal bank accounts.", - "relationships": [ - { - "id": "3", - "tags": "Relationship", - "sourceId": "1", - "destinationId": "2", - "description": "Views account balances, and makes payments using" - }, - { - "id": "23", - "tags": "Relationship", - "sourceId": "1", - "destinationId": "17", - "description": "Views account balances, and makes payments using" - }, - { - "id": "22", - "tags": "Relationship", - "sourceId": "1", - "destinationId": "19", - "description": "Visits bigbank.com/ib using", - "technology": "HTTPS" - }, - { - "id": "24", - "tags": "Relationship", - "sourceId": "1", - "destinationId": "18", - "description": "Views account balances, and makes payments using" - }, - { - "id": "11", - "tags": "Relationship", - "sourceId": "1", - "destinationId": "9", - "description": "Withdraws cash using" - }, - { - "id": "14", - "tags": "Relationship,Synchronous", - "sourceId": "1", - "destinationId": "12", - "description": "Asks questions to", - "technology": "Telephone", - "interactionStyle": "Synchronous" - } - ], - "location": "External" - } - ], - "softwareSystems": [ - { - "id": "9", - "tags": "Element,Software System,Existing System", - "name": "ATM", - "description": "Allows customers to withdraw cash.", - "relationships": [ - { - "id": "10", - "tags": "Relationship", - "sourceId": "9", - "destinationId": "4", - "description": "Uses" - } - ], - "location": "Internal" + "sourceId" : "12", + "destinationId" : "15", + "description" : "Uses" + } ], + "technology" : "Spring MVC Rest Controller", + "documentation" : { } + }, { + "id" : "13", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "accountssummarycontroller" + }, + "name" : "Accounts Summary Controller", + "description" : "Provides customers with a summary of their bank accounts.", + "relationships" : [ { + "id" : "41", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "9920b188-70d1-4f83-b51d-a2f7eb4c6ffc" }, - { - "id": "6", - "tags": "Element,Software System,Existing System", - "name": "E-mail System", - "description": "The internal Microsoft Exchange e-mail system.", - "relationships": [ - { - "id": "8", - "tags": "Relationship", - "sourceId": "6", - "destinationId": "1", - "description": "Sends e-mails to" - } - ], - "location": "Internal" + "sourceId" : "13", + "destinationId" : "16", + "description" : "Uses" + } ], + "technology" : "Spring MVC Rest Controller", + "documentation" : { } + }, { + "id" : "15", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "securitycomponent" + }, + "name" : "Security Component", + "description" : "Provides functionality related to signing in, changing passwords, etc.", + "relationships" : [ { + "id" : "44", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "67d3a4e5-1e3f-4046-a121-c0a342a26d25" }, - { - "id": "2", - "tags": "Element,Software System", - "name": "Internet Banking System", - "description": "Allows customers to view information about their bank accounts, and make payments.", - "relationships": [ - { - "id": "5", - "tags": "Relationship", - "sourceId": "2", - "destinationId": "4", - "description": "Gets account information from, and makes payments using" - }, - { - "id": "7", - "tags": "Relationship", - "sourceId": "2", - "destinationId": "6", - "description": "Sends e-mail using" - } - ], - "location": "Internal", - "containers": [ - { - "id": "20", - "tags": "Element,Container", - "name": "API Application", - "description": "Provides Internet banking functionality via a JSON/HTTPS API.", - "relationships": [ - { - "id": "26", - "tags": "Relationship", - "sourceId": "20", - "destinationId": "21", - "description": "Reads from and writes to", - "technology": "JDBC" - }, - { - "id": "27", - "tags": "Relationship", - "sourceId": "20", - "destinationId": "4", - "description": "Makes API calls to", - "technology": "XML/HTTPS" - }, - { - "id": "28", - "tags": "Relationship", - "sourceId": "20", - "destinationId": "6", - "description": "Sends e-mail using", - "technology": "SMTP" - } - ], - "technology": "Java and Spring MVC", - "components": [ - { - "id": "30", - "tags": "Element,Component", - "name": "Accounts Summary Controller", - "description": "Provides customers with a summary of their bank accounts.", - "relationships": [ - { - "id": "44", - "tags": "Relationship", - "sourceId": "30", - "destinationId": "33", - "description": "Uses" - } - ], - "technology": "Spring MVC Rest Controller", - "size": 0 - }, - { - "id": "34", - "tags": "Element,Component", - "name": "E-mail Component", - "description": "Sends e-mails to users.", - "relationships": [ - { - "id": "49", - "tags": "Relationship", - "sourceId": "34", - "destinationId": "6", - "description": "Sends e-mail using" - } - ], - "technology": "Spring Bean", - "size": 0 - }, - { - "id": "33", - "tags": "Element,Component", - "name": "Mainframe Banking System Facade", - "description": "A facade onto the mainframe banking system.", - "relationships": [ - { - "id": "48", - "tags": "Relationship", - "sourceId": "33", - "destinationId": "4", - "description": "Uses", - "technology": "XML/HTTPS" - } - ], - "technology": "Spring Bean", - "size": 0 - }, - { - "id": "31", - "tags": "Element,Component", - "name": "Reset Password Controller", - "description": "Allows users to reset their passwords with a single use URL.", - "relationships": [ - { - "id": "45", - "tags": "Relationship", - "sourceId": "31", - "destinationId": "32", - "description": "Uses" - }, - { - "id": "46", - "tags": "Relationship", - "sourceId": "31", - "destinationId": "34", - "description": "Uses" - } - ], - "technology": "Spring MVC Rest Controller", - "size": 0 - }, - { - "id": "32", - "tags": "Element,Component", - "name": "Security Component", - "description": "Provides functionality related to signing in, changing passwords, etc.", - "relationships": [ - { - "id": "47", - "tags": "Relationship", - "sourceId": "32", - "destinationId": "21", - "description": "Reads from and writes to", - "technology": "JDBC" - } - ], - "technology": "Spring Bean", - "size": 0 - }, - { - "id": "29", - "tags": "Element,Component", - "name": "Sign In Controller", - "description": "Allows users to sign in to the Internet Banking System.", - "relationships": [ - { - "id": "43", - "tags": "Relationship", - "sourceId": "29", - "destinationId": "32", - "description": "Uses" - } - ], - "technology": "Spring MVC Rest Controller", - "size": 0 - } - ] - }, - { - "id": "21", - "tags": "Element,Container,Database", - "name": "Database", - "description": "Stores user registration information, hashed authentication credentials, access logs, etc.", - "technology": "Oracle Database Schema" - }, - { - "id": "18", - "tags": "Element,Container,Mobile App", - "name": "Mobile App", - "description": "Provides a limited subset of the Internet banking functionality to customers via their mobile device.", - "relationships": [ - { - "id": "39", - "tags": "Relationship", - "sourceId": "18", - "destinationId": "29", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - }, - { - "id": "41", - "tags": "Relationship", - "sourceId": "18", - "destinationId": "31", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - }, - { - "id": "42", - "tags": "Relationship", - "sourceId": "18", - "destinationId": "30", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - }, - { - "id": "40", - "tags": "Relationship", - "sourceId": "18", - "destinationId": "20", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - } - ], - "technology": "Xamarin" - }, - { - "id": "17", - "tags": "Element,Container,Web Browser", - "name": "Single-Page Application", - "description": "Provides all of the Internet banking functionality to customers via their web browser.", - "relationships": [ - { - "id": "38", - "tags": "Relationship", - "sourceId": "17", - "destinationId": "30", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - }, - { - "id": "36", - "tags": "Relationship", - "sourceId": "17", - "destinationId": "20", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - }, - { - "id": "37", - "tags": "Relationship", - "sourceId": "17", - "destinationId": "31", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - }, - { - "id": "35", - "tags": "Relationship", - "sourceId": "17", - "destinationId": "29", - "description": "Makes API calls to", - "technology": "JSON/HTTPS" - } - ], - "technology": "JavaScript and Angular" - }, - { - "id": "19", - "tags": "Element,Container", - "name": "Web Application", - "description": "Delivers the static content and the Internet banking single page application.", - "relationships": [ - { - "id": "25", - "tags": "Relationship", - "sourceId": "19", - "destinationId": "17", - "description": "Delivers to the customer's web browser" - } - ], - "technology": "Java and Spring MVC" - } - ] + "sourceId" : "15", + "destinationId" : "18", + "description" : "Reads from and writes to", + "technology" : "SQL/TCP" + } ], + "technology" : "Spring Bean", + "documentation" : { } + }, { + "id" : "14", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "resetpasswordcontroller" + }, + "name" : "Reset Password Controller", + "description" : "Allows users to reset their passwords with a single use URL.", + "relationships" : [ { + "id" : "42", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "bd04aed2-60d9-45f9-a75a-bd570b5a9cc4" }, - { - "id": "4", - "tags": "Element,Software System,Existing System", - "name": "Mainframe Banking System", - "description": "Stores all of the core banking information about customers, accounts, transactions, etc.", - "location": "Internal" - } - ], - "deploymentNodes": [ - { - "id": "55", - "tags": "Element,Deployment Node", - "name": "Big Bank plc", - "environment": "Development", - "technology": "Big Bank plc data center", - "instances": 1, - "children": [ - { - "id": "56", - "tags": "Element,Deployment Node", - "name": "bigbank-dev001", - "environment": "Development", - "instances": 1, - "softwareSystemInstances": [ - { - "id": "57", - "tags": "Software System Instance", - "environment": "Development", - "instanceId": 1, - "softwareSystemId": "4", - "properties": {} - } - ], - "children": [], - "containerInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] + "sourceId" : "14", + "destinationId" : "15", + "description" : "Uses" + }, { + "id" : "43", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "aa43b36b-ddc2-4aad-a021-d54f79abd36b" }, - { - "id": "50", - "tags": "Element,Deployment Node", - "name": "Developer Laptop", - "description": "A developer laptop.", - "environment": "Development", - "technology": "Microsoft Windows 10 or Apple macOS", - "instances": 1, - "children": [ - { - "id": "59", - "tags": "Element,Deployment Node", - "name": "Docker Container - Database Server", - "description": "A Docker container.", - "environment": "Development", - "technology": "Docker", - "instances": 1, - "children": [ - { - "id": "60", - "tags": "Element,Deployment Node", - "name": "Database Server", - "description": "A development database.", - "environment": "Development", - "technology": "Oracle 12c", - "instances": 1, - "containerInstances": [ - { - "id": "61", - "tags": "Container Instance", - "environment": "Development", - "instanceId": 1, - "containerId": "21", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - }, - { - "id": "51", - "tags": "Element,Deployment Node", - "name": "Docker Container - Web Server", - "description": "A Docker container.", - "environment": "Development", - "technology": "Docker", - "instances": 1, - "children": [ - { - "id": "52", - "tags": "Element,Deployment Node", - "properties": { - "Java Version": "8", - "Xms": "1024M", - "Xmx": "512M" - }, - "name": "Apache Tomcat", - "description": "An open source Java EE web server.", - "environment": "Development", - "technology": "Apache Tomcat 8.x", - "instances": 1, - "containerInstances": [ - { - "id": "54", - "tags": "Container Instance", - "relationships": [ - { - "id": "62", - "sourceId": "54", - "destinationId": "61", - "description": "Reads from and writes to", - "technology": "JDBC", - "linkedRelationshipId": "26" - }, - { - "id": "58", - "sourceId": "54", - "destinationId": "57", - "description": "Makes API calls to", - "technology": "XML/HTTPS", - "linkedRelationshipId": "27" - } - ], - "environment": "Development", - "instanceId": 1, - "containerId": "20", - "properties": {} - }, - { - "id": "53", - "tags": "Container Instance", - "relationships": [ - { - "id": "66", - "sourceId": "53", - "destinationId": "64", - "description": "Delivers to the customer's web browser", - "linkedRelationshipId": "25" - } - ], - "environment": "Development", - "instanceId": 1, - "containerId": "19", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - }, - { - "id": "63", - "tags": "Element,Deployment Node", - "name": "Web Browser", - "environment": "Development", - "technology": "Chrome, Firefox, Safari, or Edge", - "instances": 1, - "containerInstances": [ - { - "id": "64", - "tags": "Container Instance", - "relationships": [ - { - "id": "65", - "sourceId": "64", - "destinationId": "54", - "description": "Makes API calls to", - "technology": "JSON/HTTPS", - "linkedRelationshipId": "36" - } - ], - "environment": "Development", - "instanceId": 1, - "containerId": "17", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] + "sourceId" : "14", + "destinationId" : "17", + "description" : "Uses" + } ], + "technology" : "Spring MVC Rest Controller", + "documentation" : { } + }, { + "id" : "17", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "emailcomponent" + }, + "name" : "E-mail Component", + "description" : "Sends e-mails to users.", + "relationships" : [ { + "id" : "48", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "3c022388-9eb7-4e47-b524-f480fc361897" }, - { - "id": "72", - "tags": "Element,Deployment Node", - "name": "Big Bank plc", - "environment": "Live", - "technology": "Big Bank plc data center", - "instances": 1, - "children": [ - { - "id": "79", - "tags": "Element,Deployment Node", - "properties": { - "Location": "London and Reading" - }, - "name": "bigbank-api***", - "description": "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", - "environment": "Live", - "technology": "Ubuntu 16.04 LTS", - "instances": 8, - "children": [ - { - "id": "80", - "tags": "Element,Deployment Node", - "properties": { - "Java Version": "8", - "Xms": "1024M", - "Xmx": "512M" - }, - "name": "Apache Tomcat", - "description": "An open source Java EE web server.", - "environment": "Live", - "technology": "Apache Tomcat 8.x", - "instances": 1, - "containerInstances": [ - { - "id": "81", - "tags": "Container Instance", - "relationships": [ - { - "id": "88", - "sourceId": "81", - "destinationId": "87", - "description": "Reads from and writes to", - "technology": "JDBC", - "linkedRelationshipId": "26" - }, - { - "id": "84", - "sourceId": "81", - "destinationId": "74", - "description": "Makes API calls to", - "technology": "XML/HTTPS", - "linkedRelationshipId": "27" - }, - { - "id": "92", - "tags": "Failover", - "sourceId": "81", - "destinationId": "91", - "description": "Reads from and writes to", - "technology": "JDBC", - "linkedRelationshipId": "26" - } - ], - "environment": "Live", - "instanceId": 1, - "containerId": "20", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - }, - { - "id": "85", - "tags": "Element,Deployment Node", - "properties": { - "Location": "London" - }, - "name": "bigbank-db01", - "description": "The primary database server.", - "environment": "Live", - "technology": "Ubuntu 16.04 LTS", - "instances": 1, - "children": [ - { - "id": "86", - "tags": "Element,Deployment Node", - "name": "Oracle - Primary", - "description": "The primary, live database server.", - "relationships": [ - { - "id": "93", - "tags": "Relationship,Synchronous", - "sourceId": "86", - "destinationId": "90", - "description": "Replicates data to", - "interactionStyle": "Synchronous" - } - ], - "environment": "Live", - "technology": "Oracle 12c", - "instances": 1, - "containerInstances": [ - { - "id": "87", - "tags": "Container Instance", - "environment": "Live", - "instanceId": 1, - "containerId": "21", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - }, - { - "id": "89", - "tags": "Element,Deployment Node,Failover", - "properties": { - "Location": "Reading" - }, - "name": "bigbank-db02", - "description": "The secondary database server.", - "environment": "Live", - "technology": "Ubuntu 16.04 LTS", - "instances": 1, - "children": [ - { - "id": "90", - "tags": "Element,Deployment Node,Failover", - "name": "Oracle - Secondary", - "description": "A secondary, standby database server, used for failover purposes only.", - "environment": "Live", - "technology": "Oracle 12c", - "instances": 1, - "containerInstances": [ - { - "id": "91", - "tags": "Container Instance,Failover", - "environment": "Live", - "instanceId": 2, - "containerId": "21", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - }, - { - "id": "73", - "tags": "Element,Deployment Node", - "name": "bigbank-prod001", - "environment": "Live", - "instances": 1, - "softwareSystemInstances": [ - { - "id": "74", - "tags": "Software System Instance", - "environment": "Live", - "instanceId": 1, - "softwareSystemId": "4", - "properties": {} - } - ], - "children": [], - "containerInstances": [], - "infrastructureNodes": [] - }, - { - "id": "75", - "tags": "Element,Deployment Node", - "properties": { - "Location": "London and Reading" - }, - "name": "bigbank-web***", - "description": "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", - "environment": "Live", - "technology": "Ubuntu 16.04 LTS", - "instances": 4, - "children": [ - { - "id": "76", - "tags": "Element,Deployment Node", - "properties": { - "Java Version": "8", - "Xms": "1024M", - "Xmx": "512M" - }, - "name": "Apache Tomcat", - "description": "An open source Java EE web server.", - "environment": "Live", - "technology": "Apache Tomcat 8.x", - "instances": 1, - "containerInstances": [ - { - "id": "77", - "tags": "Container Instance", - "relationships": [ - { - "id": "78", - "sourceId": "77", - "destinationId": "71", - "description": "Delivers to the customer's web browser", - "linkedRelationshipId": "25" - } - ], - "environment": "Live", - "instanceId": 1, - "containerId": "19", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] + "sourceId" : "17", + "destinationId" : "5", + "description" : "Sends e-mail using" + } ], + "technology" : "Spring Bean", + "documentation" : { } + } ], + "documentation" : { } + }, { + "id" : "8", + "tags" : "Element,Container,Web Browser", + "properties" : { + "structurizr.dsl.identifier" : "singlepageapplication" + }, + "name" : "Single-Page Application", + "description" : "Provides all of the Internet banking functionality to customers via their web browser.", + "relationships" : [ { + "id" : "32", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "adb4007f-e5e8-41b9-ac61-d027fac89cdb" + }, + "sourceId" : "8", + "destinationId" : "12", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS" + }, { + "id" : "33", + "sourceId" : "8", + "destinationId" : "11", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS", + "linkedRelationshipId" : "32" + }, { + "id" : "34", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "235c5bc1-8163-4415-abb2-9f24d7a03155" + }, + "sourceId" : "8", + "destinationId" : "13", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS" + }, { + "id" : "35", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "607c5ef3-186c-4f68-a4f1-3be26eb4591f" + }, + "sourceId" : "8", + "destinationId" : "14", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS" + } ], + "technology" : "JavaScript and Angular", + "documentation" : { } + } ], + "documentation" : { } + } ], + "deploymentNodes" : [ { + "id" : "50", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "25239481-454a-4edd-bca5-7ee77ced54e1" + }, + "name" : "Developer Laptop", + "environment" : "Development", + "technology" : "Microsoft Windows 10 or Apple macOS", + "instances" : "1", + "children" : [ { + "id" : "53", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "e8ce8a91-df97-4486-b2a0-dcd171cd2ee3" + }, + "name" : "Docker Container - Web Server", + "environment" : "Development", + "technology" : "Docker", + "instances" : "1", + "children" : [ { + "id" : "54", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "af91db02-1a49-4a73-848a-ff230caf459b" + }, + "name" : "Apache Tomcat", + "environment" : "Development", + "technology" : "Apache Tomcat 8.x", + "instances" : "1", + "containerInstances" : [ { + "id" : "55", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "developerwebapplicationinstance" }, - { - "id": "69", - "tags": "Element,Deployment Node", - "name": "Customer's computer", - "environment": "Live", - "technology": "Microsoft Windows or Apple macOS", - "instances": 1, - "children": [ - { - "id": "70", - "tags": "Element,Deployment Node", - "name": "Web Browser", - "environment": "Live", - "technology": "Chrome, Firefox, Safari, or Edge", - "instances": 1, - "containerInstances": [ - { - "id": "71", - "tags": "Container Instance", - "relationships": [ - { - "id": "83", - "sourceId": "71", - "destinationId": "81", - "description": "Makes API calls to", - "technology": "JSON/HTTPS", - "linkedRelationshipId": "36" - } - ], - "environment": "Live", - "instanceId": 1, - "containerId": "17", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] + "relationships" : [ { + "id" : "56", + "sourceId" : "55", + "destinationId" : "52", + "description" : "Delivers to the customer's web browser", + "linkedRelationshipId" : "31" + } ], + "environment" : "Development", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "10" + }, { + "id" : "57", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "developerapiapplicationinstance" }, - { - "id": "67", - "tags": "Element,Deployment Node", - "name": "Customer's mobile device", - "environment": "Live", - "technology": "Apple iOS or Android", - "instances": 1, - "containerInstances": [ - { - "id": "68", - "tags": "Container Instance", - "relationships": [ - { - "id": "82", - "sourceId": "68", - "destinationId": "81", - "description": "Makes API calls to", - "technology": "JSON/HTTPS", - "linkedRelationshipId": "40" - } - ], - "environment": "Live", - "instanceId": 1, - "containerId": "18", - "properties": {} - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ] - }, - "documentation": { - "sections": [ - { - "elementId": "2", - "title": "Context", - "order": 1, - "format": "Markdown", - "content": "Here is some context about the Internet Banking System...\n![](embed:SystemLandscape)\n![](embed:SystemContext)\n### Internet Banking System\n...\n### Mainframe Banking System\n...\n" + "relationships" : [ { + "id" : "62", + "sourceId" : "57", + "destinationId" : "61", + "description" : "Reads from and writes to", + "technology" : "SQL/TCP", + "linkedRelationshipId" : "45" + }, { + "id" : "66", + "sourceId" : "57", + "destinationId" : "65", + "description" : "Makes API calls to", + "technology" : "XML/HTTPS", + "linkedRelationshipId" : "47" + } ], + "environment" : "Development", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "11" + } ] + } ] + }, { + "id" : "51", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "b7da5a3d-2f5f-41f4-ab17-a81118c842ad" + }, + "name" : "Web Browser", + "environment" : "Development", + "technology" : "Chrome, Firefox, Safari, or Edge", + "instances" : "1", + "containerInstances" : [ { + "id" : "52", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "developersinglepageapplicationinstance" + }, + "relationships" : [ { + "id" : "58", + "sourceId" : "52", + "destinationId" : "57", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS", + "linkedRelationshipId" : "33" + } ], + "environment" : "Development", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "8" + } ] + }, { + "id" : "59", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "5f95573e-5028-40e6-9b55-b1402dc184b4" + }, + "name" : "Docker Container - Database Server", + "environment" : "Development", + "technology" : "Docker", + "instances" : "1", + "children" : [ { + "id" : "60", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "562a146b-9830-4fef-ab67-cadbede1cabb" + }, + "name" : "Database Server", + "environment" : "Development", + "technology" : "Oracle 12c", + "instances" : "1", + "containerInstances" : [ { + "id" : "61", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "developerdatabaseinstance" }, - { - "elementId": "19", - "title": "Components", - "order": 3, - "format": "Markdown", - "content": "Here is some information about the API Application...\n![](embed:Components)\n### Sign in process\nHere is some information about the Sign In Controller, including how the sign in process works...\n![](embed:SignIn)" + "environment" : "Development", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "18" + } ] + } ] + } ] + }, { + "id" : "63", + "tags" : "Element,Deployment Node,", + "properties" : { + "structurizr.dsl.identifier" : "77f85941-3884-4918-bfb5-a55a6d2d7448" + }, + "name" : "Big Bank plc", + "environment" : "Development", + "technology" : "Big Bank plc data center", + "instances" : "1", + "children" : [ { + "id" : "64", + "tags" : "Element,Deployment Node,", + "properties" : { + "structurizr.dsl.identifier" : "9e854be8-32f3-4d69-88fd-245871775b15" + }, + "name" : "bigbank-dev001", + "environment" : "Development", + "instances" : "1", + "softwareSystemInstances" : [ { + "id" : "65", + "tags" : "Software System Instance", + "properties" : { + "structurizr.dsl.identifier" : "437ac7c3-ebbb-44ce-8a60-80bef8b6fb6e" + }, + "environment" : "Development", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "softwareSystemId" : "4" + } ] + } ] + }, { + "id" : "67", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "80be1ff8-fe6c-413b-aac6-9bb6cb9b85c7" + }, + "name" : "Customer's mobile device", + "environment" : "Live", + "technology" : "Apple iOS or Android", + "instances" : "1", + "containerInstances" : [ { + "id" : "68", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "livemobileappinstance" + }, + "relationships" : [ { + "id" : "80", + "sourceId" : "68", + "destinationId" : "79", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS", + "linkedRelationshipId" : "37" + } ], + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "9" + } ] + }, { + "id" : "69", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "493075d0-96ce-4274-98e2-5327fc48aeb6" + }, + "name" : "Customer's computer", + "environment" : "Live", + "technology" : "Microsoft Windows or Apple macOS", + "instances" : "1", + "children" : [ { + "id" : "70", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "45b6f442-c8bf-4923-8270-b9325afe0581" + }, + "name" : "Web Browser", + "environment" : "Live", + "technology" : "Chrome, Firefox, Safari, or Edge", + "instances" : "1", + "containerInstances" : [ { + "id" : "71", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "livesinglepageapplicationinstance" + }, + "relationships" : [ { + "id" : "81", + "sourceId" : "71", + "destinationId" : "79", + "description" : "Makes API calls to", + "technology" : "JSON/HTTPS", + "linkedRelationshipId" : "33" + } ], + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "8" + } ] + } ] + }, { + "id" : "72", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "e7539131-ef26-4370-beda-31449e4c6b07" + }, + "name" : "Big Bank plc", + "environment" : "Live", + "technology" : "Big Bank plc data center", + "instances" : "1", + "children" : [ { + "id" : "86", + "tags" : "Element,Deployment Node,Failover", + "properties" : { + "structurizr.dsl.identifier" : "edeb7dc9-7738-4516-9c81-c879369b2ab5" + }, + "name" : "bigbank-db02", + "environment" : "Live", + "technology" : "Ubuntu 16.04 LTS", + "instances" : "1", + "children" : [ { + "id" : "87", + "tags" : "Element,Deployment Node,Failover", + "properties" : { + "structurizr.dsl.identifier" : "secondarydatabaseserver" + }, + "name" : "Oracle - Secondary", + "environment" : "Live", + "technology" : "Oracle 12c", + "instances" : "1", + "containerInstances" : [ { + "id" : "88", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "livesecondarydatabaseinstance" }, - { - "elementId": "2", - "title": "Development Environment", - "order": 4, - "format": "AsciiDoc", - "content": "Here is some information about how to set up a development environment for the Internet Banking System...\nimage::embed:DevelopmentDeployment[]" + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "18" + } ] + } ] + }, { + "id" : "82", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "7ec5cdd8-2cfe-46f6-8298-575bf81c2463" + }, + "name" : "bigbank-db01", + "environment" : "Live", + "technology" : "Ubuntu 16.04 LTS", + "instances" : "1", + "children" : [ { + "id" : "83", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "primarydatabaseserver" + }, + "name" : "Oracle - Primary", + "relationships" : [ { + "id" : "93", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "982513db-e0af-4cde-a468-b98ff002d363" }, - { - "elementId": "2", - "title": "Containers", - "order": 2, - "format": "Markdown", - "content": "Here is some information about the containers within the Internet Banking System...\n![](embed:Containers)\n### Web Application\n...\n### Database\n...\n" + "sourceId" : "83", + "destinationId" : "87", + "description" : "Replicates data to" + } ], + "environment" : "Live", + "technology" : "Oracle 12c", + "instances" : "1", + "containerInstances" : [ { + "id" : "84", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "liveprimarydatabaseinstance" }, - { - "elementId": "2", - "title": "Deployment", - "order": 5, - "format": "AsciiDoc", - "content": "Here is some information about the live deployment environment for the Internet Banking System...\nimage::embed:LiveDeployment[]" - } - ], - "template": { - "name": "Software Guidebook", - "author": "Simon Brown", - "url": "https://leanpub.com/visualising-software-architecture" + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "18" + } ] + } ] + }, { + "id" : "77", + "tags" : "Element,Deployment Node,", + "properties" : { + "structurizr.dsl.identifier" : "291edaeb-88f1-4c8e-9a26-54d0c113b1ae" }, - "decisions": [], - "images": [] - }, - "views": { - "systemLandscapeViews": [ - { - "description": "The system landscape diagram for Big Bank plc.", - "key": "SystemLandscape", - "paperSize": "A5_Landscape", - "animations": [ - { - "order": 1, - "elements": [ - "1", - "2", - "4", - "6" - ], - "relationships": [ - "3", - "5", - "7", - "8" - ] - }, - { - "order": 2, - "elements": [ - "9" - ], - "relationships": [ - "11", - "10" - ] - }, - { - "order": 3, - "elements": [ - "12", - "15" - ], - "relationships": [ - "13", - "14", - "16" - ] - } - ], - "enterpriseBoundaryVisible": true, - "elements": [ - { - "id": "1", - "x": 87, - "y": 643 - }, - { - "id": "12", - "x": 1947, - "y": 36 - }, - { - "id": "2", - "x": 1012, - "y": 813 - }, - { - "id": "4", - "x": 1922, - "y": 693 - }, - { - "id": "15", - "x": 1947, - "y": 1241 - }, - { - "id": "6", - "x": 1012, - "y": 1326 - }, - { - "id": "9", - "x": 1012, - "y": 301 - } - ], - "relationships": [ - { - "id": "16" - }, - { - "id": "3" - }, - { - "id": "14", - "vertices": [ - { - "x": 285, - "y": 240 - } - ] - }, - { - "id": "5" - }, - { - "id": "13" - }, - { - "id": "11" - }, - { - "id": "7" - }, - { - "id": "8" - }, - { - "id": "10" - } - ] - } - ], - "systemContextViews": [ - { - "softwareSystemId": "2", - "description": "The system context diagram for the Internet Banking System.", - "key": "SystemContext", - "paperSize": "A5_Landscape", - "animations": [ - { - "order": 1, - "elements": [ - "2" - ], - "relationships": [] - }, - { - "order": 2, - "elements": [ - "1" - ], - "relationships": [ - "3" - ] - }, - { - "order": 3, - "elements": [ - "4" - ], - "relationships": [ - "5" - ] - }, - { - "order": 4, - "elements": [ - "6" - ], - "relationships": [ - "7", - "8" - ] - } - ], - "enterpriseBoundaryVisible": false, - "elements": [ - { - "id": "1", - "x": 632, - "y": 69 - }, - { - "id": "2", - "x": 607, - "y": 714 - }, - { - "id": "4", - "x": 607, - "y": 1259 - }, - { - "id": "6", - "x": 1422, - "y": 714 - } - ], - "relationships": [ - { - "id": "3" - }, - { - "id": "5" - }, - { - "id": "7" - }, - { - "id": "8" - } - ] - } - ], - "containerViews": [ - { - "softwareSystemId": "2", - "description": "The container diagram for the Internet Banking System.", - "key": "Containers", - "paperSize": "A5_Landscape", - "animations": [ - { - "order": 1, - "elements": [ - "1", - "4", - "6" - ], - "relationships": [ - "8" - ] - }, - { - "order": 2, - "elements": [ - "19" - ], - "relationships": [ - "22" - ] - }, - { - "order": 3, - "elements": [ - "17" - ], - "relationships": [ - "23", - "25" - ] - }, - { - "order": 4, - "elements": [ - "18" - ], - "relationships": [ - "24" - ] - }, - { - "order": 5, - "elements": [ - "20" - ], - "relationships": [ - "36", - "27", - "28", - "40" - ] - }, - { - "order": 6, - "elements": [ - "21" - ], - "relationships": [ - "26" - ] - } - ], - "externalSoftwareSystemBoundariesVisible": false, - "elements": [ - { - "id": "1", - "x": 1056, - "y": 24 - }, - { - "id": "4", - "x": 2012, - "y": 1214 - }, - { - "id": "17", - "x": 780, - "y": 664 - }, - { - "id": "6", - "x": 2012, - "y": 664 - }, - { - "id": "18", - "x": 1283, - "y": 664 - }, - { - "id": "19", - "x": 37, - "y": 664 - }, - { - "id": "20", - "x": 1031, - "y": 1214 - }, - { - "id": "21", - "x": 37, - "y": 1214 - } - ], - "relationships": [ - { - "id": "28" - }, - { - "id": "27" - }, - { - "id": "26" - }, - { - "id": "25" - }, - { - "id": "36" - }, - { - "id": "40" - }, - { - "id": "24" - }, - { - "id": "23" - }, - { - "id": "22" - }, - { - "id": "8" - } - ] - } - ], - "componentViews": [ - { - "description": "The component diagram for the API Application.", - "key": "Components", - "paperSize": "A5_Landscape", - "animations": [ - { - "order": 1, - "elements": [ - "4", - "17", - "6", - "18", - "21" - ], - "relationships": [] - }, - { - "order": 2, - "elements": [ - "29", - "32" - ], - "relationships": [ - "35", - "47", - "39", - "43" - ] - }, - { - "order": 3, - "elements": [ - "33", - "30" - ], - "relationships": [ - "44", - "48", - "38", - "42" - ] - }, - { - "order": 4, - "elements": [ - "34", - "31" - ], - "relationships": [ - "45", - "46", - "37", - "49", - "41" - ] - } - ], - "containerId": "20", - "externalContainerBoundariesVisible": false, - "elements": [ - { - "id": "33", - "x": 1925, - "y": 817 - }, - { - "id": "34", - "x": 1015, - "y": 817 - }, - { - "id": "4", - "x": 1925, - "y": 1307 - }, - { - "id": "17", - "x": 560, - "y": 10 - }, - { - "id": "6", - "x": 1015, - "y": 1307 - }, - { - "id": "18", - "x": 1470, - "y": 11 - }, - { - "id": "29", - "x": 105, - "y": 436 - }, - { - "id": "30", - "x": 1925, - "y": 436 - }, - { - "id": "31", - "x": 1015, - "y": 436 - }, - { - "id": "21", - "x": 105, - "y": 1307 - }, - { - "id": "32", - "x": 105, - "y": 817 - } - ], - "relationships": [ - { - "id": "41", - "position": 40 - }, - { - "id": "42", - "position": 40 - }, - { - "id": "43", - "position": 55 - }, - { - "id": "37", - "position": 45 - }, - { - "id": "35", - "position": 35 - }, - { - "id": "44", - "position": 50 - }, - { - "id": "45" - }, - { - "id": "46" - }, - { - "id": "47", - "position": 60 - }, - { - "id": "48" - }, - { - "id": "38", - "position": 85 - }, - { - "id": "49" - }, - { - "id": "39", - "position": 85 - } - ] - } - ], - "dynamicViews": [ - { - "description": "Summarises how the sign in feature works in the single-page application.", - "key": "SignIn", - "paperSize": "A5_Landscape", - "elementId": "20", - "relationships": [ - { - "id": "35", - "description": "Submits credentials to", - "order": "1", - "response": false, - "vertices": [ - { - "x": 1238, - "y": 236 - } - ], - "routing": "Curved", - "position": 50 - }, - { - "id": "43", - "description": "Validates credentials using", - "order": "2", - "response": false, - "vertices": [ - { - "x": 2065, - "y": 845 - } - ], - "routing": "Curved" - }, - { - "id": "47", - "description": "select * from users where username = ?", - "order": "3", - "response": false, - "vertices": [ - { - "x": 1218, - "y": 1416 - } - ], - "routing": "Curved" - }, - { - "id": "47", - "description": "Returns user data to", - "order": "4", - "response": true, - "vertices": [ - { - "x": 1240, - "y": 1220 - } - ], - "routing": "Curved" - }, - { - "id": "43", - "description": "Returns true if the hashed password matches", - "order": "5", - "response": true, - "vertices": [ - { - "x": 1828, - "y": 841 - } - ], - "routing": "Curved" - }, - { - "id": "35", - "description": "Sends back an authentication token to", - "order": "6", - "response": true, - "vertices": [ - { - "x": 1210, - "y": 450 - } - ], - "routing": "Curved" - } - ], - "elements": [ - { - "id": "17", - "x": 290, - "y": 192 - }, - { - "id": "29", - "x": 1720, - "y": 192 - }, - { - "id": "32", - "x": 1720, - "y": 1182 - }, - { - "id": "21", - "x": 290, - "y": 1182 - } - ] - } - ], - "deploymentViews": [ - { - "softwareSystemId": "2", - "description": "An example live deployment scenario for the Internet Banking System.", - "key": "LiveDeployment", - "paperSize": "A4_Landscape", - "environment": "Live", - "animations": [ - { - "order": 1, - "elements": [ - "69", - "70", - "71" - ] - }, - { - "order": 2, - "elements": [ - "67", - "68" - ] - }, - { - "order": 3, - "elements": [ - "77", - "79", - "80", - "81", - "72", - "75", - "76" - ], - "relationships": [ - "78", - "82", - "83" - ] - }, - { - "order": 4, - "elements": [ - "85", - "86", - "87" - ], - "relationships": [ - "88" - ] - }, - { - "order": 5, - "elements": [ - "89", - "90", - "91" - ], - "relationships": [ - "92", - "93" - ] - }, - { - "order": 6, - "elements": [ - "74" - ], - "relationships": [ - "84" - ] - } - ], - "elements": [ - { - "id": "77", - "x": 1504, - "y": 184 - }, - { - "id": "89", - "x": 0, - "y": 0 - }, - { - "id": "67", - "x": 0, - "y": 0 - }, - { - "id": "79", - "x": 0, - "y": 0 - }, - { - "id": "68", - "x": 424, - "y": 1071 - }, - { - "id": "69", - "x": 0, - "y": 0 - }, - { - "id": "90", - "x": 0, - "y": 0 - }, - { - "id": "91", - "x": 2584, - "y": 184 - }, - { - "id": "80", - "x": 0, - "y": 0 - }, - { - "id": "81", - "x": 1504, - "y": 1071 - }, - { - "id": "70", - "x": 0, - "y": 0 - }, - { - "id": "71", - "x": 424, - "y": 184 - }, - { - "id": "72", - "x": 0, - "y": 0 - }, - { - "id": "73", - "x": 0, - "y": 0 - }, - { - "id": "74", - "x": 2584, - "y": 1959 - }, - { - "id": "85", - "x": 0, - "y": 0 - }, - { - "id": "86", - "x": 0, - "y": 0 - }, - { - "id": "75", - "x": 0, - "y": 0 - }, - { - "id": "87", - "x": 2584, - "y": 1071 - }, - { - "id": "76", - "x": 0, - "y": 0 - } - ], - "relationships": [ - { - "id": "93" - }, - { - "id": "82" - }, - { - "id": "83" - }, - { - "id": "92" - }, - { - "id": "84" - }, - { - "id": "78" - }, - { - "id": "88" - } - ] + "name" : "bigbank-api***", + "environment" : "Live", + "technology" : "Ubuntu 16.04 LTS", + "instances" : "8", + "children" : [ { + "id" : "78", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "a89ebe72-8ae6-43bf-8708-cc142a531f6b" + }, + "name" : "Apache Tomcat", + "environment" : "Live", + "technology" : "Apache Tomcat 8.x", + "instances" : "1", + "containerInstances" : [ { + "id" : "79", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "liveapiapplicationinstance" }, - { - "softwareSystemId": "2", - "description": "An example development deployment scenario for the Internet Banking System.", - "key": "DevelopmentDeployment", - "paperSize": "A5_Landscape", - "environment": "Development", - "animations": [ - { - "order": 1, - "elements": [ - "50", - "63", - "64" - ] - }, - { - "order": 2, - "elements": [ - "51", - "52", - "53", - "54" - ], - "relationships": [ - "66", - "65" - ] - }, - { - "order": 3, - "elements": [ - "59", - "60", - "61" - ], - "relationships": [ - "62" - ] - }, - { - "order": 4, - "elements": [ - "57" - ], - "relationships": [ - "58" - ] - } - ], - "elements": [ - { - "id": "55", - "x": 0, - "y": 0 - }, - { - "id": "56", - "x": 0, - "y": 0 - }, - { - "id": "57", - "x": 1827, - "y": 1236 - }, - { - "id": "59", - "x": 0, - "y": 0 - }, - { - "id": "60", - "x": 0, - "y": 0 - }, - { - "id": "61", - "x": 1827, - "y": 176 - }, - { - "id": "50", - "x": 0, - "y": 0 - }, - { - "id": "51", - "x": 0, - "y": 0 - }, - { - "id": "63", - "x": 0, - "y": 0 - }, - { - "id": "52", - "x": 0, - "y": 0 - }, - { - "id": "64", - "x": 152, - "y": 346 - }, - { - "id": "53", - "x": 989, - "y": 176 - }, - { - "id": "54", - "x": 989, - "y": 516 - } - ], - "relationships": [ - { - "id": "62", - "position": 50 - }, - { - "id": "65" - }, - { - "id": "66" - }, - { - "id": "58" - } - ] - } - ], - "configuration": { - "branding": {}, - "styles": { - "elements": [ - { - "tag": "Software System", - "background": "#1168bd", - "color": "#ffffff" - }, - { - "tag": "Container", - "background": "#438dd5", - "color": "#ffffff" - }, - { - "tag": "Component", - "background": "#85bbf0", - "color": "#000000" - }, - { - "tag": "Person", - "background": "#08427b", - "color": "#ffffff", - "fontSize": 22, - "shape": "Person" - }, - { - "tag": "Existing System", - "background": "#999999", - "color": "#ffffff" - }, - { - "tag": "Bank Staff", - "background": "#999999", - "color": "#ffffff" - }, - { - "tag": "Web Browser", - "shape": "WebBrowser" - }, - { - "tag": "Mobile App", - "shape": "MobileDeviceLandscape" - }, - { - "tag": "Database", - "shape": "Cylinder" - }, - { - "tag": "Failover", - "opacity": 25 - } - ], - "relationships": [ - { - "tag": "Failover", - "position": 70, - "opacity": 25 - } - ] - }, - "terminology": {}, - "lastSavedView": "SignIn", - "themes": [] + "relationships" : [ { + "id" : "85", + "sourceId" : "79", + "destinationId" : "84", + "description" : "Reads from and writes to", + "technology" : "SQL/TCP", + "linkedRelationshipId" : "45" + }, { + "id" : "89", + "sourceId" : "79", + "destinationId" : "88", + "description" : "Reads from and writes to", + "technology" : "SQL/TCP", + "linkedRelationshipId" : "45" + }, { + "id" : "92", + "sourceId" : "79", + "destinationId" : "91", + "description" : "Makes API calls to", + "technology" : "XML/HTTPS", + "linkedRelationshipId" : "47" + } ], + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "11" + } ] + } ] + }, { + "id" : "90", + "tags" : "Element,Deployment Node,", + "properties" : { + "structurizr.dsl.identifier" : "cdb15b14-58b4-4ca2-aa56-a162198908c8" + }, + "name" : "bigbank-prod001", + "environment" : "Live", + "instances" : "1", + "softwareSystemInstances" : [ { + "id" : "91", + "tags" : "Software System Instance", + "properties" : { + "structurizr.dsl.identifier" : "d3633a49-63a4-4495-80b3-e173391180bb" + }, + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "softwareSystemId" : "4" + } ] + }, { + "id" : "73", + "tags" : "Element,Deployment Node,", + "properties" : { + "structurizr.dsl.identifier" : "f55b7219-d698-485d-91c1-ee303f3f5386" }, - "filteredViews": [] + "name" : "bigbank-web***", + "environment" : "Live", + "technology" : "Ubuntu 16.04 LTS", + "instances" : "4", + "children" : [ { + "id" : "74", + "tags" : "Element,Deployment Node", + "properties" : { + "structurizr.dsl.identifier" : "1d58ef82-2528-4ded-84b2-af35a7f18009" + }, + "name" : "Apache Tomcat", + "environment" : "Live", + "technology" : "Apache Tomcat 8.x", + "instances" : "1", + "containerInstances" : [ { + "id" : "75", + "tags" : "Container Instance", + "properties" : { + "structurizr.dsl.identifier" : "livewebapplicationinstance" + }, + "relationships" : [ { + "id" : "76", + "sourceId" : "75", + "destinationId" : "71", + "description" : "Delivers to the customer's web browser", + "linkedRelationshipId" : "31" + } ], + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "10" + } ] + } ] + } ] + } ] + }, + "documentation" : { }, + "views" : { + "systemLandscapeViews" : [ { + "key" : "SystemLandscape", + "order" : 1, + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "enterpriseBoundaryVisible" : true, + "relationships" : [ { + "id" : "27" + }, { + "id" : "26" + }, { + "id" : "25" + }, { + "id" : "24" + }, { + "id" : "23" + }, { + "id" : "22" + }, { + "id" : "21" + }, { + "id" : "20" + }, { + "id" : "19" + } ], + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "2", + "x" : 0, + "y" : 0 + }, { + "id" : "3", + "x" : 0, + "y" : 0 + }, { + "id" : "4", + "x" : 0, + "y" : 0 + }, { + "id" : "5", + "x" : 0, + "y" : 0 + }, { + "id" : "6", + "x" : 0, + "y" : 0 + }, { + "id" : "7", + "x" : 0, + "y" : 0 + } ] + } ], + "systemContextViews" : [ { + "key" : "SystemContext", + "order" : 2, + "description" : "The system context diagram for the Internet Banking System.", + "properties" : { + "structurizr.groups" : "false" + }, + "softwareSystemId" : "7", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "animations" : [ { + "order" : 1, + "elements" : [ "7" ] + }, { + "order" : 2, + "elements" : [ "1" ], + "relationships" : [ "19" ] + }, { + "order" : 3, + "elements" : [ "4" ], + "relationships" : [ "20" ] + }, { + "order" : 4, + "elements" : [ "5" ], + "relationships" : [ "22", "21" ] + } ], + "enterpriseBoundaryVisible" : true, + "relationships" : [ { + "id" : "22" + }, { + "id" : "21" + }, { + "id" : "20" + }, { + "id" : "19" + } ], + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "4", + "x" : 0, + "y" : 0 + }, { + "id" : "5", + "x" : 0, + "y" : 0 + }, { + "id" : "7", + "x" : 0, + "y" : 0 + } ] + } ], + "containerViews" : [ { + "key" : "Containers", + "order" : 3, + "description" : "The container diagram for the Internet Banking System.", + "softwareSystemId" : "7", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "animations" : [ { + "order" : 1, + "elements" : [ "1", "4", "5" ], + "relationships" : [ "22" ] + }, { + "order" : 2, + "elements" : [ "10" ], + "relationships" : [ "28" ] + }, { + "order" : 3, + "elements" : [ "8" ], + "relationships" : [ "29", "31" ] + }, { + "order" : 4, + "elements" : [ "9" ], + "relationships" : [ "30" ] + }, { + "order" : 5, + "elements" : [ "11" ], + "relationships" : [ "33", "47", "37", "49" ] + }, { + "order" : 6, + "elements" : [ "18" ], + "relationships" : [ "45" ] + } ], + "externalSoftwareSystemBoundariesVisible" : true, + "relationships" : [ { + "id" : "29" + }, { + "id" : "28" + }, { + "id" : "37" + }, { + "id" : "22" + }, { + "id" : "33" + }, { + "id" : "45" + }, { + "id" : "31" + }, { + "id" : "30" + }, { + "id" : "47" + }, { + "id" : "49" + } ], + "elements" : [ { + "id" : "11", + "x" : 0, + "y" : 0 + }, { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "4", + "x" : 0, + "y" : 0 + }, { + "id" : "5", + "x" : 0, + "y" : 0 + }, { + "id" : "18", + "x" : 0, + "y" : 0 + }, { + "id" : "8", + "x" : 0, + "y" : 0 + }, { + "id" : "9", + "x" : 0, + "y" : 0 + }, { + "id" : "10", + "x" : 0, + "y" : 0 + } ] + } ], + "componentViews" : [ { + "key" : "Components", + "order" : 4, + "description" : "The component diagram for the API Application.", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "animations" : [ { + "order" : 1, + "elements" : [ "4", "5", "18", "8", "9" ] + }, { + "order" : 2, + "elements" : [ "12", "15" ], + "relationships" : [ "44", "36", "40", "32" ] + }, { + "order" : 3, + "elements" : [ "13", "16" ], + "relationships" : [ "34", "46", "38", "41" ] + }, { + "order" : 4, + "elements" : [ "14", "17" ], + "relationships" : [ "35", "48", "39", "42", "43" ] + } ], + "containerId" : "11", + "externalContainerBoundariesVisible" : true, + "relationships" : [ { + "id" : "40" + }, { + "id" : "41" + }, { + "id" : "42" + }, { + "id" : "43" + }, { + "id" : "32" + }, { + "id" : "36" + }, { + "id" : "35" + }, { + "id" : "34" + }, { + "id" : "44" + }, { + "id" : "46" + }, { + "id" : "48" + }, { + "id" : "38" + }, { + "id" : "39" + } ], + "elements" : [ { + "id" : "12", + "x" : 0, + "y" : 0 + }, { + "id" : "13", + "x" : 0, + "y" : 0 + }, { + "id" : "14", + "x" : 0, + "y" : 0 + }, { + "id" : "4", + "x" : 0, + "y" : 0 + }, { + "id" : "15", + "x" : 0, + "y" : 0 + }, { + "id" : "16", + "x" : 0, + "y" : 0 + }, { + "id" : "5", + "x" : 0, + "y" : 0 + }, { + "id" : "17", + "x" : 0, + "y" : 0 + }, { + "id" : "18", + "x" : 0, + "y" : 0 + }, { + "id" : "8", + "x" : 0, + "y" : 0 + }, { + "id" : "9", + "x" : 0, + "y" : 0 + } ] + } ], + "dynamicViews" : [ { + "key" : "SignIn", + "order" : 6, + "description" : "Summarises how the sign in feature works in the single-page application.", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "elementId" : "11", + "externalBoundariesVisible" : true, + "relationships" : [ { + "id" : "32", + "description" : "Submits credentials to", + "order" : "1", + "response" : false + }, { + "id" : "40", + "description" : "Validates credentials using", + "order" : "2", + "response" : false + }, { + "id" : "44", + "description" : "select * from users where username = ?", + "order" : "3", + "response" : false + }, { + "id" : "44", + "description" : "Returns user data to", + "order" : "4", + "response" : true + }, { + "id" : "40", + "description" : "Returns true if the hashed password matches", + "order" : "5", + "response" : true + }, { + "id" : "32", + "description" : "Sends back an authentication token to", + "order" : "6", + "response" : true + } ], + "elements" : [ { + "id" : "12", + "x" : 0, + "y" : 0 + }, { + "id" : "15", + "x" : 0, + "y" : 0 + }, { + "id" : "18", + "x" : 0, + "y" : 0 + }, { + "id" : "8", + "x" : 0, + "y" : 0 + } ] + } ], + "deploymentViews" : [ { + "key" : "LiveDeployment", + "order" : 8, + "description" : "An example live deployment scenario for the Internet Banking System.", + "softwareSystemId" : "7", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "environment" : "Live", + "animations" : [ { + "order" : 1, + "elements" : [ "69", "70", "71" ] + }, { + "order" : 2, + "elements" : [ "67", "68" ] + }, { + "order" : 3, + "elements" : [ "77", "78", "79", "72", "73", "74", "75" ], + "relationships" : [ "80", "81", "76" ] + }, { + "order" : 4, + "elements" : [ "82", "83", "84" ], + "relationships" : [ "85" ] + }, { + "order" : 5, + "elements" : [ "88", "86", "87" ], + "relationships" : [ "89", "93" ] + } ], + "relationships" : [ { + "id" : "93" + }, { + "id" : "80" + }, { + "id" : "81" + }, { + "id" : "92" + }, { + "id" : "76" + }, { + "id" : "85" + }, { + "id" : "89" + } ], + "elements" : [ { + "id" : "88", + "x" : 0, + "y" : 0 + }, { + "id" : "77", + "x" : 0, + "y" : 0 + }, { + "id" : "67", + "x" : 0, + "y" : 0 + }, { + "id" : "78", + "x" : 0, + "y" : 0 + }, { + "id" : "68", + "x" : 0, + "y" : 0 + }, { + "id" : "79", + "x" : 0, + "y" : 0 + }, { + "id" : "69", + "x" : 0, + "y" : 0 + }, { + "id" : "90", + "x" : 0, + "y" : 0 + }, { + "id" : "91", + "x" : 0, + "y" : 0 + }, { + "id" : "70", + "x" : 0, + "y" : 0 + }, { + "id" : "71", + "x" : 0, + "y" : 0 + }, { + "id" : "82", + "x" : 0, + "y" : 0 + }, { + "id" : "83", + "x" : 0, + "y" : 0 + }, { + "id" : "72", + "x" : 0, + "y" : 0 + }, { + "id" : "84", + "x" : 0, + "y" : 0 + }, { + "id" : "73", + "x" : 0, + "y" : 0 + }, { + "id" : "74", + "x" : 0, + "y" : 0 + }, { + "id" : "86", + "x" : 0, + "y" : 0 + }, { + "id" : "75", + "x" : 0, + "y" : 0 + }, { + "id" : "87", + "x" : 0, + "y" : 0 + } ] + }, { + "key" : "DevelopmentDeployment", + "order" : 7, + "description" : "An example development deployment scenario for the Internet Banking System.", + "softwareSystemId" : "7", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "environment" : "Development", + "animations" : [ { + "order" : 1, + "elements" : [ "50", "51", "52" ] + }, { + "order" : 2, + "elements" : [ "55", "57", "53", "54" ], + "relationships" : [ "56", "58" ] + }, { + "order" : 3, + "elements" : [ "59", "60", "61" ], + "relationships" : [ "62" ] + } ], + "relationships" : [ { + "id" : "62" + }, { + "id" : "56" + }, { + "id" : "66" + }, { + "id" : "58" + } ], + "elements" : [ { + "id" : "55", + "x" : 0, + "y" : 0 + }, { + "id" : "57", + "x" : 0, + "y" : 0 + }, { + "id" : "59", + "x" : 0, + "y" : 0 + }, { + "id" : "60", + "x" : 0, + "y" : 0 + }, { + "id" : "61", + "x" : 0, + "y" : 0 + }, { + "id" : "50", + "x" : 0, + "y" : 0 + }, { + "id" : "51", + "x" : 0, + "y" : 0 + }, { + "id" : "52", + "x" : 0, + "y" : 0 + }, { + "id" : "63", + "x" : 0, + "y" : 0 + }, { + "id" : "53", + "x" : 0, + "y" : 0 + }, { + "id" : "64", + "x" : 0, + "y" : 0 + }, { + "id" : "54", + "x" : 0, + "y" : 0 + }, { + "id" : "65", + "x" : 0, + "y" : 0 + } ] + } ], + "imageViews" : [ { + "key" : "MainframeBankingSystemFacade", + "order" : 5, + "title" : "[Code] Mainframe Banking System Facade", + "elementId" : "16", + "content" : "https://raw.githubusercontent.com/structurizr/examples/main/dsl/big-bank-plc/internet-banking-system/mainframe-banking-system-facade.png", + "contentType" : "image/png" + } ], + "configuration" : { + "branding" : { }, + "styles" : { + "elements" : [ { + "tag" : "Person", + "color" : "#ffffff", + "fontSize" : 22, + "shape" : "Person" + }, { + "tag" : "Customer", + "background" : "#08427b" + }, { + "tag" : "Bank Staff", + "background" : "#999999" + }, { + "tag" : "Software System", + "background" : "#1168bd", + "color" : "#ffffff" + }, { + "tag" : "Existing System", + "background" : "#999999", + "color" : "#ffffff" + }, { + "tag" : "Container", + "background" : "#438dd5", + "color" : "#ffffff" + }, { + "tag" : "Web Browser", + "shape" : "WebBrowser" + }, { + "tag" : "Mobile App", + "shape" : "MobileDeviceLandscape" + }, { + "tag" : "Database", + "shape" : "Cylinder" + }, { + "tag" : "Component", + "background" : "#85bbf0", + "color" : "#000000" + }, { + "tag" : "Failover", + "opacity" : 25 + } ] + }, + "terminology" : { } } + } } \ No newline at end of file diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java index b3ffc35de..0f9214949 100644 --- a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java +++ b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java @@ -43,19 +43,16 @@ public void test_writeCustomView() { } @Test - public void test_writeSystemLandscapeViewWithNoEnterpriseBoundary() { + public void test_writeSystemLandscapeView() { Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); Person user = workspace.getModel().addPerson("User", ""); - user.setLocation(Location.External); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); - softwareSystem.setLocation(Location.Internal); user.uses(softwareSystem, "Uses"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); view.addAllElements(); view.add(box); - view.setEnterpriseBoundaryVisible(false); DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); Diagram diagram = exporter.export(view); @@ -88,7 +85,6 @@ public void test_writeSystemLandscapeViewWithGroupedElements() throws Exception SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); view.addAllElements(); view.add(box); - view.setEnterpriseBoundaryVisible(false); DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); Diagram diagram = exporter.export(view); @@ -179,21 +175,18 @@ public void test_writeSystemLandscapeViewWithNestedGroupedElements() throws Exce } @Test - public void test_writeSystemLandscapeViewWithNoEnterpriseBoundaryInGermanLocale() throws Exception { + public void test_writeSystemLandscapeViewInGermanLocale() throws Exception { // ranksep=1.0 was being output as ranksep=1,0 Locale.setDefault(new Locale("de", "DE")); Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); Person user = workspace.getModel().addPerson("User", ""); - user.setLocation(Location.External); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); - softwareSystem.setLocation(Location.Internal); user.uses(softwareSystem, "Uses"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); view.addAllElements(); view.add(box); - view.setEnterpriseBoundaryVisible(false); DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); Diagram diagram = exporter.export(view); @@ -214,56 +207,16 @@ public void test_writeSystemLandscapeViewWithNoEnterpriseBoundaryInGermanLocale( } @Test - public void test_writeSystemLandscapeViewWithAnEnterpriseBoundary() throws Exception { + public void test_writeSystemContextView() throws Exception { Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); Person user = workspace.getModel().addPerson("User", ""); - user.setLocation(Location.External); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); - softwareSystem.setLocation(Location.Internal); - user.uses(softwareSystem, "Uses"); - - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); - view.addAllElements(); - view.add(box); - view.setEnterpriseBoundaryVisible(true); - - DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); - Diagram diagram = exporter.export(view); - - String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " subgraph cluster_enterprise {\n" + - " margin=25\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + - " }\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + - "\n" + - " 2 -> 3 [id=4]\n" + - "}", content); - } - - @Test - public void test_writeSystemContextViewWithNoEnterpriseBoundary() throws Exception { - Workspace workspace = new Workspace("Name", ""); - CustomElement box = workspace.getModel().addCustomElement("Box"); - Person user = workspace.getModel().addPerson("User", ""); - user.setLocation(Location.External); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); - softwareSystem.setLocation(Location.Internal); user.uses(softwareSystem, "Uses"); SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); view.addAllElements(); view.add(box); - view.setEnterpriseBoundaryVisible(false); DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); Diagram diagram = exporter.export(view); @@ -284,43 +237,6 @@ public void test_writeSystemContextViewWithNoEnterpriseBoundary() throws Excepti } - @Test - public void test_writeSystemContextViewWithAnEnterpriseBoundary() throws Exception { - Workspace workspace = new Workspace("Name", ""); - CustomElement box = workspace.getModel().addCustomElement("Box"); - Person user = workspace.getModel().addPerson("User", ""); - user.setLocation(Location.External); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); - softwareSystem.setLocation(Location.Internal); - user.uses(softwareSystem, "Uses"); - - SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); - view.addAllElements(); - view.add(box); - view.setEnterpriseBoundaryVisible(true); - - DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); - Diagram diagram = exporter.export(view); - - String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " subgraph cluster_enterprise {\n" + - " margin=25\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + - " }\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + - "\n" + - " 2 -> 3 [id=4]\n" + - "}", content); - } - @Test public void test_writeSystemContextViewWithGroupedElements() throws Exception { Workspace workspace = new Workspace("Name", ""); @@ -334,7 +250,6 @@ public void test_writeSystemContextViewWithGroupedElements() throws Exception { SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); view.addAllElements(); view.add(box); - view.setEnterpriseBoundaryVisible(false); DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); Diagram diagram = exporter.export(view); diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java index 0c5ecc86b..041472503 100644 --- a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java +++ b/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java @@ -5,6 +5,7 @@ import com.structurizr.model.Person; import com.structurizr.model.SoftwareSystem; import com.structurizr.model.Tags; +import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.PaperSize; import com.structurizr.view.Shape; import com.structurizr.view.SystemContextView; @@ -49,14 +50,11 @@ public void test_readView() throws Exception { private static Workspace createWorkspace() { Workspace workspace = new Workspace("Name", ""); Person user = workspace.getModel().addPerson("User", ""); - user.setLocation(Location.External); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); - softwareSystem.setLocation(Location.Internal); user.uses(softwareSystem, "Uses"); SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); view.addAllElements(); - view.setEnterpriseBoundaryVisible(true); workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.PERSON).shape(Shape.Person); From 27ede3e9ffac97ac398c468c5141b69a675f9da8 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 10 Jan 2024 08:38:42 +0000 Subject: [PATCH 144/418] Removes `StructurizrClient` (use `WorkspaceApiClient` instead). --- ...> WorkspaceApiClientIntegrationTests.java} | 30 +++---- .../structurizr/api/StructurizrClient.java | 54 ------------- ...ests.java => WorkspaceApiClientTests.java} | 80 ++++++++----------- 3 files changed, 50 insertions(+), 114 deletions(-) rename structurizr-client/src/integrationTest/java/com/structurizr/api/{StructurizrClientIntegrationTests.java => WorkspaceApiClientIntegrationTests.java} (81%) delete mode 100644 structurizr-client/src/main/java/com/structurizr/api/StructurizrClient.java rename structurizr-client/src/test/java/com/structurizr/api/{StructurizrClientTests.java => WorkspaceApiClientTests.java} (55%) diff --git a/structurizr-client/src/integrationTest/java/com/structurizr/api/StructurizrClientIntegrationTests.java b/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java similarity index 81% rename from structurizr-client/src/integrationTest/java/com/structurizr/api/StructurizrClientIntegrationTests.java rename to structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java index 48712d3ce..e54981f04 100644 --- a/structurizr-client/src/integrationTest/java/com/structurizr/api/StructurizrClientIntegrationTests.java +++ b/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java @@ -17,19 +17,19 @@ import static org.junit.jupiter.api.Assertions.*; -public class StructurizrClientIntegrationTests { +public class WorkspaceApiClientIntegrationTests { - private StructurizrClient structurizrClient; - private File workspaceArchiveLocation = new File(System.getProperty("java.io.tmpdir"), "structurizr"); + private WorkspaceApiClient client; + private final File workspaceArchiveLocation = new File(System.getProperty("java.io.tmpdir"), "structurizr"); @BeforeEach void setUp() { - structurizrClient = new StructurizrClient("81ace434-94a1-486f-a786-37bbeaa44e08", "a8673e21-7b6f-4f52-be65-adb7248be86b"); - structurizrClient.setWorkspaceArchiveLocation(workspaceArchiveLocation); + client = new WorkspaceApiClient("81ace434-94a1-486f-a786-37bbeaa44e08", "a8673e21-7b6f-4f52-be65-adb7248be86b"); + client.setWorkspaceArchiveLocation(workspaceArchiveLocation); workspaceArchiveLocation.mkdirs(); clearWorkspaceArchive(); assertEquals(0, workspaceArchiveLocation.listFiles().length); - structurizrClient.setMergeFromRemote(false); + client.setMergeFromRemote(false); } @AfterEach @@ -59,9 +59,9 @@ void putAndGetWorkspace_WithoutEncryption() throws Exception { SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); systemContextView.addAllElements(); - structurizrClient.putWorkspace(20081, workspace); + client.putWorkspace(20081, workspace); - workspace = structurizrClient.getWorkspace(20081); + workspace = client.getWorkspace(20081); assertNotNull(workspace.getModel().getSoftwareSystemWithName("Software System")); assertNotNull(workspace.getModel().getPersonWithName("Person")); assertEquals(1, workspace.getModel().getRelationships().size()); @@ -78,7 +78,7 @@ void putAndGetWorkspace_WithoutEncryption() throws Exception { @Test void putAndGetWorkspace_WithEncryption() throws Exception { - structurizrClient.setEncryptionStrategy(new AesEncryptionStrategy("password")); + client.setEncryptionStrategy(new AesEncryptionStrategy("password")); Workspace workspace = new Workspace("Structurizr client library tests - with encryption", "A test workspace for the Structurizr client library"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); Person person = workspace.getModel().addPerson("Person", "Description"); @@ -86,9 +86,9 @@ void putAndGetWorkspace_WithEncryption() throws Exception { SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); systemContextView.addAllElements(); - structurizrClient.putWorkspace(20081, workspace); + client.putWorkspace(20081, workspace); - workspace = structurizrClient.getWorkspace(20081); + workspace = client.getWorkspace(20081); assertNotNull(workspace.getModel().getSoftwareSystemWithName("Software System")); assertNotNull(workspace.getModel().getPersonWithName("Person")); assertEquals(1, workspace.getModel().getRelationships().size()); @@ -105,15 +105,15 @@ void putAndGetWorkspace_WithEncryption() throws Exception { @Test void lockWorkspace() throws Exception { - structurizrClient.unlockWorkspace(20081); - assertTrue(structurizrClient.lockWorkspace(20081)); + client.unlockWorkspace(20081); + assertTrue(client.lockWorkspace(20081)); } @Test void unlockWorkspace() throws Exception { - structurizrClient.lockWorkspace(20081); - assertTrue(structurizrClient.unlockWorkspace(20081)); + client.lockWorkspace(20081); + assertTrue(client.unlockWorkspace(20081)); } } \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/api/StructurizrClient.java b/structurizr-client/src/main/java/com/structurizr/api/StructurizrClient.java deleted file mode 100644 index 422bad04c..000000000 --- a/structurizr-client/src/main/java/com/structurizr/api/StructurizrClient.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.structurizr.api; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -/** - * A client for the Structurizr workspace API that allows you to get and put Structurizr workspaces in a JSON format. - * - * @deprecated Use WorkspaceApiClient instead - */ -@Deprecated -public class StructurizrClient extends WorkspaceApiClient { - - private static final String STRUCTURIZR_API_URL = "structurizr.api.url"; - private static final String STRUCTURIZR_API_KEY = "structurizr.api.key"; - private static final String STRUCTURIZR_API_SECRET = "structurizr.api.secret"; - - /** - * Creates a new Structurizr client based upon configuration in a structurizr.properties file - * on the classpath with the following name-value pairs: - * - structurizr.api.url - * - structurizr.api.key - * - structurizr.api.secret - * - * @throws StructurizrClientException if something goes wrong - */ - public StructurizrClient() throws StructurizrClientException { - try (InputStream in = - WorkspaceApiClient.class.getClassLoader().getResourceAsStream("structurizr.properties")) { - Properties properties = new Properties(); - if (in != null) { - properties.load(in); - - setUrl(properties.getProperty(STRUCTURIZR_API_URL)); - setApiKey(properties.getProperty(STRUCTURIZR_API_KEY)); - setApiSecret(properties.getProperty(STRUCTURIZR_API_SECRET)); - } else { - throw new StructurizrClientException("Could not find a structurizr.properties file on the classpath."); - } - } catch (IOException e) { - throw new StructurizrClientException(e); - } - } - - public StructurizrClient(String apiKey, String apiSecret) { - super(apiKey, apiSecret); - } - - public StructurizrClient(String url, String apiKey, String apiSecret) { - super(url, apiKey, apiSecret); - } - -} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/api/StructurizrClientTests.java b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientTests.java similarity index 55% rename from structurizr-client/src/test/java/com/structurizr/api/StructurizrClientTests.java rename to structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientTests.java index 4b7fdef59..0e2aeca51 100644 --- a/structurizr-client/src/test/java/com/structurizr/api/StructurizrClientTests.java +++ b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientTests.java @@ -5,38 +5,38 @@ import static org.junit.jupiter.api.Assertions.*; -public class StructurizrClientTests { +public class WorkspaceApiClientTests { - private StructurizrClient structurizrClient; + private WorkspaceApiClient client; @Test void construction_WithTwoParameters() { - structurizrClient = new StructurizrClient("key", "secret"); - assertEquals("https://api.structurizr.com", structurizrClient.getUrl()); - assertEquals("key", structurizrClient.getApiKey()); - assertEquals("secret", structurizrClient.getApiSecret()); + client = new WorkspaceApiClient("key", "secret"); + assertEquals("https://api.structurizr.com", client.getUrl()); + assertEquals("key", client.getApiKey()); + assertEquals("secret", client.getApiSecret()); } @Test void construction_WithThreeParameters() { - structurizrClient = new StructurizrClient("https://localhost", "key", "secret"); - assertEquals("https://localhost", structurizrClient.getUrl()); - assertEquals("key", structurizrClient.getApiKey()); - assertEquals("secret", structurizrClient.getApiSecret()); + client = new WorkspaceApiClient("https://localhost", "key", "secret"); + assertEquals("https://localhost", client.getUrl()); + assertEquals("key", client.getApiKey()); + assertEquals("secret", client.getApiSecret()); } @Test void construction_WithThreeParameters_TruncatesTheApiUrl_WhenTheApiUrlHasATrailingSlashCharacter() { - structurizrClient = new StructurizrClient("https://localhost/", "key", "secret"); - assertEquals("https://localhost", structurizrClient.getUrl()); - assertEquals("key", structurizrClient.getApiKey()); - assertEquals("secret", structurizrClient.getApiSecret()); + client = new WorkspaceApiClient("https://localhost/", "key", "secret"); + assertEquals("https://localhost", client.getUrl()); + assertEquals("key", client.getApiKey()); + assertEquals("secret", client.getApiSecret()); } @Test void construction_ThrowsAnException_WhenANullApiKeyIsUsed() { try { - structurizrClient = new StructurizrClient(null, "secret"); + client = new WorkspaceApiClient(null, "secret"); fail(); } catch (IllegalArgumentException iae) { assertEquals("The API key must not be null or empty.", iae.getMessage()); @@ -46,7 +46,7 @@ void construction_ThrowsAnException_WhenANullApiKeyIsUsed() { @Test void construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { try { - structurizrClient = new StructurizrClient(" ", "secret"); + client = new WorkspaceApiClient(" ", "secret"); fail(); } catch (IllegalArgumentException iae) { assertEquals("The API key must not be null or empty.", iae.getMessage()); @@ -56,7 +56,7 @@ void construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { @Test void construction_ThrowsAnException_WhenANullApiSecretIsUsed() { try { - structurizrClient = new StructurizrClient("key", null); + client = new WorkspaceApiClient("key", null); fail(); } catch (IllegalArgumentException iae) { assertEquals("The API secret must not be null or empty.", iae.getMessage()); @@ -66,7 +66,7 @@ void construction_ThrowsAnException_WhenANullApiSecretIsUsed() { @Test void construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { try { - structurizrClient = new StructurizrClient("key", " "); + client = new WorkspaceApiClient("key", " "); fail(); } catch (IllegalArgumentException iae) { assertEquals("The API secret must not be null or empty.", iae.getMessage()); @@ -76,7 +76,7 @@ void construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { @Test void construction_ThrowsAnException_WhenANullApiUrlIsUsed() { try { - structurizrClient = new StructurizrClient(null, "key", "secret"); + client = new WorkspaceApiClient(null, "key", "secret"); fail(); } catch (IllegalArgumentException iae) { assertEquals("The API URL must not be null or empty.", iae.getMessage()); @@ -86,7 +86,7 @@ void construction_ThrowsAnException_WhenANullApiUrlIsUsed() { @Test void construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { try { - structurizrClient = new StructurizrClient(" ", "key", "secret"); + client = new WorkspaceApiClient(" ", "key", "secret"); fail(); } catch (IllegalArgumentException iae) { assertEquals("The API URL must not be null or empty.", iae.getMessage()); @@ -96,8 +96,8 @@ void construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { @Test void getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { try { - structurizrClient = new StructurizrClient("key", "secret"); - structurizrClient.getWorkspace(0); + client = new WorkspaceApiClient("key", "secret"); + client.getWorkspace(0); fail(); } catch (IllegalArgumentException iae) { assertEquals("The workspace ID must be a positive integer.", iae.getMessage()); @@ -107,8 +107,8 @@ void getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Except @Test void putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { try { - structurizrClient = new StructurizrClient("key", "secret"); - structurizrClient.putWorkspace(0, new Workspace("Name", "Description")); + client = new WorkspaceApiClient("key", "secret"); + client.putWorkspace(0, new Workspace("Name", "Description")); fail(); } catch (IllegalArgumentException iae) { assertEquals("The workspace ID must be a positive integer.", iae.getMessage()); @@ -118,43 +118,33 @@ void putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Except @Test void putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws Exception { try { - structurizrClient = new StructurizrClient("key", "secret"); - structurizrClient.putWorkspace(1234, null); + client = new WorkspaceApiClient("key", "secret"); + client.putWorkspace(1234, null); fail(); } catch (IllegalArgumentException iae) { assertEquals("The workspace must not be null.", iae.getMessage()); } } - @Test - void constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropertiesAreFound() { - try { - structurizrClient = new StructurizrClient(); - fail(); - } catch (Exception e) { - assertEquals("Could not find a structurizr.properties file on the classpath.", e.getMessage()); - } - } - @Test void getAgent() { - structurizrClient = new StructurizrClient("key", "secret"); - assertTrue(structurizrClient.getAgent().startsWith("structurizr-java/")); + client = new WorkspaceApiClient("key", "secret"); + assertTrue(client.getAgent().startsWith("structurizr-java/")); } @Test void setAgent() { - structurizrClient = new StructurizrClient("key", "secret"); - structurizrClient.setAgent("new_agent"); - assertEquals("new_agent", structurizrClient.getAgent()); + client = new WorkspaceApiClient("key", "secret"); + client.setAgent("new_agent"); + assertEquals("new_agent", client.getAgent()); } @Test void setAgent_ThrowsAnException_WhenPassedNull() { - structurizrClient = new StructurizrClient("key", "secret"); + client = new WorkspaceApiClient("key", "secret"); try { - structurizrClient.setAgent(null); + client.setAgent(null); fail(); } catch (Exception e) { assertEquals("An agent must be provided.", e.getMessage()); @@ -163,10 +153,10 @@ void setAgent_ThrowsAnException_WhenPassedNull() { @Test void setAgent_ThrowsAnException_WhenPassedAnEmptyString() { - structurizrClient = new StructurizrClient("key", "secret"); + client = new WorkspaceApiClient("key", "secret"); try { - structurizrClient.setAgent(" "); + client.setAgent(" "); fail(); } catch (Exception e) { assertEquals("An agent must be provided.", e.getMessage()); From 5389f42002a441de1dd14b57902c6f4b12976313 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 10 Jan 2024 08:42:00 +0000 Subject: [PATCH 145/418] Clean up of deprecated items, updated changelog. --- changelog.md | 3 ++- .../main/java/com/structurizr/model/Enterprise.java | 2 +- .../main/java/com/structurizr/view/Configuration.java | 10 ---------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/changelog.md b/changelog.md index 7e95c9fb2..f4b17d08d 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## 2.0.0 (unreleased) +- Removes the deprecated concepts (e.g. location and enterprise. - structurizr-core: Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). -- structurizr-core: Removes the deprecated location and enterprise concepts from `Model`. +- structurizr-client: Removes `StructurizrClient` (use `WorkspaceApiClient` instead). diff --git a/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java b/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java index 5a4b58f2a..ff841e0fb 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java @@ -17,7 +17,7 @@ public final class Enterprise { * @throws IllegalArgumentException if the name is not specified */ @Deprecated - public Enterprise(String name) { + Enterprise(String name) { if (name == null || name.trim().length() == 0) { throw new IllegalArgumentException("Name must be specified."); } diff --git a/structurizr-core/src/main/java/com/structurizr/view/Configuration.java b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java index dc9043da6..9d1a0c3fe 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Configuration.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java @@ -38,16 +38,6 @@ public Styles getStyles() { return styles; } - @JsonIgnore - @Deprecated - public String getTheme() { - if (themes == null || themes.size() == 0) { - return null; - } - - return themes.get(0); - } - /** * Sets the theme used to render views. * From e6d97bf1278da8bafe73b8faf03c437c1e8fbda3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 10 Jan 2024 08:44:57 +0000 Subject: [PATCH 146/418] Fix tests. --- .../java/com/structurizr/view/ConfigurationTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java b/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java index ff3490c42..5a15e24dc 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java @@ -36,14 +36,14 @@ void copyConfigurationFrom() { void setTheme_WithAUrl() { Configuration configuration = new Configuration(); configuration.setTheme("https://example.com/theme.json"); - assertEquals("https://example.com/theme.json", configuration.getTheme()); + assertEquals("https://example.com/theme.json", configuration.getThemes()[0]); } @Test void setTheme_WithAUrlThatHasATrailingSpace() { Configuration configuration = new Configuration(); configuration.setTheme("https://example.com/theme.json "); - assertEquals("https://example.com/theme.json", configuration.getTheme()); + assertEquals("https://example.com/theme.json", configuration.getThemes()[0]); } @Test @@ -58,14 +58,14 @@ void setTheme_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { void setTheme_DoesNothing_WhenANullUrlIsSpecified() { Configuration configuration = new Configuration(); configuration.setTheme(null); - assertNull(configuration.getTheme()); + assertEquals(0, configuration.getThemes().length); } @Test void setTheme_DoesNothing_WhenAnEmptyUrlIsSpecified() { Configuration configuration = new Configuration(); configuration.setTheme(" "); - assertNull(configuration.getTheme()); + assertEquals(0, configuration.getThemes().length); } } \ No newline at end of file From 9162b16d32c561c816e7d3d21f6d03fefbace9ae Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 10 Jan 2024 08:46:38 +0000 Subject: [PATCH 147/418] Adds a note about structurizr-assistant. --- README.md | 3 ++- changelog.md | 1 + structurizr-assistant/README.md | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 41abcb2b6..b66dac88e 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ This repository contains the source code for the following libraries: - [structurizr-core](structurizr-core) - [structurizr-dsl](structurizr-dsl) - [structurizr-export](structurizr-export) -- [structurizr-graphviz](structurizr-graphviz) - [structurizr-import](structurizr-import) +- [structurizr-graphviz](structurizr-graphviz) +- [structurizr-assistant](structurizr-assistant) - [Documentation](https://docs.structurizr.com) - [Changelog](changelog.md) \ No newline at end of file diff --git a/changelog.md b/changelog.md index f4b17d08d..dc9cdf4bd 100644 --- a/changelog.md +++ b/changelog.md @@ -5,4 +5,5 @@ - Removes the deprecated concepts (e.g. location and enterprise. - structurizr-core: Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). - structurizr-client: Removes `StructurizrClient` (use `WorkspaceApiClient` instead). +- structurizr-assistant: Initial version. diff --git a/structurizr-assistant/README.md b/structurizr-assistant/README.md index 790b286e8..816a01919 100644 --- a/structurizr-assistant/README.md +++ b/structurizr-assistant/README.md @@ -1,3 +1,7 @@ # structurizr-assistant [![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-assistant.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-assistant) + +This library provides some utilities that will make recommendations on how to improve a Structurizr workspace. + +- [Documentation](https://docs.structurizr.com/workspaces) \ No newline at end of file From 538a51ed0501422690cab2128687b1991b5b5e33 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 10 Jan 2024 10:47:57 +0000 Subject: [PATCH 148/418] structurizr-graphviz -> structurizr-autolayout (because the former has version numbers 2.0.0+) --- .github/workflows/gradle.yml | 2 +- README.md | 2 +- settings.gradle | 2 +- {structurizr-graphviz => structurizr-autolayout}/README.md | 6 +++--- .../build.gradle | 0 .../com/structurizr/autolayout}/graphviz/Constants.java | 2 +- .../com/structurizr/autolayout}/graphviz/DOTDiagram.java | 2 +- .../com/structurizr/autolayout}/graphviz/DOTExporter.java | 2 +- .../autolayout}/graphviz/GraphvizAutomaticLayout.java | 2 +- .../com/structurizr/autolayout}/graphviz/RankDirection.java | 2 +- .../com/structurizr/autolayout}/graphviz/SVGReader.java | 2 +- .../structurizr/autolayout}/graphviz/DOTExporterTests.java | 4 +++- .../autolayout}/graphviz/GraphvizAutomaticLayoutTests.java | 3 ++- .../structurizr/autolayout}/graphviz/SVGReaderTests.java | 5 ++--- .../src/test/resources/graphviz/SystemContext.dot | 0 .../src/test/resources/graphviz/SystemContext.dot.svg | 0 .../src/test/resources/structurizr-54915-workspace.json | 0 17 files changed, 19 insertions(+), 17 deletions(-) rename {structurizr-graphviz => structurizr-autolayout}/README.md (77%) rename {structurizr-graphviz => structurizr-autolayout}/build.gradle (100%) rename {structurizr-graphviz/src/main/java/com/structurizr => structurizr-autolayout/src/main/java/com/structurizr/autolayout}/graphviz/Constants.java (91%) rename {structurizr-graphviz/src/main/java/com/structurizr => structurizr-autolayout/src/main/java/com/structurizr/autolayout}/graphviz/DOTDiagram.java (86%) rename {structurizr-graphviz/src/main/java/com/structurizr => structurizr-autolayout/src/main/java/com/structurizr/autolayout}/graphviz/DOTExporter.java (99%) rename {structurizr-graphviz/src/main/java/com/structurizr => structurizr-autolayout/src/main/java/com/structurizr/autolayout}/graphviz/GraphvizAutomaticLayout.java (99%) rename {structurizr-graphviz/src/main/java/com/structurizr => structurizr-autolayout/src/main/java/com/structurizr/autolayout}/graphviz/RankDirection.java (87%) rename {structurizr-graphviz/src/main/java/com/structurizr => structurizr-autolayout/src/main/java/com/structurizr/autolayout}/graphviz/SVGReader.java (99%) rename {structurizr-graphviz/src/test/java/com/structurizr => structurizr-autolayout/src/test/java/com/structurizr/autolayout}/graphviz/DOTExporterTests.java (99%) rename {structurizr-graphviz/src/test/java/com/structurizr => structurizr-autolayout/src/test/java/com/structurizr/autolayout}/graphviz/GraphvizAutomaticLayoutTests.java (94%) rename {structurizr-graphviz/src/test/java/com/structurizr => structurizr-autolayout/src/test/java/com/structurizr/autolayout}/graphviz/SVGReaderTests.java (95%) rename {structurizr-graphviz => structurizr-autolayout}/src/test/resources/graphviz/SystemContext.dot (100%) rename {structurizr-graphviz => structurizr-autolayout}/src/test/resources/graphviz/SystemContext.dot.svg (100%) rename {structurizr-graphviz => structurizr-autolayout}/src/test/resources/structurizr-54915-workspace.json (100%) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 68c76f168..07cd9183c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -28,4 +28,4 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew -x :structurizr-graphviz:test + run: ./gradlew -x :structurizr-autolayout:test diff --git a/README.md b/README.md index b66dac88e..7fe8a09ff 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repository contains the source code for the following libraries: - [structurizr-dsl](structurizr-dsl) - [structurizr-export](structurizr-export) - [structurizr-import](structurizr-import) -- [structurizr-graphviz](structurizr-graphviz) +- [structurizr-autolayout](structurizr-autolayout) - [structurizr-assistant](structurizr-assistant) - [Documentation](https://docs.structurizr.com) diff --git a/settings.gradle b/settings.gradle index e06f2f882..235846a6b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,5 +5,5 @@ include 'structurizr-client' include 'structurizr-core' include 'structurizr-dsl' include 'structurizr-export' -include 'structurizr-graphviz' +include 'structurizr-autolayout' include 'structurizr-import' diff --git a/structurizr-graphviz/README.md b/structurizr-autolayout/README.md similarity index 77% rename from structurizr-graphviz/README.md rename to structurizr-autolayout/README.md index bed0d0a76..92e58e3e6 100644 --- a/structurizr-graphviz/README.md +++ b/structurizr-autolayout/README.md @@ -1,6 +1,6 @@ -# structurizr-graphviz +# structurizr-autolayout -[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-graphviz.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-graphviz) +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-autolayout.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-autolayout) This library provides automatic facilities for Structurizr views. It's a wrapper around the [Graphviz tool](http://www.graphviz.org), @@ -17,7 +17,7 @@ GraphvizAutomaticLayout graphviz = new GraphvizAutomaticLayout(); graphviz.apply(workspace); ``` -The ```structurizr-graphviz``` library does the following for every view in the workspace: +The ```structurizr-autolayout``` library does the following for every view in the workspace: 1. Export the view to a DOT file. 2. Run Graphviz (via the ```dot``` command), with the output format set to SVG. diff --git a/structurizr-graphviz/build.gradle b/structurizr-autolayout/build.gradle similarity index 100% rename from structurizr-graphviz/build.gradle rename to structurizr-autolayout/build.gradle diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/Constants.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/Constants.java similarity index 91% rename from structurizr-graphviz/src/main/java/com/structurizr/graphviz/Constants.java rename to structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/Constants.java index 37340f9cb..34cf8efc5 100644 --- a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/Constants.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/Constants.java @@ -1,4 +1,4 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; /** * Some constants used when applying graphviz. diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTDiagram.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTDiagram.java similarity index 86% rename from structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTDiagram.java rename to structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTDiagram.java index 40b232858..0e2cc9e7b 100644 --- a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTDiagram.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTDiagram.java @@ -1,4 +1,4 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; import com.structurizr.export.Diagram; import com.structurizr.view.ModelView; diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTExporter.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java similarity index 99% rename from structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTExporter.java rename to structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java index c3c73b6cc..bff97a7e3 100644 --- a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/DOTExporter.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java @@ -1,4 +1,4 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; import com.structurizr.export.AbstractDiagramExporter; import com.structurizr.export.Diagram; diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java similarity index 99% rename from structurizr-graphviz/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java rename to structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java index defdc86f0..07ca03630 100644 --- a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java @@ -1,4 +1,4 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; import com.structurizr.Workspace; import com.structurizr.export.Diagram; diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/RankDirection.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/RankDirection.java similarity index 87% rename from structurizr-graphviz/src/main/java/com/structurizr/graphviz/RankDirection.java rename to structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/RankDirection.java index 4bbe7486e..e3c4e366c 100644 --- a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/RankDirection.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/RankDirection.java @@ -1,4 +1,4 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; /** * The various rank directions used by graphviz. diff --git a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/SVGReader.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java similarity index 99% rename from structurizr-graphviz/src/main/java/com/structurizr/graphviz/SVGReader.java rename to structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java index 4fde3f242..359c2edb5 100644 --- a/structurizr-graphviz/src/main/java/com/structurizr/graphviz/SVGReader.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java @@ -1,4 +1,4 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; import com.structurizr.model.DeploymentNode; import com.structurizr.model.Element; diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java similarity index 99% rename from structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java rename to structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java index 0f9214949..367015556 100644 --- a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/DOTExporterTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java @@ -1,6 +1,8 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; import com.structurizr.Workspace; +import com.structurizr.autolayout.graphviz.DOTExporter; +import com.structurizr.autolayout.graphviz.RankDirection; import com.structurizr.export.Diagram; import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/GraphvizAutomaticLayoutTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java similarity index 94% rename from structurizr-graphviz/src/test/java/com/structurizr/graphviz/GraphvizAutomaticLayoutTests.java rename to structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java index c92385bb4..233423401 100644 --- a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/GraphvizAutomaticLayoutTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java @@ -1,6 +1,7 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; import com.structurizr.Workspace; +import com.structurizr.autolayout.graphviz.GraphvizAutomaticLayout; import com.structurizr.model.Person; import com.structurizr.model.SoftwareSystem; import com.structurizr.model.Tags; diff --git a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/SVGReaderTests.java similarity index 95% rename from structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java rename to structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/SVGReaderTests.java index 041472503..c74a59937 100644 --- a/structurizr-graphviz/src/test/java/com/structurizr/graphviz/SVGReaderTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/SVGReaderTests.java @@ -1,11 +1,10 @@ -package com.structurizr.graphviz; +package com.structurizr.autolayout.graphviz; import com.structurizr.Workspace; -import com.structurizr.model.Location; +import com.structurizr.autolayout.graphviz.SVGReader; import com.structurizr.model.Person; import com.structurizr.model.SoftwareSystem; import com.structurizr.model.Tags; -import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.PaperSize; import com.structurizr.view.Shape; import com.structurizr.view.SystemContextView; diff --git a/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot b/structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot similarity index 100% rename from structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot rename to structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot diff --git a/structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot.svg b/structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot.svg similarity index 100% rename from structurizr-graphviz/src/test/resources/graphviz/SystemContext.dot.svg rename to structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot.svg diff --git a/structurizr-graphviz/src/test/resources/structurizr-54915-workspace.json b/structurizr-autolayout/src/test/resources/structurizr-54915-workspace.json similarity index 100% rename from structurizr-graphviz/src/test/resources/structurizr-54915-workspace.json rename to structurizr-autolayout/src/test/resources/structurizr-54915-workspace.json From 4f5aa81a5cfb743cd85312dbfc4221b975c0b86d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 10 Jan 2024 16:31:58 +0000 Subject: [PATCH 149/418] Adds a public `parse(String, File)` method ... closes #230 --- .../structurizr/dsl/StructurizrDslParser.java | 115 ++++++++++-------- 1 file changed, 64 insertions(+), 51 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index db957889f..8bfa5bc9e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -35,12 +35,12 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private Charset characterEncoding = StandardCharsets.UTF_8; private IdentifierScope identifierScope = IdentifierScope.Flat; - private Stack contextStack; - private Set parsedTokens = new HashSet<>(); - private IdentifiersRegister identifiersRegister; - private Map constants; + private final Stack contextStack; + private final Set parsedTokens = new HashSet<>(); + private final IdentifiersRegister identifiersRegister; + private final Map constants; - private List dslSourceLines = new ArrayList<>(); + private final List dslSourceLines = new ArrayList<>(); private Workspace workspace; private boolean extendingWorkspace = false; @@ -121,27 +121,22 @@ void parse(DslParserContext context, File path) throws StructurizrDslParserExcep } /** - * Parses the specified Structurizr DSL file(s), adding the parsed content to the workspace. - * If "path" represents a single file, that single file will be parsed. - * If "path" represents a directory, all files in that directory (recursively) will be parsed. + * Parses the specified Structurizr DSL file. * - * @param path a File object representing a file or directory + * @param dslFile a File object representing a DSL file * @throws StructurizrDslParserException when something goes wrong */ - public void parse(File path) throws StructurizrDslParserException { - if (path == null) { + public void parse(File dslFile) throws StructurizrDslParserException { + if (dslFile == null) { throw new StructurizrDslParserException("A file must be specified"); } - if (!path.exists()) { - throw new StructurizrDslParserException("The file at " + path.getAbsolutePath() + " does not exist"); + if (!dslFile.exists()) { + throw new StructurizrDslParserException("The file at " + dslFile.getAbsolutePath() + " does not exist"); } - List files = FileUtils.findFiles(path); try { - for (File file : files) { - parse(Files.readAllLines(file.toPath(), characterEncoding), file); - } + parse(Files.readAllLines(dslFile.toPath(), characterEncoding), dslFile); } catch (IOException e) { throw new StructurizrDslParserException(e.getMessage()); } @@ -154,51 +149,38 @@ void parse(DslParserContext context, String dsl) throws StructurizrDslParserExce } /** - * Parses the specified Structurizr DSL fragment, adding the parsed content to the workspace. + * Parses the specified Structurizr DSL, adding the parsed content to the workspace. * - * @param dsl a DSL fragment + * @param dsl a Structurizr DSL definition, as a single String * @throws StructurizrDslParserException when something goes wrong */ public void parse(String dsl) throws StructurizrDslParserException { + parse(dsl, new File(".")); + } + + /** + * Parses the specified Structurizr DSL, adding the parsed content to the workspace. + * + * @param dsl a Structurizr DSL definition, as a single String + * @param dslFile a File representing the DSL file, and therefore where includes/images/etc should be loaded relative to + * @throws StructurizrDslParserException when something goes wrong + */ + public void parse(String dsl, File dslFile) throws StructurizrDslParserException { if (StringUtils.isNullOrEmpty(dsl)) { throw new StructurizrDslParserException("A DSL fragment must be specified"); } List lines = Arrays.asList(dsl.split("\\r?\\n")); - parse(lines, new File(".")); - } - - private List preProcessLines(List lines) { - List dslLines = new ArrayList<>(); - - int lineNumber = 1; - StringBuilder buf = new StringBuilder(); - boolean lineComplete = true; - - for (String line : lines) { - if (line.endsWith(MULTI_LINE_SEPARATOR)) { - buf.append(line, 0, line.length()-1); - lineComplete = false; - } else { - if (lineComplete) { - buf.append(line); - } else { - buf.append(line.stripLeading()); - lineComplete = true; - } - } - - if (lineComplete) { - dslLines.add(new DslLine(buf.toString(), lineNumber)); - buf = new StringBuilder(); - } - - lineNumber++; - } - - return dslLines; + parse(lines, dslFile); } + /** + * Parses a list of Structurizr DSL lines. + * + * @param lines a Structurizr DSL definition, as a List of String objects (one per line) + * @param dslFile a File representing the DSL file, and therefore where includes/images/etc should be loaded relative to + * @throws StructurizrDslParserException when something goes wrong + */ void parse(List lines, File dslFile) throws StructurizrDslParserException { List dslLines = preProcessLines(lines); @@ -930,6 +912,37 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio } } + private List preProcessLines(List lines) { + List dslLines = new ArrayList<>(); + + int lineNumber = 1; + StringBuilder buf = new StringBuilder(); + boolean lineComplete = true; + + for (String line : lines) { + if (line.endsWith(MULTI_LINE_SEPARATOR)) { + buf.append(line, 0, line.length()-1); + lineComplete = false; + } else { + if (lineComplete) { + buf.append(line); + } else { + buf.append(line.stripLeading()); + lineComplete = true; + } + } + + if (lineComplete) { + dslLines.add(new DslLine(buf.toString(), lineNumber)); + buf = new StringBuilder(); + } + + lineNumber++; + } + + return dslLines; + } + private String substituteStrings(String token) { Matcher m = STRING_SUBSTITUTION_PATTERN.matcher(token); while (m.find()) { From c6daf729048115141ebc9ab7cd1994995f4f0863 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 11 Jan 2024 09:09:04 +0000 Subject: [PATCH 150/418] Refactoring of priority enum. --- .../main/java/com/structurizr/assistant/Inspection.java | 6 +++--- .../main/java/com/structurizr/assistant/Priority.java | 9 --------- .../java/com/structurizr/assistant/Recommendation.java | 6 ++++++ .../structurizr/assistant/model/ComponentInspection.java | 2 +- .../structurizr/assistant/model/ContainerInspection.java | 2 +- .../model/ComponentDescriptionInspectionTests.java | 3 +-- .../model/ComponentTechnologyInspectionTests.java | 3 +-- .../model/ContainerDescriptionInspectionTests.java | 3 +-- .../model/ContainerTechnologyInspectionTests.java | 3 +-- .../model/DeploymentNodeDescriptionInspectionTests.java | 3 +-- .../model/DeploymentNodeTechnologyInspectionTests.java | 3 +-- .../ElementNotIncludedInAnyViewsInspectionTests.java | 3 +-- .../assistant/model/EmptyDeploymentNodeCheckTests.java | 3 +-- .../assistant/model/EmptyModelInspectionTests.java | 3 +-- .../InfrastructureNodeDescriptionInspectionTests.java | 3 +-- .../InfrastructureNodeTechnologyInspectionTests.java | 3 +-- .../MultipleSoftwareSystemsDetailedInspectionTests.java | 7 +++---- .../assistant/model/OrphanedElementInspectionTests.java | 3 +-- .../model/PersonDescriptionInspectionTests.java | 3 +-- .../model/RelationshipDescriptionInspectionTests.java | 5 ++--- .../model/RelationshipTechnologyInspectionTests.java | 5 ++--- .../model/SoftwareSystemDescriptionInspectionTests.java | 3 +-- ...erViewsForMultipleSoftwareSystemsInspectionTests.java | 3 +-- .../assistant/view/EmptyViewsInspectionTests.java | 3 +-- ...xtViewsForMultipleSoftwareSystemsInspectionTests.java | 3 +-- .../workspace/WorkspaceScopeInspectionTests.java | 3 +-- 26 files changed, 36 insertions(+), 60 deletions(-) delete mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java index 22458a136..23442bea2 100644 --- a/structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/Inspection.java @@ -36,15 +36,15 @@ protected Recommendation noRecommendation() { } protected Recommendation lowPriorityRecommendation(String description) { - return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Priority.Low, description); + return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Recommendation.Priority.Low, description); } protected Recommendation mediumPriorityRecommendation(String description) { - return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Priority.Medium, description); + return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Recommendation.Priority.Medium, description); } protected Recommendation highPriorityRecommendation(String description) { - return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Priority.High, description); + return new Recommendation(STRUCTURIZR_RECOMMENDATIONS_PREFIX + getType(), Recommendation.Priority.High, description); } } \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java deleted file mode 100644 index 5a2c3085e..000000000 --- a/structurizr-assistant/src/main/java/com/structurizr/assistant/Priority.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.structurizr.assistant; - -public enum Priority { - - Low, - Medium, - High - -} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java index 1e35b3c63..11f957724 100644 --- a/structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/Recommendation.java @@ -29,4 +29,10 @@ public String toString() { return type + " | " + priority + " | " + description; } + public enum Priority { + Low, + Medium, + High + } + } \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java index 4c64146f4..f09576439 100644 --- a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ComponentInspection.java @@ -12,7 +12,7 @@ public ComponentInspection(Workspace workspace) { } @Override - protected Recommendation inspect(Element element) { + protected final Recommendation inspect(Element element) { return inspect((Component)element); } diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java index 4c7dea0fd..5dcebd052 100644 --- a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/ContainerInspection.java @@ -12,7 +12,7 @@ public ContainerInspection(Workspace workspace) { } @Override - protected Recommendation inspect(Element element) { + protected final Recommendation inspect(Element element) { return inspect((Container)element); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java index eb17e0c0f..5402ae992 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentDescriptionInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Component; import com.structurizr.model.Container; @@ -22,7 +21,7 @@ public void run_WithoutDescription() { Component component = container.addComponent("Name"); Recommendation recommendation = new ComponentDescriptionInspection(workspace).run(component); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.component.description", recommendation.getType()); assertEquals("Add a description to the component named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java index f5f1826a2..bd5eee769 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ComponentTechnologyInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Component; import com.structurizr.model.Container; @@ -22,7 +21,7 @@ public void run_WithoutDescription() { Component component = container.addComponent("Name"); Recommendation recommendation = new ComponentTechnologyInspection(workspace).run(component); - Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Low, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.component.technology", recommendation.getType()); assertEquals("Add a technology to the component named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java index 9b0655c04..abd8a2c90 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerDescriptionInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Container; import com.structurizr.model.SoftwareSystem; @@ -20,7 +19,7 @@ public void run_WithoutDescription() { Container container = softwareSystem.addContainer("Name"); Recommendation recommendation = new ContainerDescriptionInspection(workspace).run(container); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.container.description", recommendation.getType()); assertEquals("Add a description to the container named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java index 3f2e08854..af1164c2a 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ContainerTechnologyInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Container; import com.structurizr.model.SoftwareSystem; @@ -20,7 +19,7 @@ public void run_WithoutDescription() { Container container = softwareSystem.addContainer("Name"); Recommendation recommendation = new ContainerTechnologyInspection(workspace).run(container); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.container.technology", recommendation.getType()); assertEquals("Add a technology to the container named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java index e495234e2..5f7ac2cda 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeDescriptionInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.DeploymentNode; import org.junit.jupiter.api.Assertions; @@ -18,7 +17,7 @@ public void run_WithoutDescription() { DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name"); Recommendation recommendation = new DeploymentNodeDescriptionInspection(workspace).run(deploymentNode); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.deploymentnode.description", recommendation.getType()); assertEquals("Add a description to the deployment node named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java index 5c0aa5660..ef63a920f 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/DeploymentNodeTechnologyInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.DeploymentNode; import org.junit.jupiter.api.Assertions; @@ -18,7 +17,7 @@ public void run_WithoutDescription() { DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name"); Recommendation recommendation = new DeploymentNodeTechnologyInspection(workspace).run(deploymentNode); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.deploymentnode.technology", recommendation.getType()); assertEquals("Add a technology to the deployment node named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java index d92e2913a..29996f387 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/ElementNotIncludedInAnyViewsInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Assertions; @@ -18,7 +17,7 @@ public void run_NotInViews() { SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); Recommendation recommendation = new ElementNotIncludedInAnyViewsInspection(workspace).run(softwareSystem); - Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Low, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.element.noview", recommendation.getType()); assertEquals("The software system named \"Name\" is not included on any views - add it to a view.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java index 2bde35613..20509e17f 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyDeploymentNodeCheckTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Container; import com.structurizr.model.DeploymentNode; @@ -20,7 +19,7 @@ public void run_Empty() { DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name"); Recommendation recommendation = new EmptyDeploymentNodeInspection(workspace).run(deploymentNode); - Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Low, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.deploymentnode.empty", recommendation.getType()); assertEquals("The deployment node named \"Name\" is empty.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java index e022667e6..3f6e7afe7 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/EmptyModelInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,7 +15,7 @@ public void run_WhenThereAreNoElements() { Workspace workspace = new Workspace("Name", "Description"); Recommendation recommendation = new EmptyModelInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.empty", recommendation.getType()); assertEquals("Add some elements to the model.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java index b9257dd0c..a74694c76 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeDescriptionInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.InfrastructureNode; import org.junit.jupiter.api.Assertions; @@ -19,7 +18,7 @@ public void run_WithoutDescription() { .addInfrastructureNode("Name"); Recommendation recommendation = new InfrastructureNodeDescriptionInspection(workspace).run(infrastructureNode); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.infrastructurenode.description", recommendation.getType()); assertEquals("Add a description to the infrastructure node named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java index fd990cdae..b22a991f8 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/InfrastructureNodeTechnologyInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.InfrastructureNode; import org.junit.jupiter.api.Assertions; @@ -19,7 +18,7 @@ public void run_WithoutDescription() { .addInfrastructureNode("Name"); Recommendation recommendation = new InfrastructureNodeTechnologyInspection(workspace).run(infrastructureNode); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.infrastructurenode.technology", recommendation.getType()); assertEquals("Add a technology to the infrastructure node named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java index 3dcce3c0d..86e238522 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/MultipleSoftwareSystemsDetailedInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.documentation.Decision; import com.structurizr.documentation.Format; @@ -22,7 +21,7 @@ public void run_MultipleSoftwareSystemsWithContainers() { workspace.getModel().addSoftwareSystem("B").addContainer("Container"); Recommendation recommendation = new MultipleSoftwareSystemsDetailedInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); assertEquals("This workspace describes the internal details of 2 software systems. It is recommended that a workspace contains the model, views, and documentation for a single software system only.", recommendation.getDescription()); } @@ -34,7 +33,7 @@ public void run_MultipleSoftwareSystemsWithDocumentation() { workspace.getModel().addSoftwareSystem("B").getDocumentation().addSection(new Section(Format.Markdown, "# Section 1")); Recommendation recommendation = new MultipleSoftwareSystemsDetailedInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); assertEquals("This workspace describes the internal details of 2 software systems. It is recommended that a workspace contains the model, views, and documentation for a single software system only.", recommendation.getDescription()); } @@ -51,7 +50,7 @@ public void run_MultipleSoftwareSystemsWithDecisions() { workspace.getModel().addSoftwareSystem("B").getDocumentation().addDecision(decision); Recommendation recommendation = new MultipleSoftwareSystemsDetailedInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); assertEquals("This workspace describes the internal details of 2 software systems. It is recommended that a workspace contains the model, views, and documentation for a single software system only.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java index a92153dea..04d40014b 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/OrphanedElementInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Assertions; @@ -18,7 +17,7 @@ public void run_WithOrphan() { SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); Recommendation recommendation = new OrphanedElementInspection(workspace).run(a); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.element.orphaned", recommendation.getType()); assertEquals("The software system named \"A\" is orphaned - add a relationship to/from it, or consider removing it from the model.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java index 03a4b07e6..e71b68671 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/PersonDescriptionInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Person; import org.junit.jupiter.api.Assertions; @@ -18,7 +17,7 @@ public void run_WithoutDescription() { Person person = workspace.getModel().addPerson("Name"); Recommendation recommendation = new PersonDescriptionInspection(workspace).run(person); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.person.description", recommendation.getType()); assertEquals("Add a description to the person named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java index 69b46f4af..d17de1c38 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipDescriptionInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Component; import com.structurizr.model.Container; @@ -23,7 +22,7 @@ public void run_WithoutDescription_MediumPriority() { Relationship relationship = a.uses(b, ""); Recommendation recommendation = new RelationshipDescriptionInspection(workspace).run(relationship); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.relationship.description", recommendation.getType()); assertEquals("Add a description to the relationship between the software system named \"A\" and the software system named \"B\".", recommendation.getDescription()); } @@ -38,7 +37,7 @@ public void run_WithoutDescription_LowPriority() { Relationship relationship = a.uses(b, ""); Recommendation recommendation = new RelationshipDescriptionInspection(workspace).run(relationship); - Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Low, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.relationship.description", recommendation.getType()); assertEquals("Add a description to the relationship between the component named \"A\" and the component named \"B\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java index 2fe68d6c5..d956cf29e 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/RelationshipTechnologyInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.Container; import com.structurizr.model.Relationship; @@ -22,7 +21,7 @@ public void run_WithoutDescription_LowPriority() { Relationship relationship = a.uses(b, "Description"); Recommendation recommendation = new RelationshipTechnologyInspection(workspace).run(relationship); - Assertions.assertEquals(Priority.Low, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Low, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.relationship.technology", recommendation.getType()); assertEquals("Add a technology to the relationship between the software system named \"A\" and the software system named \"B\".", recommendation.getDescription()); } @@ -36,7 +35,7 @@ public void run_WithoutDescription_MediumPriority() { Relationship relationship = a.uses(b, "Description"); Recommendation recommendation = new RelationshipTechnologyInspection(workspace).run(relationship); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.relationship.technology", recommendation.getType()); assertEquals("Add a technology to the relationship between the container named \"A\" and the container named \"B\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java index 49389b30d..49242a971 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDescriptionInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.model; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Assertions; @@ -18,7 +17,7 @@ public void run_WithoutDescription() { SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); Recommendation recommendation = new SoftwareSystemDescriptionInspection(workspace).run(softwareSystem); - Assertions.assertEquals(Priority.Medium, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.Medium, recommendation.getPriority()); assertEquals("structurizr.recommendations.model.softwaresystem.description", recommendation.getType()); assertEquals("Add a description to the software system named \"Name\".", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java index 8b389b481..92ef519a6 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.view; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Assertions; @@ -21,7 +20,7 @@ public void run_MultipleSoftwareSystems() { workspace.getViews().createContainerView(b, "Containers-B", "Description"); Recommendation recommendation = new ContainerViewsForMultipleSoftwareSystemsInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); assertEquals("Container views exist for 2 software systems. It is recommended that a workspace includes container views for a single software system only.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java index 2d5c9b40c..977ee83c4 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/EmptyViewsInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.view; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,7 +15,7 @@ public void run_WhenThereAreNoViews() { Workspace workspace = new Workspace("Name", "Description"); Recommendation recommendation = new EmptyViewsInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.views.empty", recommendation.getType()); assertEquals("Add some views to the workspace.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java index 48ec21171..331af6966 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.view; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Assertions; @@ -21,7 +20,7 @@ public void run_MultipleSoftwareSystems() { workspace.getViews().createSystemContextView(b, "SystemContext-B", "Description"); Recommendation recommendation = new SystemContextViewsForMultipleSoftwareSystemsInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); assertEquals("System context views exist for 2 software systems. It is recommended that a workspace includes system context views for a single software system only.", recommendation.getDescription()); } diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java index d48e93c77..10df6726b 100644 --- a/structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/workspace/WorkspaceScopeInspectionTests.java @@ -1,7 +1,6 @@ package com.structurizr.assistant.workspace; import com.structurizr.Workspace; -import com.structurizr.assistant.Priority; import com.structurizr.assistant.Recommendation; import com.structurizr.configuration.WorkspaceScope; import org.junit.jupiter.api.Assertions; @@ -17,7 +16,7 @@ public void run_WithUnscopedWorkspace() { Workspace workspace = new Workspace("Name", "Description"); Recommendation recommendation = new WorkspaceScopeInspection(workspace).run(); - Assertions.assertEquals(Priority.High, recommendation.getPriority()); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); assertEquals("structurizr.recommendations.workspace.scope", recommendation.getType()); assertEquals("This workspace has no defined scope. It is recommended that the workspace scope is set to \"Landscape\" or \"SoftwareSystem\".", recommendation.getDescription()); } From 0de6e7c197cb8455c345de818bf6fb19b9a666e3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 11 Jan 2024 09:11:04 +0000 Subject: [PATCH 151/418] Adds documentation/decisions inspections for software systems that have containers. --- .../assistant/DefaultAssistant.java | 2 + .../SoftwareSystemDecisionsInspection.java | 27 +++++++++++ ...SoftwareSystemDocumentationInspection.java | 27 +++++++++++ .../model/SoftwareSystemInspection.java | 22 +++++++++ ...SoftwareSystemDecisionInspectionTests.java | 47 +++++++++++++++++++ ...areSystemDocumentationInspectionTests.java | 40 ++++++++++++++++ 6 files changed, 165 insertions(+) create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDecisionsInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspection.java create mode 100644 structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemInspection.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDecisionInspectionTests.java create mode 100644 structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspectionTests.java diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java index ec1114489..4af07a797 100644 --- a/structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/DefaultAssistant.java @@ -42,6 +42,8 @@ private void runModelInspections(Workspace workspace) { if (element instanceof SoftwareSystem) { add(new SoftwareSystemDescriptionInspection(workspace).run(element)); + add(new SoftwareSystemDocumentationInspection(workspace).run(element)); + add(new SoftwareSystemDecisionsInspection(workspace).run(element)); } if (element instanceof Container) { diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDecisionsInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDecisionsInspection.java new file mode 100644 index 000000000..7705e844d --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDecisionsInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; + +public class SoftwareSystemDecisionsInspection extends SoftwareSystemInspection { + + public SoftwareSystemDecisionsInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(SoftwareSystem softwareSystem) { + if (softwareSystem.hasContainers() && softwareSystem.getDocumentation().getDecisions().isEmpty()) { + return highPriorityRecommendation("The " + terminologyFor(softwareSystem).toLowerCase() + " named \"" + softwareSystem.getName() + "\" has containers, but is missing decisions."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.softwaresystem.decisions"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspection.java new file mode 100644 index 000000000..bca6ac926 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.SoftwareSystem; + +public class SoftwareSystemDocumentationInspection extends SoftwareSystemInspection { + + public SoftwareSystemDocumentationInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected Recommendation inspect(SoftwareSystem softwareSystem) { + if (softwareSystem.hasContainers() && softwareSystem.getDocumentation().getSections().isEmpty()) { + return highPriorityRecommendation("The " + terminologyFor(softwareSystem).toLowerCase() + " named \"" + softwareSystem.getName() + "\" has containers, but is missing documentation."); + } + + return noRecommendation(); + } + + @Override + protected String getType() { + return "model.softwaresystem.documentation"; + } + +} \ No newline at end of file diff --git a/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemInspection.java b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemInspection.java new file mode 100644 index 000000000..4ce0dea23 --- /dev/null +++ b/structurizr-assistant/src/main/java/com/structurizr/assistant/model/SoftwareSystemInspection.java @@ -0,0 +1,22 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.model.Container; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; + +abstract class SoftwareSystemInspection extends ElementInspection { + + public SoftwareSystemInspection(Workspace workspace) { + super(workspace); + } + + @Override + protected final Recommendation inspect(Element element) { + return inspect((SoftwareSystem)element); + } + + protected abstract Recommendation inspect(SoftwareSystem softwareSystem); + +} \ No newline at end of file diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDecisionInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDecisionInspectionTests.java new file mode 100644 index 000000000..1e79c4a68 --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDecisionInspectionTests.java @@ -0,0 +1,47 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class SoftwareSystemDecisionInspectionTests { + + @Test + public void run_WithoutDecision() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name"); + + Recommendation recommendation = new SoftwareSystemDecisionsInspection(workspace).run(softwareSystem); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.softwaresystem.decisions", recommendation.getType()); + assertEquals("The software system named \"Software System\" has containers, but is missing decisions.", recommendation.getDescription()); + } + + @Test + public void run_WithDecision() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name", "Description"); + + Decision decision = new Decision("1"); + decision.setFormat(Format.Markdown); + decision.setTitle("Decision 1"); + decision.setContent("Content"); + decision.setStatus("Accepted"); + softwareSystem.getDocumentation().addDecision(decision); + + Recommendation recommendation = new SoftwareSystemDecisionsInspection(workspace).run(softwareSystem); + assertNull(recommendation); + } + +} diff --git a/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspectionTests.java b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspectionTests.java new file mode 100644 index 000000000..f0a8b34ad --- /dev/null +++ b/structurizr-assistant/src/test/java/com/structurizr/assistant/model/SoftwareSystemDocumentationInspectionTests.java @@ -0,0 +1,40 @@ +package com.structurizr.assistant.model; + +import com.structurizr.Workspace; +import com.structurizr.assistant.Recommendation; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class SoftwareSystemDocumentationInspectionTests { + + @Test + public void run_WithoutDocumentation() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name"); + + Recommendation recommendation = new SoftwareSystemDocumentationInspection(workspace).run(softwareSystem); + Assertions.assertEquals(Recommendation.Priority.High, recommendation.getPriority()); + assertEquals("structurizr.recommendations.model.softwaresystem.documentation", recommendation.getType()); + assertEquals("The software system named \"Software System\" has containers, but is missing documentation.", recommendation.getDescription()); + } + + @Test + public void run_WithDocumentation() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name", "Description"); + softwareSystem.getDocumentation().addSection(new Section(Format.Markdown, "# Section 1")); + + Recommendation recommendation = new SoftwareSystemDocumentationInspection(workspace).run(softwareSystem); + assertNull(recommendation); + } + +} From 08e4434d044fd3e2195ac0efebf86551891bf7cd Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 11 Jan 2024 13:01:21 +0000 Subject: [PATCH 152/418] Adds support for decisions managed by Log4brains, also adds `!decisions` as a synonym for `!adrs`. --- changelog.md | 2 + .../structurizr/dsl/ComponentDslContext.java | 2 + .../structurizr/dsl/ContainerDslContext.java | 2 +- .../{AdrsParser.java => DecisionsParser.java} | 33 ++- .../dsl/SoftwareSystemDslContext.java | 2 +- .../structurizr/dsl/StructurizrDslParser.java | 16 +- .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../structurizr/dsl/WorkspaceDslContext.java | 2 +- ...erTests.java => DecisionsParserTests.java} | 8 +- .../java/com/structurizr/dsl/DslTests.java | 15 +- .../0001-record-architecture-decisions.md | 0 .../0002-implement-as-shell-scripts.md | 0 .../0003-single-command-with-subcommands.md | 0 .../adrtools}/0004-markdown-format.md | 0 .../adrtools}/0005-help-comments.md | 0 ...n-in-other-version-control-repositories.md | 0 ...-config-executable-to-get-configuration.md | 0 .../0008-use-iso-8601-format-for-dates.md | 0 .../adrtools}/0009-help-scripts.md | 0 .../adrtools}/0010-asciidoc-format.md | 0 ...40111-use-log4brains-to-manage-the-adrs.md | 22 ++ ...markdown-architectural-decision-records.md | 42 ++++ .../log4brains/20240113-decision-3.md | 7 + .../log4brains/20240114-decision-4.md | 11 + .../dsl/{adrs => decisions}/workspace.dsl | 8 +- .../AbstractDecisionImporter.java | 72 ++++++ .../AdrToolsDecisionImporter.java | 24 +- .../Log4brainsDecisionImporter.java | 209 ++++++++++++++++ .../AdrToolsDecisionImporterTests.java | 8 +- .../Log4brainsDecisionImporterTests.java | 232 ++++++++++++++++++ .../0001-record-architecture-decisions.md | 0 .../0002-implement-as-shell-scripts.md | 0 .../0003-single-command-with-subcommands.md | 0 .../adrtools}/0004-markdown-format.md | 0 .../adrtools}/0005-help-comments.md | 0 ...n-in-other-version-control-repositories.md | 0 ...-config-executable-to-get-configuration.md | 0 .../0008-use-iso-8601-format-for-dates.md | 0 .../adrtools}/0009-help-scripts.md | 0 ...40111-use-log4brains-to-manage-the-adrs.md | 22 ++ ...markdown-architectural-decision-records.md | 42 ++++ .../log4brains/20240113-decision-3.md | 7 + .../log4brains/20240114-decision-4.md | 11 + 43 files changed, 740 insertions(+), 60 deletions(-) rename structurizr-dsl/src/main/java/com/structurizr/dsl/{AdrsParser.java => DecisionsParser.java} (61%) rename structurizr-dsl/src/test/java/com/structurizr/dsl/{AdrsParserTests.java => DecisionsParserTests.java} (61%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0001-record-architecture-decisions.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0002-implement-as-shell-scripts.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0003-single-command-with-subcommands.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0004-markdown-format.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0005-help-comments.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0006-packaging-and-distribution-in-other-version-control-repositories.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0007-invoke-adr-config-executable-to-get-configuration.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0008-use-iso-8601-format-for-dates.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0009-help-scripts.md (100%) rename structurizr-dsl/src/test/resources/dsl/{adrs/adrs => decisions/adrtools}/0010-asciidoc-format.md (100%) create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240113-decision-3.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240114-decision-4.md rename structurizr-dsl/src/test/resources/dsl/{adrs => decisions}/workspace.dsl (52%) create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/AbstractDecisionImporter.java create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/Log4brainsDecisionImporter.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/documentation/Log4brainsDecisionImporterTests.java rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0001-record-architecture-decisions.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0002-implement-as-shell-scripts.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0003-single-command-with-subcommands.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0004-markdown-format.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0005-help-comments.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0006-packaging-and-distribution-in-other-version-control-repositories.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0007-invoke-adr-config-executable-to-get-configuration.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0008-use-iso-8601-format-for-dates.md (100%) rename structurizr-import/src/test/resources/{adrs => decisions/adrtools}/0009-help-scripts.md (100%) create mode 100644 structurizr-import/src/test/resources/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md create mode 100644 structurizr-import/src/test/resources/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md create mode 100644 structurizr-import/src/test/resources/decisions/log4brains/20240113-decision-3.md create mode 100644 structurizr-import/src/test/resources/decisions/log4brains/20240114-decision-4.md diff --git a/changelog.md b/changelog.md index dc9cdf4bd..40c04c51c 100644 --- a/changelog.md +++ b/changelog.md @@ -5,5 +5,7 @@ - Removes the deprecated concepts (e.g. location and enterprise. - structurizr-core: Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). - structurizr-client: Removes `StructurizrClient` (use `WorkspaceApiClient` instead). +- structurizr-import: Adds support for importing decisions managed by Log4brains. +- structurizr-dsl: Adds `!decisions` as a synonym for `!adrs`. - structurizr-assistant: Initial version. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java index 39e079084..d6e5da57f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java @@ -29,6 +29,8 @@ GroupableElement getElement() { @Override protected String[] getPermittedTokens() { return new String[] { + StructurizrDslTokens.DOCS_TOKEN, + StructurizrDslTokens.DECISIONS_TOKEN, StructurizrDslTokens.DESCRIPTION_TOKEN, StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAGS_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java index 2099a3b15..6a30bf792 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java @@ -36,7 +36,7 @@ GroupableElement getElement() { protected String[] getPermittedTokens() { return new String[] { StructurizrDslTokens.DOCS_TOKEN, - StructurizrDslTokens.ADRS_TOKEN, + StructurizrDslTokens.DECISIONS_TOKEN, StructurizrDslTokens.GROUP_TOKEN, StructurizrDslTokens.COMPONENT_TOKEN, StructurizrDslTokens.DESCRIPTION_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AdrsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java similarity index 61% rename from structurizr-dsl/src/main/java/com/structurizr/dsl/AdrsParser.java rename to structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java index 110e23462..5557d07d0 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AdrsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java @@ -6,15 +6,25 @@ import java.io.File; import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; -final class AdrsParser extends AbstractParser { +final class DecisionsParser extends AbstractParser { - private static final String DEFAULT_DECISION_IMPORTER = "com.structurizr.importer.documentation.AdrToolsDecisionImporter"; + private static final Map DECISION_IMPORTERS = new HashMap<>(); - private static final String GRAMMAR = "!adrs "; + private static final String ADRTOOLS_DECISION_IMPORTER = "adrtools"; + private static final String LOG4BRAINS_DECISION_IMPORTER = "log4brains"; + + static { + DECISION_IMPORTERS.put(ADRTOOLS_DECISION_IMPORTER, "com.structurizr.importer.documentation.AdrToolsDecisionImporter"); + DECISION_IMPORTERS.put(LOG4BRAINS_DECISION_IMPORTER, "com.structurizr.importer.documentation.Log4brainsDecisionImporter"); + } + + private static final String GRAMMAR = "!decisions "; private static final int PATH_INDEX = 1; - private static final int FQN_INDEX = 2; + private static final int TYPE_OR_FQN_INDEX = 2; void parse(WorkspaceDslContext context, File dslFile, Tokens tokens) { parse(context, context.getWorkspace(), dslFile, tokens); @@ -35,7 +45,7 @@ void parse(ComponentDslContext context, File dslFile, Tokens tokens) { private void parse(DslContext context, Documentable documentable, File dslFile, Tokens tokens) { // !adrs - if (tokens.hasMoreThan(FQN_INDEX)) { + if (tokens.hasMoreThan(TYPE_OR_FQN_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); } @@ -43,9 +53,10 @@ private void parse(DslContext context, Documentable documentable, File dslFile, throw new RuntimeException("Expected: " + GRAMMAR); } - String fullyQualifiedClassName = DEFAULT_DECISION_IMPORTER; - if (tokens.includes(FQN_INDEX)) { - fullyQualifiedClassName = tokens.get(FQN_INDEX); + String fullyQualifiedClassName = DECISION_IMPORTERS.get(ADRTOOLS_DECISION_IMPORTER); + if (tokens.includes(TYPE_OR_FQN_INDEX)) { + String typeOrFullyQualifiedName = tokens.get(TYPE_OR_FQN_INDEX); + fullyQualifiedClassName = DECISION_IMPORTERS.getOrDefault(typeOrFullyQualifiedName, typeOrFullyQualifiedName); } if (dslFile != null) { @@ -65,14 +76,14 @@ private void parse(DslContext context, Documentable documentable, File dslFile, DocumentationImporter decisionImporter = (DocumentationImporter)constructor.newInstance(); decisionImporter.importDocumentation(documentable, path); - if (!tokens.includes(FQN_INDEX)) { + if (!tokens.includes(TYPE_OR_FQN_INDEX)) { DefaultImageImporter imageImporter = new DefaultImageImporter(); imageImporter.importDocumentation(documentable, path); } } catch (ClassNotFoundException cnfe) { - throw new RuntimeException("Error importing ADRs from " + path.getAbsolutePath() + ": " + fullyQualifiedClassName + " was not found"); + throw new RuntimeException("Error importing decisions from " + path.getAbsolutePath() + ": " + fullyQualifiedClassName + " was not found"); } catch (Exception e) { - throw new RuntimeException("Error importing ADRs from " + path.getAbsolutePath() + ": " + e.getMessage()); + throw new RuntimeException("Error importing decisions from " + path.getAbsolutePath() + ": " + e.getMessage()); } } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java index e85280d0c..d86cbdaeb 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java @@ -36,7 +36,7 @@ GroupableElement getElement() { protected String[] getPermittedTokens() { return new String[] { StructurizrDslTokens.DOCS_TOKEN, - StructurizrDslTokens.ADRS_TOKEN, + StructurizrDslTokens.DECISIONS_TOKEN, StructurizrDslTokens.GROUP_TOKEN, StructurizrDslTokens.CONTAINER_TOKEN, StructurizrDslTokens.DESCRIPTION_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 8bfa5bc9e..f90a0fb9d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -844,24 +844,24 @@ void parse(List lines, File dslFile) throws StructurizrDslParserExceptio new DocsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); } - } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(WorkspaceDslContext.class)) { if (!restricted) { - new AdrsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); + new DecisionsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); } - } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(SoftwareSystemDslContext.class)) { if (!restricted) { - new AdrsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); + new DecisionsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); } - } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ContainerDslContext.class)) { if (!restricted) { - new AdrsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); + new DecisionsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); } - } else if (ADRS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) { + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ComponentDslContext.class)) { if (!restricted) { - new AdrsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); + new DecisionsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); } } else if (CONSTANT_TOKEN.equalsIgnoreCase(firstToken)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index acae127aa..98e031292 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -97,6 +97,7 @@ class StructurizrDslTokens { static final String INCLUDE_FILE_TOKEN = "!include"; static final String DOCS_TOKEN = "!docs"; static final String ADRS_TOKEN = "!adrs"; + static final String DECISIONS_TOKEN = "!decisions"; static final String CONSTANT_TOKEN = "!constant"; static final String IDENTIFIERS_TOKEN = "!identifiers"; static final String IMPLIED_RELATIONSHIPS_TOKEN = "!impliedRelationships"; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java index 5a5ab7c4e..74fbbe6f8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceDslContext.java @@ -9,7 +9,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.DESCRIPTION_TOKEN, StructurizrDslTokens.PROPERTIES_TOKEN, StructurizrDslTokens.DOCS_TOKEN, - StructurizrDslTokens.ADRS_TOKEN, + StructurizrDslTokens.DECISIONS_TOKEN, StructurizrDslTokens.IDENTIFIERS_TOKEN, StructurizrDslTokens.IMPLIED_RELATIONSHIPS_TOKEN, StructurizrDslTokens.MODEL_TOKEN, diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AdrsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DecisionsParserTests.java similarity index 61% rename from structurizr-dsl/src/test/java/com/structurizr/dsl/AdrsParserTests.java rename to structurizr-dsl/src/test/java/com/structurizr/dsl/DecisionsParserTests.java index 240a3a621..e1c913609 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/AdrsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DecisionsParserTests.java @@ -5,17 +5,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -class AdrsParserTests extends AbstractTests { +class DecisionsParserTests extends AbstractTests { - private AdrsParser parser = new AdrsParser(); + private final DecisionsParser parser = new DecisionsParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(new WorkspaceDslContext(), null, tokens("adrs", "path", "fqn", "extra")); + parser.parse(new WorkspaceDslContext(), null, tokens("decisions", "path", "fqn", "extra")); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: !adrs ", e.getMessage()); + assertEquals("Too many tokens, expected: !decisions ", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index ed76f3eff..4394875a7 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -697,18 +697,21 @@ void test_docs() throws Exception { } @Test - void test_adrs() throws Exception { + void test_decisions() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); - parser.parse(new File("src/test/resources/dsl/adrs/workspace.dsl")); + parser.parse(new File("src/test/resources/dsl/decisions/workspace.dsl")); SoftwareSystem softwareSystem = parser.getWorkspace().getModel().getSoftwareSystemWithName("Software System"); Container container = softwareSystem.getContainerWithName("Container"); Component component = container.getComponentWithName("Component"); + // adrtools decisions assertEquals(10, parser.getWorkspace().getDocumentation().getDecisions().size()); assertEquals(10, softwareSystem.getDocumentation().getDecisions().size()); assertEquals(10, container.getDocumentation().getDecisions().size()); - assertEquals(10, component.getDocumentation().getDecisions().size()); + + // log4brains decisions + assertEquals(4, component.getDocumentation().getDecisions().size()); } @Test @@ -764,7 +767,7 @@ void test_unexpectedTokensInWorkspace() { parser.parse(dslFile); fail(); } catch (StructurizrDslParserException e) { - assertEquals("Unexpected tokens (expected: name, description, properties, !docs, !adrs, !identifiers, !impliedRelationships, model, views, configuration) at line 3 of " + dslFile.getAbsolutePath() + ": softwareSystem \"Name\"", e.getMessage()); + assertEquals("Unexpected tokens (expected: name, description, properties, !docs, !decisions, !identifiers, !impliedRelationships, model, views, configuration) at line 3 of " + dslFile.getAbsolutePath() + ": softwareSystem \"Name\"", e.getMessage()); } } @@ -777,7 +780,7 @@ void test_urlNotPermittedInGroup() { parser.parse(dslFile); fail(); } catch (StructurizrDslParserException e) { - assertEquals("Unexpected tokens (expected: !docs, !adrs, group, container, description, tags, url, properties, perspectives, ->) at line 6 of " + dslFile.getAbsolutePath() + ": url \"https://example.com\"", e.getMessage()); + assertEquals("Unexpected tokens (expected: !docs, !decisions, group, container, description, tags, url, properties, perspectives, ->) at line 6 of " + dslFile.getAbsolutePath() + ": url \"https://example.com\"", e.getMessage()); } } @@ -966,7 +969,7 @@ void test_MultiLineWithError() { fail(); } catch (StructurizrDslParserException e) { // check that the error message includes the original line number - assertEquals("Unexpected tokens (expected: !docs, !adrs, group, container, description, tags, url, properties, perspectives, ->) at line 8 of " + dslFile.getAbsolutePath() + ": component \"Component\" // components not permitted inside software systems", e.getMessage()); + assertEquals("Unexpected tokens (expected: !docs, !decisions, group, container, description, tags, url, properties, perspectives, ->) at line 8 of " + dslFile.getAbsolutePath() + ": component \"Component\" // components not permitted inside software systems", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0001-record-architecture-decisions.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0001-record-architecture-decisions.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0001-record-architecture-decisions.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0001-record-architecture-decisions.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0002-implement-as-shell-scripts.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0002-implement-as-shell-scripts.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0002-implement-as-shell-scripts.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0002-implement-as-shell-scripts.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0003-single-command-with-subcommands.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0003-single-command-with-subcommands.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0003-single-command-with-subcommands.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0003-single-command-with-subcommands.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0004-markdown-format.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0004-markdown-format.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0004-markdown-format.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0004-markdown-format.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0005-help-comments.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0005-help-comments.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0005-help-comments.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0005-help-comments.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0006-packaging-and-distribution-in-other-version-control-repositories.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0006-packaging-and-distribution-in-other-version-control-repositories.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0007-invoke-adr-config-executable-to-get-configuration.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0007-invoke-adr-config-executable-to-get-configuration.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0007-invoke-adr-config-executable-to-get-configuration.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0007-invoke-adr-config-executable-to-get-configuration.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0008-use-iso-8601-format-for-dates.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0008-use-iso-8601-format-for-dates.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0008-use-iso-8601-format-for-dates.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0008-use-iso-8601-format-for-dates.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0009-help-scripts.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0009-help-scripts.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0009-help-scripts.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0009-help-scripts.md diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/adrs/0010-asciidoc-format.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0010-asciidoc-format.md similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/adrs/adrs/0010-asciidoc-format.md rename to structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0010-asciidoc-format.md diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md new file mode 100644 index 000000000..26cceeb7a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md @@ -0,0 +1,22 @@ +# Use Log4brains to manage the ADRs + +- Status: accepted +- Date: 2024-01-10 +- Tags: dev-tools, doc + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which tool(s) should we use to manage these records? + +## Considered Options + +- [Log4brains](https://github.com/thomvaill/log4brains): architecture knowledge base (command-line + static site generator) +- [ADR Tools](https://github.com/npryce/adr-tools): command-line to create ADRs +- [ADR Tools Python](https://bitbucket.org/tinkerer_/adr-tools-python/src/master/): command-line to create ADRs +- [adr-viewer](https://github.com/mrwilson/adr-viewer): static site generator +- [adr-log](https://adr.github.io/adr-log/): command-line to create a TOC of ADRs + +## Decision Outcome + +Chosen option: "Log4brains", because it includes the features of all the other tools, and even more. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md new file mode 100644 index 000000000..f5ee1949a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md @@ -0,0 +1,42 @@ +# Use Markdown Architectural Decision Records + +- Status: accepted +- Date: 2024-01-10 +- Tags: doc + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? + +## Considered Options + +- [MADR](https://adr.github.io/madr/) 2.1.2 with Log4brains patch +- [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records +- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements +- Other templates listed at +- Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.2 with Log4brains patch", because + +- Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +- The MADR format is lean and fits our development style. +- The MADR structure is comprehensible and facilitates usage & maintenance. +- The MADR project is vivid. +- Version 2.1.2 is the latest one available when starting to document ADRs. +- The Log4brains patch adds more features, like tags. + +The "Log4brains patch" performs the following modifications to the original template: + +- Change the ADR filenames format (`NNN-adr-name` becomes `YYYYMMDD-adr-name`), to avoid conflicts during Git merges. +- Add a `draft` status, to enable collaborative writing. +- Add a `Tags` field. + +## Links + +- Relates to [Use Log4brains to manage the ADRs](20240111-use-log4brains-to-manage-the-adrs.md) diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240113-decision-3.md b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240113-decision-3.md new file mode 100644 index 000000000..4a2c40918 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240113-decision-3.md @@ -0,0 +1,7 @@ +# Decision 3 + +- Status: superseded by [20240111-decision-4](20240114-decision-4.md) + +## Context and Problem Statement + +Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240114-decision-4.md b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240114-decision-4.md new file mode 100644 index 000000000..bff063a29 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/log4brains/20240114-decision-4.md @@ -0,0 +1,11 @@ +# Decision 4 + +- Status: accepted + +## Context and Problem Statement + +Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question. + +## Links + +- Supersedes [20240111-decision-3](20240113-decision-3.md) diff --git a/structurizr-dsl/src/test/resources/dsl/adrs/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/decisions/workspace.dsl similarity index 52% rename from structurizr-dsl/src/test/resources/dsl/adrs/workspace.dsl rename to structurizr-dsl/src/test/resources/dsl/decisions/workspace.dsl index a27b08488..df35859e3 100644 --- a/structurizr-dsl/src/test/resources/dsl/adrs/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/decisions/workspace.dsl @@ -1,16 +1,16 @@ workspace { - !adrs adrs com.structurizr.example.ExampleDecisionImporter + !adrs adrtools com.structurizr.example.ExampleDecisionImporter model { softwareSystem = softwareSystem "Software System" { - !adrs adrs + !decisions adrtools container "Container" { - !adrs adrs + !decisions adrtools adrtools component "Component" { - !adrs adrs + !decisions log4brains log4brains } } } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/AbstractDecisionImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/AbstractDecisionImporter.java new file mode 100644 index 000000000..9d02ebea9 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/AbstractDecisionImporter.java @@ -0,0 +1,72 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Format; +import com.structurizr.util.StringUtils; + +import java.io.File; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Imports architecture decision records created/managed by adr-tools (https://github.com/npryce/adr-tools). + * The format for ADRs is as follows: + * + * Filename: {DECISION_ID:0000}-*.md + * + * Content: + * # {DECISION_ID}. {DECISION_TITLE} + * + * Date: {DECISION_DATE:YYYY-MM-DD} + * + * ## Status + * + * {DECISION_STATUS and links} + * + * ## Context + * ... + */ +public abstract class AbstractDecisionImporter implements DocumentationImporter { + + protected TimeZone timeZone = TimeZone.getDefault(); + protected Charset characterEncoding = StandardCharsets.UTF_8; + + /** + * Sets the time zone to use when parsing dates (the default is UTC) + * + * @param timeZone a time zone as a String (e.g. "Europe/London" or "UTC") + */ + public void setTimeZone(String timeZone) { + this.timeZone = TimeZone.getTimeZone(timeZone); + } + + /** + * Sets the time zone to use when parsing dates. + * + * @param timeZone a TimeZone instance + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Provides a way to change the character encoding used by the DSL parser. + * + * @param characterEncoding a Charset instance + */ + public void setCharacterEncoding(Charset characterEncoding) { + if (characterEncoding == null) { + throw new IllegalArgumentException("A character encoding must be specified"); + } + + this.characterEncoding = characterEncoding; + } + +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java index 0a14e90e3..d159bde45 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java @@ -7,6 +7,7 @@ import java.io.File; import java.net.URLEncoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.text.SimpleDateFormat; @@ -32,7 +33,7 @@ * ## Context * ... */ -public class AdrToolsDecisionImporter implements DocumentationImporter { +public class AdrToolsDecisionImporter extends AbstractDecisionImporter { private static final String STATUS_PROPOSED = "Proposed"; private static final String STATUS_SUPERSEDED = "Superseded"; @@ -45,7 +46,6 @@ public class AdrToolsDecisionImporter implements DocumentationImporter { private static final Pattern LINK_REGEX = Pattern.compile("(.*) \\[.*]\\((.*)\\)"); private String dateFormat = "yyyy-MM-dd"; - private TimeZone timeZone = TimeZone.getDefault(); /** * Sets the date format to use when parsing dates (the default is "yyyy-MM-dd"). @@ -56,24 +56,6 @@ public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } - /** - * Sets the time zone to use when parsing dates (the default is UTC) - * - * @param timeZone a time zone as a String (e.g. "Europe/London" or "UTC") - */ - public void setTimeZone(String timeZone) { - this.timeZone = TimeZone.getTimeZone(timeZone); - } - - /** - * Sets the time zone to use when parsing dates. - * - * @param timeZone a TimeZone instance - */ - public void setTimeZone(TimeZone timeZone) { - this.timeZone = timeZone; - } - /** * Imports Markdown files from the specified path, one per decision. * @@ -131,7 +113,7 @@ protected Decision importDecision(File file) throws Exception { String id = extractIntegerIDFromFileName(file); Decision decision = new Decision(id); - String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + String content = Files.readString(file.toPath(), characterEncoding); content = content.replace("\r", ""); decision.setContent(content); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/documentation/Log4brainsDecisionImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/Log4brainsDecisionImporter.java new file mode 100644 index 000000000..b6bc324f8 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/Log4brainsDecisionImporter.java @@ -0,0 +1,209 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Format; + +import java.io.File; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Imports architecture decision records created/managed by Log4brains (https://github.com/thomvaill/log4brains). + * See https://github.com/thomvaill/log4brains/blob/master/docs/adr/template.md for the template. + */ +public class Log4brainsDecisionImporter extends AbstractDecisionImporter { + + private static final String DATE_PREFIX = "- Date: "; + private static final String STATUS_PREFIX = "- Status: "; + private static final Pattern STATUS_LINK_REGEX = Pattern.compile("- Status: (.*) \\[.*]\\((.*)\\)"); + private static final String SUPERSEDED = "superseded"; + private static final String LINKS_HEADING = "## Links"; + + private static final Pattern LINK_REGEX = Pattern.compile("- (.*) \\[.*]\\((.*)\\)"); + + private static final String DATE_FORMAT_IN_FILENAME = "yyyyMMdd"; + private static final String DATE_FORMAT_IN_CONTENT = "yyyy-MM-dd"; + + /** + * Imports Markdown files from the specified path, one per decision. + * + * @param documentable the item that documentation should be associated with + * @param path the path to import documentation from + */ + @Override + public void importDocumentation(Documentable documentable, File path) { + if (documentable == null) { + throw new IllegalArgumentException("A workspace, software system, container, or component must be specified."); + } + + if (path == null) { + throw new IllegalArgumentException("A path must be specified."); + } else if (!path.exists()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " does not exist."); + } + + if (!path.isDirectory()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " is not a directory."); + } + + try { + Map decisionsById = new LinkedHashMap<>(); + + File[] markdownFiles = path.listFiles((dir, name) -> name.matches("\\d{4}\\d{2}\\d{2}-.+?.md")); + if (markdownFiles != null) { + Map decisionsByFilename = new HashMap<>(); + + Arrays.sort(markdownFiles, Comparator.comparing(File::getName)); + + int decisionId = 1; + for (File file : markdownFiles) { + Decision decision = importDecision(decisionId, file); + documentable.getDocumentation().addDecision(decision); + + decisionsById.put(decision.getId(), decision); + decisionsByFilename.put(file.getName(), decision); + decisionId++; + } + + for (Decision decision : decisionsById.values()) { + extractLinks(decision, decisionsByFilename); + + // and replace file references, for example "0008-some-decision.md" -> "#8" + String content = decision.getContent(); + for (String filename : decisionsByFilename.keySet()) { + content = content.replace(filename, calculateUrl(decisionsByFilename.get(filename))); + } + decision.setContent(content); + } + } + } catch (Exception e) { + throw new DocumentationImportException(e); + } + } + + protected Decision importDecision(int id, File file) throws Exception { + Decision decision = new Decision("" + id); + + String content = Files.readString(file.toPath(), characterEncoding); + content = content.replace("\r", ""); + decision.setContent(content); + + String[] lines = content.split("\\n"); + decision.setTitle(extractTitle(lines)); + + decision.setDate(extractDateFromFilename(file)); + Date dateFromContent = extractDate(lines); + if (dateFromContent != null) { + decision.setDate(dateFromContent); + } + + decision.setStatus(extractStatus(lines)); + decision.setFormat(Format.Markdown); + + return decision; + } + + protected Date extractDateFromFilename(File file) throws Exception { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_IN_FILENAME); + sdf.setTimeZone(timeZone); + + return sdf.parse(file.getName().substring(0, DATE_FORMAT_IN_FILENAME.length())); + } + + protected String extractTitle(String[] lines) { + // the title is assumed to be the first line of the content, in the format: + // # {DECISION_TITLE} + String titleLine = lines[0]; + + return titleLine.substring(2); + } + + protected Date extractDate(String[] lines) throws Exception { + // the date can optionally be on a line of its own, in the format: + // - Date: {DECISION_DATE:YYYY-MM-DD} + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_IN_CONTENT); + sdf.setTimeZone(timeZone); + + for (String line : lines) { + if (line.startsWith(DATE_PREFIX)) { + String dateAsString = line.substring(DATE_PREFIX.length()); + + return sdf.parse(dateAsString); + } + } + + return null; + } + + protected String extractStatus(String[] lines) { + // the date is on a line of its own, in the format: + // - Status: {DECISION_STATUS} + for (String line : lines) { + if (line.startsWith(STATUS_PREFIX)) { + String status = line.substring(STATUS_PREFIX.length()); + if (status.startsWith(SUPERSEDED)) { + // superseded by [slug](filename) + return SUPERSEDED; + } else { + return status; + } + } + } + + return ""; + } + + protected void extractLinks(Decision decision, Map decisionsByFilename) { + // extracts links from: + // 1. the status line + // 2. the final ## Links section (if present) + boolean inLinksSection = false; + String[] lines = decision.getContent().split("\\n"); + for (String line : lines) { + if (line.startsWith(STATUS_PREFIX)) { + Matcher matcher = STATUS_LINK_REGEX.matcher(line); + if (matcher.find()) { + String linkDescription = matcher.group(1); + String markdownFile = matcher.group(2); + + Decision targetDecision = decisionsByFilename.get(markdownFile); + if (targetDecision != null) { + decision.addLink(targetDecision, linkDescription); + } + } + } + + if (line.startsWith(LINKS_HEADING)) { + inLinksSection = true; + } + + if (inLinksSection) { + Matcher matcher = LINK_REGEX.matcher(line); + if (matcher.find()) { + String linkDescription = matcher.group(1); + String markdownFile = matcher.group(2); + + Decision targetDecision = decisionsByFilename.get(markdownFile); + if (targetDecision != null) { + decision.addLink(targetDecision, linkDescription); + } + } + } + } + } + + protected String calculateUrl(Decision decision) throws Exception { + return "#" + urlEncode(decision.getId()); + } + + protected String urlEncode(String value) throws Exception { + return URLEncoder.encode(value, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20"); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java index 8b31bbbc0..099c1edbb 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java @@ -17,6 +17,8 @@ public class AdrToolsDecisionImporterTests { + private static final File DECISIONS_FOLDER = new File("./src/test/resources/decisions/adrtools"); + private AdrToolsDecisionImporter decisionImporter; private Workspace workspace; private Documentation documentation; @@ -72,7 +74,7 @@ public void test_importDocumentation_ThrowsAnException_WhenAPathIsSpecifiedButIt @Test public void test_importDecisions() { - decisionImporter.importDocumentation(workspace, new File("./src/test/resources/adrs")); + decisionImporter.importDocumentation(workspace, DECISIONS_FOLDER); assertEquals(9, documentation.getDecisions().size()); @@ -108,7 +110,7 @@ public void test_importDecisions() { @Test public void test_importDocumentation_CapturesLinksBetweenDecisions() { - decisionImporter.importDocumentation(workspace, new File("./src/test/resources/adrs")); + decisionImporter.importDocumentation(workspace, DECISIONS_FOLDER); Decision decision5 = documentation.getDecisions().stream().filter(d -> d.getId().equals("5")).findFirst().get(); assertEquals(1, decision5.getLinks().size()); @@ -119,7 +121,7 @@ public void test_importDocumentation_CapturesLinksBetweenDecisions() { @Test public void test_importDocumentation_RewritesLinksBetweenDecisions() { - decisionImporter.importDocumentation(workspace, new File("./src/test/resources/adrs")); + decisionImporter.importDocumentation(workspace, DECISIONS_FOLDER); Decision decision5 = documentation.getDecisions().stream().filter(d -> d.getId().equals("5")).findFirst().get(); assertTrue(decision5.getContent().contains("Amended by [9. Help scripts](#9)")); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/documentation/Log4brainsDecisionImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/Log4brainsDecisionImporterTests.java new file mode 100644 index 000000000..56d95c6d1 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/Log4brainsDecisionImporterTests.java @@ -0,0 +1,232 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Documentation; +import com.structurizr.documentation.Format; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import static org.junit.jupiter.api.Assertions.*; + + +public class Log4brainsDecisionImporterTests { + + private static final File DECISIONS_FOLDER = new File("./src/test/resources/decisions/log4brains"); + + private Log4brainsDecisionImporter decisionImporter; + private Workspace workspace; + private Documentation documentation; + + @BeforeEach + public void setUp() { + decisionImporter = new Log4brainsDecisionImporter(); + workspace = new Workspace("Name", "Description"); + documentation = workspace.getDocumentation(); + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenANullDocumentableIsSpecified() { + try { + decisionImporter.importDocumentation(null, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A workspace, software system, container, or component must be specified.", iae.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenADirectoryIsNotSpecified() { + try { + decisionImporter.importDocumentation(workspace, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A path must be specified.", iae.getMessage()); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenADirectoryIsSpecifiedButItDoesNotExist() { + try { + File directory = new File("foo"); + assertFalse(directory.exists()); + decisionImporter.importDocumentation(workspace, directory); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("foo does not exist.")); + } + } + + @Test + public void test_importDocumentation_ThrowsAnException_WhenAPathIsSpecifiedButItIsNotADirectory() { + try { + decisionImporter.importDocumentation(workspace, new File("build.gradle")); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("/build.gradle is not a directory.")); + } + } + + @Test + public void test_importDecisions() { + decisionImporter.importDocumentation(workspace, DECISIONS_FOLDER); + SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + + assertEquals(4, documentation.getDecisions().size()); + + // I think these first two decisions are found in the wrong order, which is a consequence of Log4brains not having decision IDs + + Decision decision1 = documentation.getDecisions().stream().filter(d -> d.getId().equals("1")).findFirst().get(); + assertEquals("1", decision1.getId()); + assertEquals("Use Log4brains to manage the ADRs", decision1.getTitle()); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + assertEquals("10-Jan-2024", sdf.format(decision1.getDate())); + assertEquals("accepted", decision1.getStatus()); + Assertions.assertEquals(Format.Markdown, decision1.getFormat()); + assertEquals(""" +# Use Log4brains to manage the ADRs + +- Status: accepted +- Date: 2024-01-10 +- Tags: dev-tools, doc + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which tool(s) should we use to manage these records? + +## Considered Options + +- [Log4brains](https://github.com/thomvaill/log4brains): architecture knowledge base (command-line + static site generator) +- [ADR Tools](https://github.com/npryce/adr-tools): command-line to create ADRs +- [ADR Tools Python](https://bitbucket.org/tinkerer_/adr-tools-python/src/master/): command-line to create ADRs +- [adr-viewer](https://github.com/mrwilson/adr-viewer): static site generator +- [adr-log](https://adr.github.io/adr-log/): command-line to create a TOC of ADRs + +## Decision Outcome + +Chosen option: "Log4brains", because it includes the features of all the other tools, and even more. +""", + decision1.getContent()); + + Decision decision2 = documentation.getDecisions().stream().filter(d -> d.getId().equals("2")).findFirst().get(); + assertEquals("2", decision2.getId()); + assertEquals("Use Markdown Architectural Decision Records", decision2.getTitle()); + assertEquals("10-Jan-2024", sdf.format(decision2.getDate())); + assertEquals("accepted", decision2.getStatus()); + Assertions.assertEquals(Format.Markdown, decision2.getFormat()); + assertEquals(""" +# Use Markdown Architectural Decision Records + +- Status: accepted +- Date: 2024-01-10 +- Tags: doc + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? + +## Considered Options + +- [MADR](https://adr.github.io/madr/) 2.1.2 with Log4brains patch +- [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records +- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements +- Other templates listed at +- Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.2 with Log4brains patch", because + +- Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +- The MADR format is lean and fits our development style. +- The MADR structure is comprehensible and facilitates usage & maintenance. +- The MADR project is vivid. +- Version 2.1.2 is the latest one available when starting to document ADRs. +- The Log4brains patch adds more features, like tags. + +The "Log4brains patch" performs the following modifications to the original template: + +- Change the ADR filenames format (`NNN-adr-name` becomes `YYYYMMDD-adr-name`), to avoid conflicts during Git merges. +- Add a `draft` status, to enable collaborative writing. +- Add a `Tags` field. + +## Links + +- Relates to [Use Log4brains to manage the ADRs](#1) +""", + decision2.getContent()); + + Decision decision3 = documentation.getDecisions().stream().filter(d -> d.getId().equals("3")).findFirst().get(); + assertEquals("3", decision3.getId()); + assertEquals("Decision 3", decision3.getTitle()); + assertEquals("13-Jan-2024", sdf.format(decision3.getDate())); + assertEquals("superseded", decision3.getStatus()); + Assertions.assertEquals(Format.Markdown, decision3.getFormat()); + assertEquals(""" +# Decision 3 + +- Status: superseded by [20240111-decision-4](#4) + +## Context and Problem Statement + +Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question. +""", + decision3.getContent()); + + Decision decision4 = documentation.getDecisions().stream().filter(d -> d.getId().equals("4")).findFirst().get(); + assertEquals("4", decision4.getId()); + assertEquals("Decision 4", decision4.getTitle()); + assertEquals("14-Jan-2024", sdf.format(decision4.getDate())); + assertEquals("accepted", decision4.getStatus()); + Assertions.assertEquals(Format.Markdown, decision4.getFormat()); + assertEquals(""" +# Decision 4 + +- Status: accepted + +## Context and Problem Statement + +Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question. + +## Links + +- Supersedes [20240111-decision-3](#3) +""", + decision4.getContent()); + } + + @Test + public void test_importDocumentation_CapturesLinksBetweenDecisions() { + decisionImporter.importDocumentation(workspace, DECISIONS_FOLDER); + + Decision decision2 = documentation.getDecisions().stream().filter(d -> d.getId().equals("2")).findFirst().get(); + assertEquals(1, decision2.getLinks().size()); + Decision.Link link = decision2.getLinks().iterator().next(); + assertEquals("1", link.getId()); + assertEquals("Relates to", link.getDescription()); + + Decision decision3 = documentation.getDecisions().stream().filter(d -> d.getId().equals("3")).findFirst().get(); + assertEquals(1, decision3.getLinks().size()); + link = decision3.getLinks().iterator().next(); + assertEquals("4", link.getId()); + assertEquals("superseded by", link.getDescription()); + + Decision decision4 = documentation.getDecisions().stream().filter(d -> d.getId().equals("4")).findFirst().get(); + assertEquals(1, decision4.getLinks().size()); + link = decision4.getLinks().iterator().next(); + assertEquals("3", link.getId()); + assertEquals("Supersedes", link.getDescription()); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/resources/adrs/0001-record-architecture-decisions.md b/structurizr-import/src/test/resources/decisions/adrtools/0001-record-architecture-decisions.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0001-record-architecture-decisions.md rename to structurizr-import/src/test/resources/decisions/adrtools/0001-record-architecture-decisions.md diff --git a/structurizr-import/src/test/resources/adrs/0002-implement-as-shell-scripts.md b/structurizr-import/src/test/resources/decisions/adrtools/0002-implement-as-shell-scripts.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0002-implement-as-shell-scripts.md rename to structurizr-import/src/test/resources/decisions/adrtools/0002-implement-as-shell-scripts.md diff --git a/structurizr-import/src/test/resources/adrs/0003-single-command-with-subcommands.md b/structurizr-import/src/test/resources/decisions/adrtools/0003-single-command-with-subcommands.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0003-single-command-with-subcommands.md rename to structurizr-import/src/test/resources/decisions/adrtools/0003-single-command-with-subcommands.md diff --git a/structurizr-import/src/test/resources/adrs/0004-markdown-format.md b/structurizr-import/src/test/resources/decisions/adrtools/0004-markdown-format.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0004-markdown-format.md rename to structurizr-import/src/test/resources/decisions/adrtools/0004-markdown-format.md diff --git a/structurizr-import/src/test/resources/adrs/0005-help-comments.md b/structurizr-import/src/test/resources/decisions/adrtools/0005-help-comments.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0005-help-comments.md rename to structurizr-import/src/test/resources/decisions/adrtools/0005-help-comments.md diff --git a/structurizr-import/src/test/resources/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md b/structurizr-import/src/test/resources/decisions/adrtools/0006-packaging-and-distribution-in-other-version-control-repositories.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0006-packaging-and-distribution-in-other-version-control-repositories.md rename to structurizr-import/src/test/resources/decisions/adrtools/0006-packaging-and-distribution-in-other-version-control-repositories.md diff --git a/structurizr-import/src/test/resources/adrs/0007-invoke-adr-config-executable-to-get-configuration.md b/structurizr-import/src/test/resources/decisions/adrtools/0007-invoke-adr-config-executable-to-get-configuration.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0007-invoke-adr-config-executable-to-get-configuration.md rename to structurizr-import/src/test/resources/decisions/adrtools/0007-invoke-adr-config-executable-to-get-configuration.md diff --git a/structurizr-import/src/test/resources/adrs/0008-use-iso-8601-format-for-dates.md b/structurizr-import/src/test/resources/decisions/adrtools/0008-use-iso-8601-format-for-dates.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0008-use-iso-8601-format-for-dates.md rename to structurizr-import/src/test/resources/decisions/adrtools/0008-use-iso-8601-format-for-dates.md diff --git a/structurizr-import/src/test/resources/adrs/0009-help-scripts.md b/structurizr-import/src/test/resources/decisions/adrtools/0009-help-scripts.md similarity index 100% rename from structurizr-import/src/test/resources/adrs/0009-help-scripts.md rename to structurizr-import/src/test/resources/decisions/adrtools/0009-help-scripts.md diff --git a/structurizr-import/src/test/resources/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md b/structurizr-import/src/test/resources/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md new file mode 100644 index 000000000..26cceeb7a --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/log4brains/20240111-use-log4brains-to-manage-the-adrs.md @@ -0,0 +1,22 @@ +# Use Log4brains to manage the ADRs + +- Status: accepted +- Date: 2024-01-10 +- Tags: dev-tools, doc + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which tool(s) should we use to manage these records? + +## Considered Options + +- [Log4brains](https://github.com/thomvaill/log4brains): architecture knowledge base (command-line + static site generator) +- [ADR Tools](https://github.com/npryce/adr-tools): command-line to create ADRs +- [ADR Tools Python](https://bitbucket.org/tinkerer_/adr-tools-python/src/master/): command-line to create ADRs +- [adr-viewer](https://github.com/mrwilson/adr-viewer): static site generator +- [adr-log](https://adr.github.io/adr-log/): command-line to create a TOC of ADRs + +## Decision Outcome + +Chosen option: "Log4brains", because it includes the features of all the other tools, and even more. diff --git a/structurizr-import/src/test/resources/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md b/structurizr-import/src/test/resources/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md new file mode 100644 index 000000000..f5ee1949a --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/log4brains/20240111-use-markdown-architectural-decision-records.md @@ -0,0 +1,42 @@ +# Use Markdown Architectural Decision Records + +- Status: accepted +- Date: 2024-01-10 +- Tags: doc + +## Context and Problem Statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? + +## Considered Options + +- [MADR](https://adr.github.io/madr/) 2.1.2 with Log4brains patch +- [MADR](https://adr.github.io/madr/) 2.1.2 – The original Markdown Architectural Decision Records +- [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +- [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements +- Other templates listed at +- Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.2 with Log4brains patch", because + +- Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +- The MADR format is lean and fits our development style. +- The MADR structure is comprehensible and facilitates usage & maintenance. +- The MADR project is vivid. +- Version 2.1.2 is the latest one available when starting to document ADRs. +- The Log4brains patch adds more features, like tags. + +The "Log4brains patch" performs the following modifications to the original template: + +- Change the ADR filenames format (`NNN-adr-name` becomes `YYYYMMDD-adr-name`), to avoid conflicts during Git merges. +- Add a `draft` status, to enable collaborative writing. +- Add a `Tags` field. + +## Links + +- Relates to [Use Log4brains to manage the ADRs](20240111-use-log4brains-to-manage-the-adrs.md) diff --git a/structurizr-import/src/test/resources/decisions/log4brains/20240113-decision-3.md b/structurizr-import/src/test/resources/decisions/log4brains/20240113-decision-3.md new file mode 100644 index 000000000..4a2c40918 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/log4brains/20240113-decision-3.md @@ -0,0 +1,7 @@ +# Decision 3 + +- Status: superseded by [20240111-decision-4](20240114-decision-4.md) + +## Context and Problem Statement + +Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question. diff --git a/structurizr-import/src/test/resources/decisions/log4brains/20240114-decision-4.md b/structurizr-import/src/test/resources/decisions/log4brains/20240114-decision-4.md new file mode 100644 index 000000000..bff063a29 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/log4brains/20240114-decision-4.md @@ -0,0 +1,11 @@ +# Decision 4 + +- Status: accepted + +## Context and Problem Statement + +Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question. + +## Links + +- Supersedes [20240111-decision-3](20240113-decision-3.md) From bfa8bbeb041bffd4db8bc543a32e8dfddbf0fd31 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 11 Jan 2024 14:28:10 +0000 Subject: [PATCH 153/418] Adds support for MADR - closes #222. --- .../com/structurizr/dsl/DecisionsParser.java | 2 + .../java/com/structurizr/dsl/DslTests.java | 4 +- .../0000-use-markdown-any-decision-records.md | 30 +++ .../madr/0001-use-CC0-or-MIT-as-license.md | 49 ++++ .../0002-do-not-use-numbers-in-headings.md | 24 ++ .../madr/0003-provide-own-madr-tools.md | 33 +++ .../decisions/madr/0004-write-own-toc-tool.md | 29 +++ .../madr/0005-use-dashes-in-filenames.md | 26 ++ .../madr/0006-use-names-as-identifier.md | 24 ++ .../0007-do-not-emphasize-line-headings.md | 23 ++ .../decisions/madr/0008-add-status-field.md | 100 ++++++++ .../dsl/decisions/madr/0008-example-badge.png | Bin 0 -> 2073 bytes .../madr/0008-example-separate-heading.png | Bin 0 -> 2141 bytes .../dsl/decisions/madr/0008-example-table.png | Bin 0 -> 1334 bytes ...pport-links-between-adrs-inside-an-adrs.md | 79 ++++++ .../decisions/madr/0010-support-categories.md | 106 ++++++++ .../madr/0011-use-asterisk-as-list-marker.md | 20 ++ ...-use-curly-braces-to-denote-placeholder.md | 46 ++++ .../dsl/decisions/madr/0013-example.png | Bin 0 -> 23190 bytes ...013-use-yaml-front-matter-for-meta-data.md | 65 +++++ .../madr/0014-allow-neutral-arguments.md | 62 +++++ ...015-include-consulting-informed-of-raci.md | 44 ++++ .../0016-outcome-before-detailed-pros-cons.md | 74 ++++++ ...se-same-format-for-outcomes-and-options.md | 26 ++ .../madr/0018-use-confirmation-as-heading.md | 30 +++ .../resources/dsl/decisions/workspace.dsl | 2 +- .../AdrToolsDecisionImporter.java | 8 +- .../Log4brainsDecisionImporter.java | 8 +- .../documentation/MadrDecisionImporter.java | 172 +++++++++++++ .../MadrDecisionImporterTests.java | 237 ++++++++++++++++++ .../0000-use-markdown-any-decision-records.md | 30 +++ .../madr/0001-use-CC0-or-MIT-as-license.md | 49 ++++ .../0002-do-not-use-numbers-in-headings.md | 24 ++ .../madr/0003-provide-own-madr-tools.md | 33 +++ .../decisions/madr/0004-write-own-toc-tool.md | 29 +++ .../madr/0005-use-dashes-in-filenames.md | 26 ++ .../madr/0006-use-names-as-identifier.md | 24 ++ .../0007-do-not-emphasize-line-headings.md | 23 ++ .../decisions/madr/0008-add-status-field.md | 100 ++++++++ .../decisions/madr/0008-example-badge.png | Bin 0 -> 2073 bytes .../madr/0008-example-separate-heading.png | Bin 0 -> 2141 bytes .../decisions/madr/0008-example-table.png | Bin 0 -> 1334 bytes ...pport-links-between-adrs-inside-an-adrs.md | 79 ++++++ .../decisions/madr/0010-support-categories.md | 106 ++++++++ .../madr/0011-use-asterisk-as-list-marker.md | 20 ++ ...-use-curly-braces-to-denote-placeholder.md | 46 ++++ .../resources/decisions/madr/0013-example.png | Bin 0 -> 23190 bytes ...013-use-yaml-front-matter-for-meta-data.md | 65 +++++ .../madr/0014-allow-neutral-arguments.md | 62 +++++ ...015-include-consulting-informed-of-raci.md | 44 ++++ .../0016-outcome-before-detailed-pros-cons.md | 74 ++++++ ...se-same-format-for-outcomes-and-options.md | 26 ++ .../madr/0018-use-confirmation-as-heading.md | 30 +++ 53 files changed, 2203 insertions(+), 10 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0000-use-markdown-any-decision-records.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0001-use-CC0-or-MIT-as-license.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0002-do-not-use-numbers-in-headings.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0003-provide-own-madr-tools.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0004-write-own-toc-tool.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0005-use-dashes-in-filenames.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0006-use-names-as-identifier.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0007-do-not-emphasize-line-headings.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-add-status-field.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-badge.png create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-separate-heading.png create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-table.png create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0010-support-categories.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0011-use-asterisk-as-list-marker.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0012-use-curly-braces-to-denote-placeholder.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-example.png create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0014-allow-neutral-arguments.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0015-include-consulting-informed-of-raci.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0016-outcome-before-detailed-pros-cons.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0017-use-same-format-for-outcomes-and-options.md create mode 100644 structurizr-dsl/src/test/resources/dsl/decisions/madr/0018-use-confirmation-as-heading.md create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/documentation/MadrDecisionImporter.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/documentation/MadrDecisionImporterTests.java create mode 100644 structurizr-import/src/test/resources/decisions/madr/0000-use-markdown-any-decision-records.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0001-use-CC0-or-MIT-as-license.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0002-do-not-use-numbers-in-headings.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0003-provide-own-madr-tools.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0004-write-own-toc-tool.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0005-use-dashes-in-filenames.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0006-use-names-as-identifier.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0007-do-not-emphasize-line-headings.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0008-add-status-field.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0008-example-badge.png create mode 100644 structurizr-import/src/test/resources/decisions/madr/0008-example-separate-heading.png create mode 100644 structurizr-import/src/test/resources/decisions/madr/0008-example-table.png create mode 100644 structurizr-import/src/test/resources/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0010-support-categories.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0011-use-asterisk-as-list-marker.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0012-use-curly-braces-to-denote-placeholder.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0013-example.png create mode 100644 structurizr-import/src/test/resources/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0014-allow-neutral-arguments.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0015-include-consulting-informed-of-raci.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0016-outcome-before-detailed-pros-cons.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0017-use-same-format-for-outcomes-and-options.md create mode 100644 structurizr-import/src/test/resources/decisions/madr/0018-use-confirmation-as-heading.md diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java index 5557d07d0..0db52d27a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java @@ -15,9 +15,11 @@ final class DecisionsParser extends AbstractParser { private static final String ADRTOOLS_DECISION_IMPORTER = "adrtools"; private static final String LOG4BRAINS_DECISION_IMPORTER = "log4brains"; + private static final String MADR_DECISION_IMPORTER = "madr"; static { DECISION_IMPORTERS.put(ADRTOOLS_DECISION_IMPORTER, "com.structurizr.importer.documentation.AdrToolsDecisionImporter"); + DECISION_IMPORTERS.put(MADR_DECISION_IMPORTER, "com.structurizr.importer.documentation.MadrDecisionImporter"); DECISION_IMPORTERS.put(LOG4BRAINS_DECISION_IMPORTER, "com.structurizr.importer.documentation.Log4brainsDecisionImporter"); } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 4394875a7..3d06a9661 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -708,7 +708,9 @@ void test_decisions() throws Exception { // adrtools decisions assertEquals(10, parser.getWorkspace().getDocumentation().getDecisions().size()); assertEquals(10, softwareSystem.getDocumentation().getDecisions().size()); - assertEquals(10, container.getDocumentation().getDecisions().size()); + + // madr decisions + assertEquals(19, container.getDocumentation().getDecisions().size()); // log4brains decisions assertEquals(4, component.getDocumentation().getDecisions().size()); diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0000-use-markdown-any-decision-records.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0000-use-markdown-any-decision-records.md new file mode 100644 index 000000000..df5c4f9d0 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0000-use-markdown-any-decision-records.md @@ -0,0 +1,30 @@ +--- +parent: Decisions +nav_order: 0 +--- +# Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 3.0.0 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) – The Y-Statements +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 3.0.0", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* MADR allows for structured capturing of any decision. +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0001-use-CC0-or-MIT-as-license.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0001-use-CC0-or-MIT-as-license.md new file mode 100644 index 000000000..09ec7aaf0 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0001-use-CC0-or-MIT-as-license.md @@ -0,0 +1,49 @@ +--- +parent: Decisions +nav_order: 1 +--- +# Dual License the Work + +## Context and Problem Statement + +Everything needs to be licensed, otherwise the default copyright laws apply. +For instance, in Germany that means users may not alter anything without explicitly asking for permission. +For more information see . + +We want to have MADR used without any hassle and that users can just go ahead and write MADRs. + +## Considered Options + +* [CC0](https://creativecommons.org/share-your-work/public-domain/cc0/) +* BSD3 +* MIT +* Dual license with MIT and CC0 +* No license +* Other open source licenses + +## Decision Outcome + +Chosen option: "Dual license", because this lets users choose whether CC0 or MIT fits better on their work. + +## Pros and Cons of the Options + +## CC0 + +* Good, because this license donates the content to "public domain" and does so as legally as possible. +* Bad, because it does not contain attribution - and [attribution is important](https://opensource.stackexchange.com/a/9126/5671). + +## BSD3 + +* Bad, because it [is unclear whether it can be used for documentation](https://opensource.stackexchange.com/a/9545/5671) + +## MIT + +* Good, because it [explicitly may be used for documentation](https://opensource.stackexchange.com/a/9545/5671) +* Good, because it is lean. + +## Dual license with MIT and CC0 + +With the SPDX identifier `MIT OR CC0-1.0`, the receiver of the documents can decide which license thay want to use. + +* Good, because offers freedom at the receiver +* Bad, because dual licensing is not widely known diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0002-do-not-use-numbers-in-headings.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0002-do-not-use-numbers-in-headings.md new file mode 100644 index 000000000..9f3197305 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0002-do-not-use-numbers-in-headings.md @@ -0,0 +1,24 @@ +--- +parent: Decisions +nav_order: 2 +--- +# Do Not Use Numbers in Headings + +## Context and Problem Statement + +How to render the first line in an ADR? +ADRs have to take a unique identifier. + +## Considered Options + +* Use the title only +* Add the ADR number in front of the title (e.g., "# 2. Do not use numbers in headings") + +## Decision Outcome + +Chosen option: "Use the title only", because + +* This is common in other markdown files, too. + One does not add numbering manually at the markdown files, but tries to get the numbers injected by the rendering framework or CSS. +* Enables renaming of ADRs (before publication) easily +* Allows copy'n'paste of ADRs from other repositories without having to worry about the numbers. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0003-provide-own-madr-tools.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0003-provide-own-madr-tools.md new file mode 100644 index 000000000..00750fd68 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0003-provide-own-madr-tools.md @@ -0,0 +1,33 @@ +--- +parent: Decisions +nav_order: 3 +status: on hold +--- +# Write Own MADR Tooling + +## Context and Problem Statement + +Developers seem to like tooling to create ADRs. +That tooling should support MADR. +The tooling should be easy to install. + +## Considered Options + +* Include in [adr-tools](https://github.com/npryce/adr-tools), 924 stars as of 2018-06-14 +* Include in [adr-j](https://github.com/adoble/adr-j), 2 stars as of 2018-06-14 +* Include in [adr](https://github.com/phodal/adr), 72 stars as of 2018-06-14 +* Write own MADR tooling +* No tool support + +## Decision Outcome + +Chosen option: "Write own MADR tooling", because + +* adding MADR support to `adr-tools` [was rejected](https://github.com/npryce/adr-tools/pull/43) +* other tooling seem to a) modify MADR or b) do not keep up with changes on MADR. + +We accept that this comes with maintenance cost. + +## More Information + +An overview on current tooling of MADR is available at . diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0004-write-own-toc-tool.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0004-write-own-toc-tool.md new file mode 100644 index 000000000..038204407 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0004-write-own-toc-tool.md @@ -0,0 +1,29 @@ +--- +parent: Decisions +nav_order: 4 +--- +# Write Own TOC Tool + +## Context and Problem Statement + +ADRs have to be indexed somehow. E.g., for offering a website showing all ADRs. + +## Considered Options + +* Write own tool `adr-log` +* Use `adr-tools`' TOC functionality + +## Decision Outcome + +Chosen option: "Write own tool `adr-log`", because + +* we want to have the format `ADR-0001 - Title` in the TOC. +* `adr-tools` offers `title` only. + +We accept that changing `adr-tools` would also be possible. +It is prepared to included header and footer: . + +### Consequences + +* Good, because `adr-log` is installable using `npm install -g adr-log`, which is easier than installing `adr-tools`. +* Bad, because another tool has to be maintained diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0005-use-dashes-in-filenames.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0005-use-dashes-in-filenames.md new file mode 100644 index 000000000..4c94869e8 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0005-use-dashes-in-filenames.md @@ -0,0 +1,26 @@ +--- +parent: Decisions +nav_order: 5 +--- +# Use Dashes in Filenames + +## Context and Problem Statement + +What is the pattern of the filename where an ADR is stored? + +## Considered Options + +* `NNNN-title-with-dashes.md` - format used by [adr-tools](https://github.com/npryce/adr-tools) +* `YYYY-MM-DD Title` - see + +## Decision Outcome + +Chosen option: "`NNNN-title-with-dashes.md`", because + +* `NNNN` provides a unique number, which can be used for referencing in the forms + * `ADR-0001` in plain text and + * by `@ADR(1)` Java code (enabled by [e-adr](https://adr.github.io/e-adr/)) +* The creation time of an ADR is of historical interest only, if it gets updated somehow. + The arguments are similar than the ones by [Does Git have keyword expansion?](https://git.wiki.kernel.org/index.php/GitFaq#Does_Git_have_keyword_expansion.3F) +* Having no spaces in filenames eases working in the command line +* This is exactly the format offered by [adr-tools](https://github.com/npryce/adr-tools) diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0006-use-names-as-identifier.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0006-use-names-as-identifier.md new file mode 100644 index 000000000..1734742b8 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0006-use-names-as-identifier.md @@ -0,0 +1,24 @@ +--- +parent: Decisions +nav_order: 6 +--- +# Use Names as Identifier + +## Context and Problem Statement + +An option is listed at "Considered Options" and repeated at "Pros and Cons of the Options". Finally, the chosen option is stated at "Decision Outcome". + +## Decision Drivers + +* Easy to read +* Easy to write +* Avoid copy and paste errors + +## Considered Options + +* Repeat all option names if they occur +* Assign an identifier to an option, e.g., `[A] Use gradle as build tool` + +## Decision Outcome + +Chosen option: "Repeat all option names if they occur", because 1) there is no markdown standard for identifiers, 2) the document is harder to read if there are multiple options which must be remembered. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0007-do-not-emphasize-line-headings.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0007-do-not-emphasize-line-headings.md new file mode 100644 index 000000000..c12abeab3 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0007-do-not-emphasize-line-headings.md @@ -0,0 +1,23 @@ +--- +parent: Decisions +nav_order: 7 +--- +# Do Not Emphasize Line Headings + +## Context and Problem Statement + +MADR contains lines such as `Chosen option: "[option 1]"`. Should "Chosen option" be emphasized? + +## Decision Drivers + +* MADR should be easy to read +* MADR should be easy to write + +## Considered Options + +* Do not emphasize line headings +* Emphasize line headings + +## Decision Outcome + +Chosen option: "Do not emphasize line headings", because 1) these headings always are put at the beginning of a line and followed by a colon. Thus, they are already easy to identified as line heading. 2) Readers not familiar with Markdown might be confused by stars in the text. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-add-status-field.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-add-status-field.md new file mode 100644 index 000000000..bae2bb479 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-add-status-field.md @@ -0,0 +1,100 @@ +--- +parent: Decisions +nav_order: 8 +--- +# Add Status Field + +## Context and Problem Statement + +Technical Story: + +ADRs have a status. Should this be tracked? And if it should, how should we track it? + +## Considered Options + +* Use YAML front matter +* Use badge +* Use text line +* Use separate heading +* Use table +* Do not add status + +## Decision Outcome + +Chosen option: "Use YAML front matter", because comes out best (see below). + +## Pros and Cons of the Options + +### Use YAML front matter + +Example: + +```markdown +--- +parent: Decisions +nav_order: 3 +status: on hold +--- +# Write own MADR tooling +``` + +* Good, because YAML front matter is supported by most Markdown parsers + +### Use badge + +#### Examples + +* ![Example "Use Angular" with "status: accepted"](0008-example-badge.png) +* [![Example "status: superseded"](https://img.shields.io/badge/status-superseeded_by_ADR_0001-orange.svg?style=flat-square)](https://github.com/adr/madr/blob/main/docs/decisions/0001-use-CC0-as-license.md) + +--- + +* Good, because plain markdown +* Good, because looks good +* Bad, because hard to read in markdown source +* Bad, because relies on the online service or [local badges have to be generated](https://github.com/badges/shields#using-the-badge-library) +* Bad, because at local usages, many badges have to be generated (superseeded-by-ADR-0006, for each ADR number) +* Bad, because not easy to write + +### Use text line + +Example: `Status: Accepted` + +* Good, because plain markdown +* Good, because easy to read +* Good, because easy to write +* Good, because looks OK in both markdown-source (MD) and in rendered versions (HTML, PDF) +* Good, because no dependencies on external tools +* Good, because single line indicates the current state +* Bad, because "Status" line needs to be maintained +* Bad, because uses space at the beginning. When users read MADR, they should directly dive into the context and problem and not into the status + +### Use separate heading + +Example: ![example for separate heading](0008-example-separate-heading.png) + +* Good, because plain markdown +* Good, because easy to write +* Bad, because it uses much space: At least three lines: heading, status, separating empty line + +### Use table + +Example: ![example for table](0008-example-table.png) + +* Good, because history can be included +* Good, because multiple entries can be made +* Good, because already implemented in `adr-tools` fork +* Bad, because not covered by the [CommonMark specification 0.28 (2017-08-01)](http://spec.commonmark.org/0.28/) +* Bad, because hard to read +* Bad, because outdated entries cannot be easily identified +* Bad, because needs more markdown training + +### Do not add status + +* Good, because MADR is kept lean +* Bad, because users demand state field +* Bad, because not in line with other ADR templates + +## More Information + +See [ADR-0013](0013-use-yaml-front-matter-for-meta-data.md) for more reasoning on using YAML front matter. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-badge.png b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..20ced2835dea4f5b760aad8f2a739d60e56e8c6e GIT binary patch literal 2073 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 QUjyJ^>ughI6?hBy8%Xr<1ONa4 literal 0 HcmV?d00001 diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-table.png b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-table.png new file mode 100644 index 0000000000000000000000000000000000000000..768c2d145f80bdb098ec8d00cb2d4e53a24927bf GIT binary patch literal 1334 zcmeAS@N?(olHy`uVBq!ia0vp^CxO_Xg9%8U^DUdsz`*jy)5S5QV$Rz+-s=_{2)I@o z=1=n6Hp!7KlFKPcm17#yn-&kX2psKfZS)*>@N>kpPPVcpu3EnX{lx}I>T)KY!^ZCM_*UM^hdF$5u zEV~&t#ktQY^($wN66EYjI4i zVsvtKqMOK#swl=uD}rM)r*(Q4mlsWwZa=i|+0j!+r`a$1bL^e8(Khi%hi}}e^9xO{ zJ0G66o@e4On{9HnTTSli@WxigHEwQy8L(!zaPedBsmHhM<9cWD_L!T*p^dI*C2vRB zMnCj9eQF<9@i(WZ#%FV$Uw^jwwQTwcwdl&{U(Y9>c7Fcm%#$xnR_FJ>Zkc*}OQ*rE z34+_c@g&|{6qjn%Q!f+!{g>5e!%D;cgtQrQiShIAnyhOyY|GhTs(qj3TV#&!?oS6M z%q?5Ps6DrILq30LOjl_9#BeT0p z)*Ly!=u2n61yA%gALF*BvfDS}uI*2*w!Hi5;=TjV`$gl6cBm;DmTu6?%<~Km-<5g! z^7dB~-0vTp_L6^l9nd*PW)|AWcAaoA`K@}}^yVeE{c7cvn=-wpS2uUL9iQ>)!Se|d z1k@ysxV)Mo-y!$3t#`w;<)|Z7KLow zyF0q*5L;#Sy-9cSBfWRpsxdci+9Px6`{^yudDJ(>FX>}_U;0h7JxZ8);o+d`M=F`0 z#QXd8W$UQyd*a~xM`(wY#iref;hxJjHuC*#XDfbFExv8W-_?hw)vy2D@pyx)f3Az1 zmG$3O{MLDa$=+#Mn--RDd@Ad7{iNokwGKz;&D?%xdH&S%=59T@+p2ecILsO;luFKm3PDYzPd~$wnUc;@5qqE(PAJZ~=`K$X!TA!t5QTxK5 zN&n|$=o!De`|6)Twvx%qyUR}QS5IcycI0c`OUhx>W_aqKcp4`i$exaS3j3^P6 + +## Considered Options + +* Include in section "More Information" +* Use tables +* Use heading together with a bullet list directly after status +* Use heading together with a bullet list directly after "Decision Outcome" +* Use heading together with a bullet list at the end +* Do not add links + +## Decision Outcome + +Chosen option: "Include in section 'More Information'", because comes out best (see below). + +## Pros and Cons of the Options + +### Include in section "More Information" + +Example: + +```markdown +## More Information + +[ADR-0008](0008-add-status-field.md) reasons on adding meta data (such as status). +``` + +* Good, because provides freedom to the user +* Bad, because parsing gets harder + +### Use tables + +* Good, because easy to write +* Good, because history is shown (enabled by concept) +* Good, because [current `adr-tools`' support](https://github.com/npryce/adr-tools/pull/43) uses tables to describe links. +* Bad, because not supported by the CommonMark spec +* Bad, because unclear whether a link was super seeded by another one +* Bad, because valid links not clear at first sight (there might be outdated links shown) + +### Use heading together with a bullet list directly after status + +Example: +![grafik](https://user-images.githubusercontent.com/1366654/36787434-6a63e318-1c8a-11e8-8824-4dd7b3d0f2c6.png) + +* Good, because easy to write +* Good, because supported by the CommonMark spec +* Bad, because not consistent with the status label (refs ) + +### Use heading together with a bullet list directly after "Decision Outcome" + +* Good, because easy to write +* Good, because supported by the CommonMark spec +* Good, because the options are first introduced and then the links +* Good, because consistent with position of "Decision Outcome" +* Bad, because reader might get distracted: He might expect explanation of the options instead of links to something else +* Bad, because not consistent with scientific papers, where related work and future work are coming after the discussion of the content. + +### Use heading together with a bullet list at the end + +* Good, because easy to write +* Good, because supported by the CommonMark spec +* Good, because the options and pros/cons are kept together with the option list. +* Good, because consistent with pattern format + +### Do not add links + +* Good, because template stays minimal diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0010-support-categories.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0010-support-categories.md new file mode 100644 index 000000000..a9160f5d9 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0010-support-categories.md @@ -0,0 +1,106 @@ +--- +parent: Decisions +nav_order: 10 +--- +# Support Categories + +## Context and Problem Statement + +ADRs are recorded. The number of ADRs grows and the context/topic/scope of ADRs might be different (e.g., frontend, backend) + +## Decision Drivers + +* Easy to find groups ADRs in hundreds of ADRs +* Easy to group +* Easy to create +* Good finding without external tooling +* Keep newcomers in mind (should be doable in <10 minutes) +* Keep template lean + +## Considered Options + +* Use labels +* Add `* Category: CATEGORY` directly under the heading (similar to ) +* Use YAML front matter +* Encode category in filename +* Use subfolders with local IDs +* Use subfolders with global IDs +* Don't do it. + +## Decision Outcome + +Chosen option: "Use subfolders with local IDs", because comes out best (see below). + +## Pros and Cons of the Options + +### Use labels + +Example: + +Use Angular ![category-frontend](https://img.shields.io/badge/category-frontend-blue.svg?style=flat-square) + +`![category-frontend](https://img.shields.io/badge/category-frontend-blue.svg?style=flat-square)` + +* Good, because full markdown +* Good, because linking to an overview page is possible (using markdown) +* Bad, because not straight-forward to parse +* Bad, because no simple filtering using `ls` or Windows Explorer is possible + +### Add `* Category: CATEGORY` directly under the heading + +* Good, because full markdown +* Good, because linking to an overview page is possible (using markdown) +* Good, because straight-forward to parse +* Bad, because no simple filtering using `ls` or Windows Explorer is possible + +### Use YAML front matter + +Example: + +```yaml +--- +category: frontend +--- +``` + +* Good, because nearly straight-forward to parse +* Good, because Jekyll supports it +* Bad, because YAML front matter is not part of the [CommonMarc Spec](http://spec.commonmark.org/) +* Bad, because no simple filtering using `ls` or Windows Explorer is possible + +### Encode category in filename + +Example: `0050--frontend--title-with-dashes.md` + +* Good, because programmatic filtering is possible +* Good, because `ls -la | grep --category--` works +* Bad, because plain file list in Windows explorer cannot be filtered +* Bad, because as bad as [TagSpaces](https://www.tagspaces.org/), which stores the tags in the filenames in brackets. E.g., `demo[demotag secondtag].md`. + +### Use subfolders with local IDs + +Optionally "to-be-categorized" folder. + +One level of subfolder, not nested + +#### Examples + +* `docs/decisions/smar/0000-secure-entities.md` +* `docs/decisions/smar/0001-flexible-properties-selection.md` + +#### Pros/cons + +* Good, because grouping is done by folders (which are natural for grouping) +* Good, because typos can easily be spotted +* Bad, because there is no unique number identifying an ADR +* Bad, because two indices have to be maintained (`adr-log` needs to be updated) +* Bad, because [e-adr](https://github.com/adr/e-adr) needs to be adapted to `@ADR("category", number)` (not that bad) +* Bad, because when category is unknown it is hard to find the right folder +* Bad, because using categories might be hampering newcomers + +### Use subfolders with global IDs + +#### Examples + +* `docs/decisions/smar/0005-secure-entities.md` +* `docs/decisions/smar/0047-flexible-properties-selection.md` diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0011-use-asterisk-as-list-marker.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0011-use-asterisk-as-list-marker.md new file mode 100644 index 000000000..60956c008 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0011-use-asterisk-as-list-marker.md @@ -0,0 +1,20 @@ +--- +parent: Decisions +nav_order: 11 +--- +# Use Asterisk as List Marker + +## Context and Problem Statement + +Lists in Markdown can be indicated by `*` (asterisk) or `-` (hyphen). + +## Considered Options + +* Use an asterisk +* Use a hyphen + +## Decision Outcome + +Chosen option: "Use an asterisk", because an asterisk does not have a meaning of "good" or "bad", whereas a hyphen `-` could be read as indicator of something negative (in contrast to `+`, which could be more be read as "good"). + +According to the [Markdown Style Guide](http://www.cirosantilli.com/markdown-style-guide/), an asterisk as list marker is more readable (see [readability profile](http://www.cirosantilli.com/markdown-style-guide/#readability-profile)). diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0012-use-curly-braces-to-denote-placeholder.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0012-use-curly-braces-to-denote-placeholder.md new file mode 100644 index 000000000..c73921b26 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0012-use-curly-braces-to-denote-placeholder.md @@ -0,0 +1,46 @@ +--- +parent: Decisions +nav_order: 12 +--- +# Use Curly Braces to Denote Placeholders + +## Context and Problem Statement + +When crafting an ADR placeholders need to be replaced by real values. +How to mark the placeholders? + +## Considered Options + +* Use curly braces +* Use square brackets +* Use less-than and greater-than + +## Decision Outcome + +Chosen option: "Use curly braces", because comes out best (see below). + +## Pros and Cons of the Options + +### Use curly braces + +Example: `{option 1}`. + +* Good, because [consistent to mustache templates](https://krasimirtsonev.com/blog/article/markdown-smart-placeholders). +* Good, because no confusion with markdown notation for links + +### Use square brackets + +Example: `[option 1]`. + +* Good, because used in MADR 1.x and MADR 2.x +* Bad, because confusion with markdown notation for links +* Bad, because some users did not remove the brackets. Example: `Date: [2021-03-12]` or `Good, because [user no longer activatess shortcut accidently when entering task]`. + +### Use less-than and greater-than + +Example: `Documentation - * on the Structurizr website for more details. + * See Documentation + * and Decisions for more details. */ public final class Documentation { private List
sections = new ArrayList<>(); - private Set decisions = new HashSet<>(); - private Set images = new HashSet<>(); + private Set decisions = new TreeSet<>(); + private Set images = new TreeSet<>(); public Documentation() { } @@ -76,12 +76,12 @@ void setSections(Collection
sections) { * @return a Set of Decision objects */ public Set getDecisions() { - return new HashSet<>(decisions); + return new TreeSet<>(decisions); } void setDecisions(Set decisions) { if (decisions != null) { - this.decisions = new HashSet<>(decisions); + this.decisions = new TreeSet<>(decisions); } } @@ -136,12 +136,12 @@ public void addImage(Image image) { * @return a Set of {@link Image} objects */ public Set getImages() { - return new HashSet<>(images); + return new TreeSet<>(images); } void setImages(Set images) { if (images != null) { - this.images = new HashSet<>(images); + this.images = new TreeSet<>(images); } } @@ -155,8 +155,8 @@ public boolean isEmpty() { */ public void clear() { sections = new ArrayList<>(); - decisions = new HashSet<>(); - images = new HashSet<>(); + decisions = new TreeSet<>(); + images = new TreeSet<>(); } } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/documentation/Image.java b/structurizr-core/src/main/java/com/structurizr/documentation/Image.java index b76845f13..b28404215 100644 --- a/structurizr-core/src/main/java/com/structurizr/documentation/Image.java +++ b/structurizr-core/src/main/java/com/structurizr/documentation/Image.java @@ -3,7 +3,7 @@ /** * Represents a base64 encoded image (png/jpg/gif). */ -public final class Image { +public final class Image implements Comparable { private String name; private String content; @@ -42,4 +42,9 @@ void setType(String type) { this.type = type; } + @Override + public int compareTo(Image image) { + return getName().compareTo(image.getName()); + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/Container.java b/structurizr-core/src/main/java/com/structurizr/model/Container.java index d6aff51b8..5e7f799f0 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Container.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Container.java @@ -15,7 +15,7 @@ public final class Container extends StaticStructureElement implements Documenta private SoftwareSystem parent; private String technology; - private Set components = new LinkedHashSet<>(); + private Set components = new TreeSet<>(); private Documentation documentation = new Documentation(); @@ -118,12 +118,12 @@ void remove(Component component) { * @return a Set of Component objects */ public Set getComponents() { - return new HashSet<>(components); + return new TreeSet<>(components); } void setComponents(Set components) { if (components != null) { - this.components = new HashSet<>(components); + this.components = new TreeSet<>(components); } } diff --git a/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java b/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java index e40aaeac5..41f872174 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java +++ b/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java @@ -25,10 +25,10 @@ public final class DeploymentNode extends DeploymentElement { private String technology; private String instances = "1"; - private Set children = new HashSet<>(); - private Set infrastructureNodes = new HashSet<>(); - private Set softwareSystemInstances = new HashSet<>(); - private Set containerInstances = new HashSet<>(); + private Set children = new TreeSet<>(); + private Set infrastructureNodes = new TreeSet<>(); + private Set softwareSystemInstances = new TreeSet<>(); + private Set containerInstances = new TreeSet<>(); /** * Adds a software system instance to this deployment node, replicating relationships. @@ -306,12 +306,12 @@ public Relationship uses(InfrastructureNode destination, String description, Str * @return a Set of DeploymentNode objects */ public Set getChildren() { - return new HashSet<>(children); + return new TreeSet<>(children); } void setChildren(Set children) { if (children != null) { - this.children = new HashSet<>(children); + this.children = new TreeSet<>(children); } } @@ -321,12 +321,12 @@ void setChildren(Set children) { * @return a Set of InfrastructureNode objects */ public Set getInfrastructureNodes() { - return new HashSet<>(infrastructureNodes); + return new TreeSet<>(infrastructureNodes); } void setInfrastructureNodes(Set infrastructureNodes) { if (infrastructureNodes != null) { - this.infrastructureNodes = new HashSet<>(infrastructureNodes); + this.infrastructureNodes = new TreeSet<>(infrastructureNodes); } } @@ -376,12 +376,12 @@ public boolean hasContainerInstances() { * @return a Set of SoftwareSystemInstance objects */ public Set getSoftwareSystemInstances() { - return new HashSet<>(softwareSystemInstances); + return new TreeSet<>(softwareSystemInstances); } void setSoftwareSystemInstances(Set softwareSystemInstances) { if (softwareSystemInstances != null) { - this.softwareSystemInstances = new HashSet<>(softwareSystemInstances); + this.softwareSystemInstances = new TreeSet<>(softwareSystemInstances); } } @@ -391,12 +391,12 @@ void setSoftwareSystemInstances(Set softwareSystemInstan * @return a Set of ContainerInstance objects */ public Set getContainerInstances() { - return new HashSet<>(containerInstances); + return new TreeSet<>(containerInstances); } void setContainerInstances(Set containerInstances) { if (containerInstances != null) { - this.containerInstances = new HashSet<>(containerInstances); + this.containerInstances = new TreeSet<>(containerInstances); } } diff --git a/structurizr-core/src/main/java/com/structurizr/model/Element.java b/structurizr-core/src/main/java/com/structurizr/model/Element.java index a80ad2a75..7294b30bd 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Element.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Element.java @@ -4,7 +4,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.*; +import java.util.Set; +import java.util.TreeSet; /** * This is the superclass for all model elements. @@ -16,7 +17,7 @@ public abstract class Element extends ModelItem { private String name; private String description; - private Set relationships = new LinkedHashSet<>(); + private Set relationships = new TreeSet<>(); protected Element() { } @@ -83,12 +84,12 @@ public void setDescription(String description) { * @return a Set of Relationship objects, or an empty set if none exist */ public Set getRelationships() { - return new LinkedHashSet<>(relationships); + return new TreeSet<>(relationships); } void setRelationships(Set relationships) { if (relationships != null) { - this.relationships = new LinkedHashSet<>(relationships); + this.relationships = new TreeSet<>(relationships); } } @@ -153,7 +154,7 @@ public Relationship getEfferentRelationshipWith(Element element) { * @return a Set of Relationship objects; empty if no relationships exist */ public Set getEfferentRelationshipsWith(Element element) { - Set set = new HashSet<>(); + Set set = new TreeSet<>(); if (element != null) { for (Relationship relationship : relationships) { diff --git a/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java b/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java index 525e615c5..4ff1620f0 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java +++ b/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java @@ -2,11 +2,12 @@ import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; /** * Describes a HTTP based health check. */ -public final class HttpHealthCheck { +public final class HttpHealthCheck implements Comparable { /** a name for the health check */ private String name; @@ -15,7 +16,7 @@ public final class HttpHealthCheck { private String url; /** the headers that should be sent in the HTTP request */ - private Map headers = new HashMap<>(); + private final Map headers = new TreeMap<>(); /** the polling interval, in seconds */ private int interval; @@ -130,4 +131,15 @@ public int hashCode() { return result; } + @Override + public int compareTo(HttpHealthCheck healthCheck) { + int result = getName().compareTo(healthCheck.getName()); + + if (result == 0) { + result = getUrl().compareTo(healthCheck.getUrl()); + } + + return result; + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/InfrastructureNode.java b/structurizr-core/src/main/java/com/structurizr/model/InfrastructureNode.java index 1910241be..c9c293c18 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/InfrastructureNode.java +++ b/structurizr-core/src/main/java/com/structurizr/model/InfrastructureNode.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore; -import java.util.*; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; /** *

diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 3903fa767..3e49dd9e0 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -16,18 +16,18 @@ public final class Model implements PropertyHolder { private IdGenerator idGenerator = new SequentialIntegerIdGeneratorStrategy(); - private final Set elements = new LinkedHashSet<>(); + private final Set elements = new TreeSet<>(); private final Map elementsById = new HashMap<>(); - private final Set relationships = new LinkedHashSet<>(); + private final Set relationships = new TreeSet<>(); private final Map relationshipsById = new HashMap<>(); private Enterprise enterprise; - private Set people = new LinkedHashSet<>(); - private Set softwareSystems = new LinkedHashSet<>(); - private Set deploymentNodes = new LinkedHashSet<>(); - private Set customElements = new LinkedHashSet<>(); + private Set people = new TreeSet<>(); + private Set softwareSystems = new TreeSet<>(); + private Set deploymentNodes = new TreeSet<>(); + private Set customElements = new TreeSet<>(); private ImpliedRelationshipsStrategy impliedRelationshipsStrategy = new DefaultImpliedRelationshipsStrategy(); @@ -70,10 +70,9 @@ public SoftwareSystem addSoftwareSystem(@Nonnull String name, @Nullable String d SoftwareSystem softwareSystem = new SoftwareSystem(); softwareSystem.setName(name); softwareSystem.setDescription(description); + softwareSystem.setId(idGenerator.generateId(softwareSystem)); softwareSystems.add(softwareSystem); - - softwareSystem.setId(idGenerator.generateId(softwareSystem)); addElementToInternalStructures(softwareSystem); return softwareSystem; @@ -108,10 +107,9 @@ public Person addPerson(@Nonnull String name, @Nullable String description) { Person person = new Person(); person.setName(name); person.setDescription(description); + person.setId(idGenerator.generateId(person)); people.add(person); - - person.setId(idGenerator.generateId(person)); addElementToInternalStructures(person); return person; @@ -167,11 +165,11 @@ Container addContainer(SoftwareSystem parent, @Nonnull String name, @Nullable St container.setName(name); container.setDescription(description); container.setTechnology(technology); + container.setId(idGenerator.generateId(container)); container.setParent(parent); parent.add(container); - container.setId(idGenerator.generateId(container)); addElementToInternalStructures(container); return container; @@ -186,11 +184,11 @@ Component addComponent(Container parent, String name, String description, String component.setName(name); component.setDescription(description); component.setTechnology(technology); + component.setId(idGenerator.generateId(component)); component.setParent(parent); parent.add(component); - component.setId(idGenerator.generateId(component)); addElementToInternalStructures(component); return component; @@ -310,7 +308,7 @@ private void removeRelationshipFromInternalStructures(Relationship relationship) @JsonIgnore @Nonnull public Set getElements() { - return new LinkedHashSet<>(elements); + return new TreeSet<>(elements); } /** @@ -337,7 +335,7 @@ public Element getElement(@Nonnull String id) { @JsonIgnore @Nonnull public Set getRelationships() { - return new LinkedHashSet<>(this.relationships); + return new TreeSet<>(this.relationships); } /** @@ -363,12 +361,12 @@ public Relationship getRelationship(@Nonnull String id) { */ @Nonnull public Set getCustomElements() { - return new LinkedHashSet<>(customElements); + return new TreeSet<>(customElements); } void setCustomElements(Set customElements) { if (customElements != null) { - this.customElements = new LinkedHashSet<>(customElements); + this.customElements = new TreeSet<>(customElements); } } @@ -379,12 +377,12 @@ void setCustomElements(Set customElements) { */ @Nonnull public Set getPeople() { - return new LinkedHashSet<>(people); + return new TreeSet<>(people); } void setPeople(Set people) { if (people != null) { - this.people = new LinkedHashSet<>(people); + this.people = new TreeSet<>(people); } } @@ -395,12 +393,12 @@ void setPeople(Set people) { */ @Nonnull public Set getSoftwareSystems() { - return new LinkedHashSet<>(softwareSystems); + return new TreeSet<>(softwareSystems); } void setSoftwareSystems(Set softwareSystems) { if (softwareSystems != null) { - this.softwareSystems = new LinkedHashSet<>(softwareSystems); + this.softwareSystems = new TreeSet<>(softwareSystems); } } @@ -411,12 +409,12 @@ void setSoftwareSystems(Set softwareSystems) { */ @Nonnull public Set getDeploymentNodes() { - return new LinkedHashSet<>(deploymentNodes); + return new TreeSet<>(deploymentNodes); } void setDeploymentNodes(Set deploymentNodes) { if (deploymentNodes != null) { - this.deploymentNodes = new LinkedHashSet<>(deploymentNodes); + this.deploymentNodes = new TreeSet<>(deploymentNodes); } } @@ -791,6 +789,8 @@ DeploymentNode addDeploymentNode(DeploymentNode parent, @Nullable String environ deploymentNode.setParent(parent); deploymentNode.setInstances(instances); deploymentNode.setEnvironment(environment); + deploymentNode.setId(idGenerator.generateId(deploymentNode)); + if (properties != null) { deploymentNode.setProperties(properties); } @@ -799,7 +799,6 @@ DeploymentNode addDeploymentNode(DeploymentNode parent, @Nullable String environ deploymentNodes.add(deploymentNode); } - deploymentNode.setId(idGenerator.generateId(deploymentNode)); addElementToInternalStructures(deploymentNode); return deploymentNode; @@ -821,11 +820,12 @@ InfrastructureNode addInfrastructureNode(DeploymentNode parent, @Nonnull String infrastructureNode.setTechnology(technology); infrastructureNode.setParent(parent); infrastructureNode.setEnvironment(parent.getEnvironment()); + infrastructureNode.setId(idGenerator.generateId(infrastructureNode)); + if (properties != null) { infrastructureNode.setProperties(properties); } - infrastructureNode.setId(idGenerator.generateId(infrastructureNode)); addElementToInternalStructures(infrastructureNode); return infrastructureNode; diff --git a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java index 2be654cd6..7f936d238 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java @@ -11,14 +11,14 @@ /** * The base class for elements and relationships. */ -public abstract class ModelItem implements PropertyHolder { +public abstract class ModelItem implements PropertyHolder, Comparable { private String id = ""; - private Set tags = new LinkedHashSet<>(); + private final Set tags = new LinkedHashSet<>(); private String url; private Map properties = new HashMap<>(); - private Set perspectives = new HashSet<>(); + private final Set perspectives = new TreeSet<>(); @JsonIgnore public abstract String getCanonicalName(); @@ -35,7 +35,7 @@ public String getId() { return id; } - void setId(String id) { + protected void setId(String id) { this.id = id; } @@ -174,7 +174,7 @@ void setProperties(Map properties) { * @return a Set of Perspective objects (empty if there are none) */ public Set getPerspectives() { - return new HashSet<>(perspectives); + return new TreeSet<>(perspectives); } void setPerspectives(Set perspectives) { @@ -227,4 +227,16 @@ public Perspective addPerspective(String name, String description, String value) return perspective; } + @Override + public int compareTo(ModelItem modelItem) { + try { + int id1 = Integer.parseInt(getId()); + int id2 = Integer.parseInt(modelItem.getId()); + + return id1 - id2; + } catch (NumberFormatException nfe) { + return getId().compareTo(modelItem.getId()); + } + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/Perspective.java b/structurizr-core/src/main/java/com/structurizr/model/Perspective.java index b20285392..5b55e757e 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Perspective.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Perspective.java @@ -4,7 +4,7 @@ * Represents an architectural perspective, that can be applied to elements and relationships. * See https://www.viewpoints-and-perspectives.info/home/perspectives/ for more details of this concept. */ -public final class Perspective { +public final class Perspective implements Comparable { private String name; private String description; @@ -73,4 +73,9 @@ public int hashCode() { return getName().hashCode(); } + @Override + public int compareTo(Perspective perspective) { + return getName().compareTo(perspective.getName()); + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java index 74cf5d85d..005cdda30 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java @@ -7,9 +7,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Arrays; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; +import java.util.TreeSet; /** * Represents a "software system" in the C4 model. @@ -18,7 +18,7 @@ public final class SoftwareSystem extends StaticStructureElement implements Docu private Location location = Location.Unspecified; - private Set containers = new LinkedHashSet<>(); + private Set containers = new TreeSet<>(); private Documentation documentation = new Documentation(); @@ -66,12 +66,12 @@ void add(Container container) { */ @Nonnull public Set getContainers() { - return new HashSet<>(containers); + return new TreeSet<>(containers); } void setContainers(Set containers) { if (containers != null) { - this.containers = new HashSet<>(containers); + this.containers = new TreeSet<>(containers); } } diff --git a/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java index 6ed26d0c2..d7c5d3ae7 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java +++ b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java @@ -6,8 +6,8 @@ import javax.annotation.Nonnull; import java.util.Collections; -import java.util.HashSet; import java.util.Set; +import java.util.TreeSet; /** * Represents a deployment instance of a {@link SoftwareSystem} or {@link Container}, which can be added to a {@link DeploymentNode}. @@ -17,9 +17,9 @@ public abstract class StaticStructureElementInstance extends DeploymentElement { private static final int DEFAULT_HEALTH_CHECK_INTERVAL_IN_SECONDS = 60; private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_IN_MILLISECONDS = 0; - private Set deploymentGroups = new HashSet<>(); + private Set deploymentGroups = new TreeSet<>(); private int instanceId; - private Set healthChecks = new HashSet<>(); + private Set healthChecks = new TreeSet<>(); StaticStructureElementInstance() { } @@ -53,15 +53,15 @@ public Set getDeploymentGroups() { if (deploymentGroups.isEmpty()) { return Collections.singleton(DEFAULT_DEPLOYMENT_GROUP); } else { - return new HashSet<>(deploymentGroups); + return new TreeSet<>(deploymentGroups); } } void setDeploymentGroups(Set deploymentGroups) { if (deploymentGroups != null) { - this.deploymentGroups = new HashSet<>(deploymentGroups); + this.deploymentGroups = new TreeSet<>(deploymentGroups); } else { - this.deploymentGroups = new HashSet<>(); + this.deploymentGroups = new TreeSet<>(); } } @@ -123,7 +123,7 @@ public void setName(String name) { */ @Nonnull public Set getHealthChecks() { - return new HashSet<>(healthChecks); + return new TreeSet<>(healthChecks); } void setHealthChecks(Set healthChecks) { diff --git a/structurizr-core/src/main/java/com/structurizr/view/Animation.java b/structurizr-core/src/main/java/com/structurizr/view/Animation.java index c15f89dd0..0e08bb070 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Animation.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Animation.java @@ -3,8 +3,8 @@ import com.structurizr.model.Element; import com.structurizr.model.Relationship; -import java.util.HashSet; import java.util.Set; +import java.util.TreeSet; /** * A wrapper for a collection of animation steps. @@ -12,8 +12,8 @@ public final class Animation { private int order; - private Set elements = new HashSet<>(); - private Set relationships = new HashSet<>(); + private Set elements = new TreeSet<>(); + private Set relationships = new TreeSet<>(); Animation() { } @@ -39,22 +39,22 @@ void setOrder(int order) { } public Set getElements() { - return new HashSet<>(elements); + return new TreeSet<>(elements); } void setElements(Set elements) { if (elements != null) { - this.elements = new HashSet<>(elements); + this.elements = new TreeSet<>(elements); } } public Set getRelationships() { - return new HashSet<>(relationships); + return new TreeSet<>(relationships); } void setRelationships(Set relationships) { if (relationships != null) { - this.relationships = new HashSet<>(relationships); + this.relationships = new TreeSet<>(relationships); } } diff --git a/structurizr-core/src/main/java/com/structurizr/view/Configuration.java b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java index 9d1a0c3fe..87780a332 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Configuration.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java @@ -1,7 +1,6 @@ package com.structurizr.view; import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import com.structurizr.PropertyHolder; import com.structurizr.util.Url; diff --git a/structurizr-core/src/main/java/com/structurizr/view/ElementView.java b/structurizr-core/src/main/java/com/structurizr/view/ElementView.java index b3a4487a7..09518ebc6 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ElementView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ElementView.java @@ -6,7 +6,7 @@ /** * Represents an instance of an Element in a View. */ -public final class ElementView { +public final class ElementView implements Comparable { private Element element; private String id; @@ -100,4 +100,16 @@ void copyLayoutInformationFrom(ElementView source) { } } + @Override + public int compareTo(ElementView elementView) { + try { + int id1 = Integer.parseInt(getId()); + int id2 = Integer.parseInt(elementView.getId()); + + return id1 - id2; + } catch (NumberFormatException nfe) { + return getId().compareTo(elementView.getId()); + } + } + } diff --git a/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java b/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java index 64b70a507..084090afe 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.Arrays; -import java.util.HashSet; import java.util.Set; +import java.util.TreeSet; /** * Represents a view on top of a view, which can be used to include or exclude specific elements. @@ -15,7 +15,7 @@ public final class FilteredView extends View { private String baseViewKey; private FilterMode mode = FilterMode.Exclude; - private Set tags = new HashSet<>(); + private final Set tags = new TreeSet<>(); FilteredView() { } @@ -58,7 +58,7 @@ void setMode(FilterMode mode) { } public Set getTags() { - return new HashSet<>(tags); + return new TreeSet<>(tags); } @Override diff --git a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java index c80ddb72a..98f5a446a 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java @@ -7,9 +7,9 @@ import javax.annotation.Nonnull; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; /** @@ -34,8 +34,8 @@ public abstract class ModelView extends View { private AutomaticLayout automaticLayout = null; private boolean mergeFromRemote = true; - private Set elementViews = new LinkedHashSet<>(); - private Set relationshipViews = new LinkedHashSet<>(); + private Set elementViews = new TreeSet<>(); + private Set relationshipViews = new TreeSet<>(); private LayoutMergeStrategy layoutMergeStrategy = new DefaultLayoutMergeStrategy(); @@ -308,12 +308,12 @@ public void removeRelationshipsNotConnectedToElement(Element element) { * @return a Set of ElementView objects */ public Set getElements() { - return new HashSet<>(elementViews); + return new TreeSet<>(elementViews); } void setElements(Set elementViews) { if (elementViews != null) { - this.elementViews = new HashSet<>(elementViews); + this.elementViews = new TreeSet<>(elementViews); } } @@ -323,12 +323,12 @@ void setElements(Set elementViews) { * @return a Set of RelationshipView objects */ public Set getRelationships() { - return new HashSet<>(this.relationshipViews); + return new TreeSet<>(this.relationshipViews); } void setRelationships(Set relationshipViews) { if (relationshipViews != null) { - this.relationshipViews = new HashSet<>(relationshipViews); + this.relationshipViews = new TreeSet<>(relationshipViews); } } diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java index e9d01b480..38b0311fd 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java @@ -7,14 +7,14 @@ import com.structurizr.util.Url; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.Set; +import java.util.TreeSet; /** * This class represents an instance of a Relationship on a View. */ -public final class RelationshipView { +public final class RelationshipView implements Comparable { private static final int START_OF_LINE = 0; private static final int END_OF_LINE = 100; @@ -25,7 +25,7 @@ public final class RelationshipView { private String url; private String order; private Boolean response; - private Set vertices = new LinkedHashSet<>(); + private Set vertices = new TreeSet<>(); @JsonInclude(value = JsonInclude.Include.NON_NULL) private Routing routing; @@ -168,7 +168,7 @@ public Collection getVertices() { */ public void setVertices(Collection vertices) { if (vertices != null) { - this.vertices = new LinkedHashSet<>(vertices); + this.vertices = new TreeSet<>(vertices); } } @@ -254,4 +254,12 @@ public String toString() { return ""; } + @Override + public int compareTo(RelationshipView relationshipView) { + String identifier1 = getId() + "/" + getOrder(); + String identifier2 = relationshipView.getId() + "/" + relationshipView.getOrder(); + + return identifier1.compareTo(identifier2); + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/Vertex.java b/structurizr-core/src/main/java/com/structurizr/view/Vertex.java index eda796e23..84e2a30c8 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Vertex.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Vertex.java @@ -5,7 +5,7 @@ /** * The X, Y coordinate of a bend in a line. */ -public final class Vertex { +public final class Vertex implements Comparable { private int x; private int y; @@ -63,4 +63,14 @@ public int hashCode() { return Objects.hash(x, y); } + @Override + public int compareTo(Vertex vertex) { + int result = getX() - vertex.getX(); + if (result == 0) { + result = getY() - vertex.getY(); + } + + return result; + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/View.java b/structurizr-core/src/main/java/com/structurizr/view/View.java index 2ddfcc6b0..510437490 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/View.java +++ b/structurizr-core/src/main/java/com/structurizr/view/View.java @@ -11,7 +11,7 @@ /** * The superclass for all views. */ -public abstract class View implements PropertyHolder { +public abstract class View implements PropertyHolder, Comparable { private String key; private boolean generatedKey = false; @@ -160,4 +160,9 @@ void setProperties(Map properties) { } } + @Override + public int compareTo(View view) { + return getOrder() - view.getOrder(); + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java index 68b4e749f..cab1d8a44 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -33,16 +33,16 @@ public final class ViewSet { private Model model; - private Collection customViews = new HashSet<>(); - private Collection systemLandscapeViews = new HashSet<>(); - private Collection systemContextViews = new HashSet<>(); - private Collection containerViews = new HashSet<>(); - private Collection componentViews = new HashSet<>(); - private Collection dynamicViews = new HashSet<>(); - private Collection deploymentViews = new HashSet<>(); - private Collection imageViews = new HashSet<>(); + private Collection customViews = new TreeSet<>(); + private Collection systemLandscapeViews = new TreeSet<>(); + private Collection systemContextViews = new TreeSet<>(); + private Collection containerViews = new TreeSet<>(); + private Collection componentViews = new TreeSet<>(); + private Collection dynamicViews = new TreeSet<>(); + private Collection deploymentViews = new TreeSet<>(); + private Collection imageViews = new TreeSet<>(); - private Collection filteredViews = new HashSet<>(); + private Collection filteredViews = new TreeSet<>(); private Configuration configuration = new Configuration(); @@ -501,12 +501,12 @@ ImageView getImageViewWithKey(String key) { * @return a Collection of CustomView objects */ public Collection getCustomViews() { - return new HashSet<>(customViews); + return new TreeSet<>(customViews); } void setCustomViews(Set customViews) { if (customViews != null) { - this.customViews = new HashSet<>(customViews); + this.customViews = new TreeSet<>(customViews); } } @@ -516,12 +516,12 @@ void setCustomViews(Set customViews) { * @return a Collection of SystemLandscapeView objects */ public Collection getSystemLandscapeViews() { - return new HashSet<>(systemLandscapeViews); + return new TreeSet<>(systemLandscapeViews); } void setSystemLandscapeViews(Set systemLandscapeViews) { if (systemLandscapeViews != null) { - this.systemLandscapeViews = new HashSet<>(systemLandscapeViews); + this.systemLandscapeViews = new TreeSet<>(systemLandscapeViews); } } @@ -531,7 +531,7 @@ void setSystemLandscapeViews(Set systemLandscapeViews) { @JsonSetter("enterpriseContextViews") void setEnterpriseContextViews(Collection enterpriseContextViews) { if (enterpriseContextViews != null) { - this.systemLandscapeViews = new HashSet<>(enterpriseContextViews); + this.systemLandscapeViews = new TreeSet<>(enterpriseContextViews); } } @@ -541,12 +541,12 @@ void setEnterpriseContextViews(Collection enterpriseContext * @return a Collection of SystemContextView objects */ public Collection getSystemContextViews() { - return new HashSet<>(systemContextViews); + return new TreeSet<>(systemContextViews); } void setSystemContextViews(Set systemContextViews) { if (systemContextViews != null) { - this.systemContextViews = new HashSet<>(systemContextViews); + this.systemContextViews = new TreeSet<>(systemContextViews); } } @@ -556,12 +556,12 @@ void setSystemContextViews(Set systemContextViews) { * @return a Collection of ContainerView objects */ public Collection getContainerViews() { - return new HashSet<>(containerViews); + return new TreeSet<>(containerViews); } void setContainerViews(Set containerViews) { if (containerViews != null) { - this.containerViews = new HashSet<>(containerViews); + this.containerViews = new TreeSet<>(containerViews); } } @@ -571,12 +571,12 @@ void setContainerViews(Set containerViews) { * @return a Collection of ComponentView objects */ public Collection getComponentViews() { - return new HashSet<>(componentViews); + return new TreeSet<>(componentViews); } void setComponentViews(Set componentViews) { if (componentViews != null) { - this.componentViews = new HashSet<>(componentViews); + this.componentViews = new TreeSet<>(componentViews); } } @@ -586,22 +586,22 @@ void setComponentViews(Set componentViews) { * @return a Collection of DynamicView objects */ public Collection getDynamicViews() { - return new HashSet<>(dynamicViews); + return new TreeSet<>(dynamicViews); } void setDynamicViews(Set dynamicViews) { if (dynamicViews != null) { - this.dynamicViews = new HashSet<>(dynamicViews); + this.dynamicViews = new TreeSet<>(dynamicViews); } } public Collection getFilteredViews() { - return new HashSet<>(filteredViews); + return new TreeSet<>(filteredViews); } void setFilteredViews(Set filteredViews) { if (filteredViews != null) { - this.filteredViews = new HashSet<>(filteredViews); + this.filteredViews = new TreeSet<>(filteredViews); } } @@ -611,12 +611,12 @@ void setFilteredViews(Set filteredViews) { * @return a Collection of DeploymentView objects */ public Collection getDeploymentViews() { - return new HashSet<>(deploymentViews); + return new TreeSet<>(deploymentViews); } void setDeploymentViews(Set deploymentViews) { if (deploymentViews != null) { - this.deploymentViews = new HashSet<>(deploymentViews); + this.deploymentViews = new TreeSet<>(deploymentViews); } } @@ -626,12 +626,12 @@ void setDeploymentViews(Set deploymentViews) { * @return a Collection of ImageView objects */ public Collection getImageViews() { - return new HashSet<>(imageViews); + return new TreeSet<>(imageViews); } void setImageView(Set imageViews) { if (imageViews != null) { - this.imageViews = new HashSet<>(imageViews); + this.imageViews = new TreeSet<>(imageViews); } } @@ -642,7 +642,7 @@ void setImageView(Set imageViews) { */ @JsonIgnore public Collection getViews() { - HashSet views = new HashSet<>(); + Set views = new TreeSet<>(); views.addAll(getCustomViews()); views.addAll(getSystemLandscapeViews()); @@ -1045,7 +1045,7 @@ public void createDefaultViews() { } private Set getSoftwareSystemInstances(DeploymentNode deploymentNode) { - Set softwareSystemInstances = new HashSet<>(deploymentNode.getSoftwareSystemInstances()); + Set softwareSystemInstances = new TreeSet<>(deploymentNode.getSoftwareSystemInstances()); for (DeploymentNode child : deploymentNode.getChildren()) { softwareSystemInstances.addAll(getSoftwareSystemInstances(child)); @@ -1055,7 +1055,7 @@ private Set getSoftwareSystemInstances(DeploymentNode de } private Set getContainerInstances(DeploymentNode deploymentNode) { - Set containerInstances = new HashSet<>(deploymentNode.getContainerInstances()); + Set containerInstances = new TreeSet<>(deploymentNode.getContainerInstances()); for (DeploymentNode child : deploymentNode.getChildren()) { containerInstances.addAll(getContainerInstances(child)); @@ -1068,14 +1068,14 @@ private Set getContainerInstances(DeploymentNode deploymentNo * Removes all views and configuration. */ public void clear() { - customViews = new HashSet<>(); - systemLandscapeViews = new HashSet<>(); - systemContextViews = new HashSet<>(); - containerViews = new HashSet<>(); - componentViews = new HashSet<>(); - dynamicViews = new HashSet<>(); - deploymentViews = new HashSet<>(); - filteredViews = new HashSet<>(); + customViews = new TreeSet<>(); + systemLandscapeViews = new TreeSet<>(); + systemContextViews = new TreeSet<>(); + containerViews = new TreeSet<>(); + componentViews = new TreeSet<>(); + dynamicViews = new TreeSet<>(); + deploymentViews = new TreeSet<>(); + filteredViews = new TreeSet<>(); configuration = new Configuration(); } diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot index b2869d886..679e725e2 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot @@ -7,9 +7,9 @@ digraph { 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] subgraph cluster_11 { margin=25 diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot index 25109d81f..3e840c871 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot @@ -17,8 +17,8 @@ digraph { 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] } - 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] 8 -> 12 [id=32, label=<1. Submits credentials to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] 12 -> 15 [id=40, label=<2. Validates credentials
using
>, style="dashed", color="#707070", fontcolor="#707070"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd index 8a96174a1..7f759f871 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd @@ -8,12 +8,12 @@ graph TB style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff 5["

E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff - 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff 9["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] style 9 fill:#438dd5,stroke:#2e6295,color:#ffffff + 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] + style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff subgraph 11 [API Application] style 11 fill:#ffffff,stroke:#2e6295,color:#2e6295 diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd index ffccd9df7..1715a20f8 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd @@ -13,10 +13,10 @@ graph TB style 15 fill:#85bbf0,stroke:#5d82a8,color:#000000 end - 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff + 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] + style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff 8-. "
1. Submits credentials to
[JSON/HTTPS]
" .->12 12-. "
2. Validates credentials
using
" .->15 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml index 3ae85ef6e..a0d351686 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml @@ -21,9 +21,9 @@ AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontCol System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") -ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") +ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml index 70127ca7a..44fc2b9c6 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml @@ -11,8 +11,8 @@ top to bottom direction AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml index f3e11defa..68f04bd91 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml @@ -12,8 +12,8 @@ top to bottom direction AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml index d3b2df46c..decda55a1 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml @@ -14,8 +14,8 @@ AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColo AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml index 1789c9fa5..b86a5b2eb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml @@ -22,8 +22,8 @@ Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Applica Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") } -ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") +ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1. Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2. Validates credentials using", $techn="", $tags="Relationship", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml index 149271a67..abd0c8043 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml @@ -87,9 +87,9 @@ skinparam rectangle<> { rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem -database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp +database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database rectangle "API Application\n[Container: Java and Spring MVC]" <> { rectangle "==Sign In Controller\n[Component: Spring MVC Rest Controller]\n\nAllows users to sign in to the Internet Banking System." <> as InternetBankingSystem.APIApplication.SignInController diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml index b7611a0b1..413f4b3a3 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml @@ -19,13 +19,13 @@ skinparam rectangle<> BorderColor #2e6295 shadowing false } -skinparam rectangle<> { +skinparam rectangle<> { BackgroundColor #ffffff FontColor #000000 BorderColor #888888 shadowing false } -skinparam rectangle<> { +skinparam rectangle<> { BackgroundColor #ffffff FontColor #000000 BorderColor #888888 @@ -49,13 +49,13 @@ skinparam rectangle<> { BorderColor #888888 shadowing false } -skinparam database<> { +skinparam database<> { BackgroundColor #438dd5 FontColor #ffffff BorderColor #2e6295 shadowing false } -skinparam database<> { +skinparam database<> { BackgroundColor #438dd5 FontColor #ffffff BorderColor #2e6295 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml index aad19e957..fc0b86f0e 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml @@ -48,8 +48,8 @@ rectangle "API Application\n[Container: Java and Spring MVC]" << rectangle "==Security Component\n[Component: Spring Bean]\n\nProvides functionality related to signing in, changing passwords, etc." <> as InternetBankingSystem.APIApplication.SecurityComponent } -database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication +database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "1. Submits credentials to\n[JSON/HTTPS]" InternetBankingSystem.APIApplication.SignInController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "2. Validates credentials using" From 586a32b15f972612a08a7fbf5ce84464fc29162b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 1 Mar 2024 17:16:09 +0000 Subject: [PATCH 199/418] Fixes #258. --- changelog.md | 1 + .../plantuml/StructurizrPlantUMLExporter.java | 2 +- ...ructurizrPlantUMLDiagramExporterTests.java | 345 ++++++++++-------- .../structurizr/36141-SystemContext.puml | 2 +- .../structurizr/36141-SystemLandscape.puml | 2 +- ...mic-view-container-scoped-with-groups.puml | 4 +- ...ew-software-system-scoped-with-groups.puml | 4 +- .../dynamic-view-unscoped-with-groups.puml | 4 +- .../plantuml/structurizr/group-styles-1.puml | 6 +- .../plantuml/structurizr/group-styles-2.puml | 6 +- .../structurizr/groups-Components.puml | 2 +- .../structurizr/groups-Containers.puml | 2 +- .../structurizr/groups-SystemLandscape.puml | 6 +- .../plantuml/structurizr/nested-groups.puml | 10 +- 14 files changed, 224 insertions(+), 172 deletions(-) diff --git a/changelog.md b/changelog.md index f28646c24..812dc8f25 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ - structurizr-client: Fixes https://github.com/structurizr/java/issues/257 (Serialization to JSON is not deterministic). - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/252 (DSL parser does not seem to handle curly brackets balance). - structurizr-dsl: Deprecates `!constant`, adds `!const` and `!var` (see https://github.com/structurizr/java/issues/253). +- structurizr-export: Fixes https://github.com/structurizr/java/issues/258 (Plantuml renderer: Group and system of same name yields puml code resulting in error). ## 2.0.0 (22nd February 2024) diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 97d92a818..320213b11 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -194,7 +194,7 @@ protected void startGroupBoundary(ModelView view, String group, IndentingWriter icon = "\\n\\n"; } - writer.writeLine(String.format("rectangle \"%s%s\" <> {", groupName, icon, groupId)); + writer.writeLine(String.format("rectangle \"%s%s\" <> as group%s {", groupName, icon, groupId, groupId)); writer.indent(); writer.writeLine(String.format("skinparam RectangleBorderColor<> %s", groupId, color)); writer.writeLine(String.format("skinparam RectangleFontColor<> %s", groupId, color)); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 4effe64c2..19869f588 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -611,38 +611,39 @@ public void staticDiagramsAreUnchangedWhenSequenceDiagramsAreEnabled() { StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); Diagram diagram; - String expected = "@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "skinparam {\n" + - " arrowFontSize 10\n" + - " defaultTextAlignment center\n" + - " wrapWidth 200\n" + - " maxMessageSize 100\n" + - "}\n" + - "\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "\n" + - "rectangle \"Group\" <> {\n" + - " skinparam RectangleBorderColor<> #cccccc\n" + - " skinparam RectangleFontColor<> #cccccc\n" + - " skinparam RectangleBorderStyle<> dashed\n" + - "\n" + - " rectangle \"==Software System\\n[Software System]\" <> as SoftwareSystem\n" + - "}\n" + - "\n" + - "\n" + - "@enduml"; + String expected = """ + @startuml + set separator none + title System Landscape + + top to bottom direction + + skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 + } + + hide stereotype + + skinparam rectangle<> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false + } + + rectangle "Group" <> as group1 { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "==Software System\\n[Software System]" <> as SoftwareSystem + } + + + @enduml"""; diagram = exporter.export(view); assertEquals(expected, diagram.getDefinition()); @@ -789,59 +790,60 @@ public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSepar ContainerView view = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); view.addAllElements(); - String expectedResult = "@startuml\n" + - "set separator none\n" + - "title Software System - Containers\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "skinparam {\n" + - " arrowFontSize 10\n" + - " defaultTextAlignment center\n" + - " wrapWidth 200\n" + - " maxMessageSize 100\n" + - "}\n" + - "\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BorderColor #9a9a9a\n" + - " FontColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "\n" + - "rectangle \"Software System\\n[Software System]\" <> {\n" + - " rectangle \"Group 1\" <> {\n" + - " skinparam RectangleBorderColor<> #cccccc\n" + - " skinparam RectangleFontColor<> #cccccc\n" + - " skinparam RectangleBorderStyle<> dashed\n" + - "\n" + - " rectangle \"==Container 1\\n[Container]\" <> as SoftwareSystem.Container1\n" + - " }\n" + - "\n" + - " rectangle \"Group 2\" <> {\n" + - " skinparam RectangleBorderColor<> #cccccc\n" + - " skinparam RectangleFontColor<> #cccccc\n" + - " skinparam RectangleBorderStyle<> dashed\n" + - "\n" + - " rectangle \"==Container 2\\n[Container]\" <> as SoftwareSystem.Container2\n" + - " }\n" + - "\n" + - "}\n" + - "\n" + - "@enduml"; + String expectedResult = """ + @startuml + set separator none + title Software System - Containers + + top to bottom direction + + skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 + } + + hide stereotype + + skinparam rectangle<> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false + } + skinparam rectangle<> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false + } + skinparam rectangle<> { + BorderColor #9a9a9a + FontColor #9a9a9a + shadowing false + } + + rectangle "Software System\\n[Software System]" <> { + rectangle "Group 1" <> as group1 { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "==Container 1\\n[Container]" <> as SoftwareSystem.Container1 + } + + rectangle "Group 2" <> as group2 { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "==Container 2\\n[Container]" <> as SoftwareSystem.Container2 + } + + } + + @enduml"""; StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); Diagram diagram = exporter.export(view); @@ -874,67 +876,68 @@ public void deploymentView_WithGroups() { view.add(infrastructureNode2); view.add(softwareSystemInstance); - String expectedResult = "@startuml\n" + - "set separator none\n" + - "title Deployment - Default\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "skinparam {\n" + - " arrowFontSize 10\n" + - " defaultTextAlignment center\n" + - " wrapWidth 200\n" + - " maxMessageSize 100\n" + - "}\n" + - "\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #ffffff\n" + - " FontColor #000000\n" + - " BorderColor #888888\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "\n" + - "rectangle \"Group 1\" <> {\n" + - " skinparam RectangleBorderColor<> #cccccc\n" + - " skinparam RectangleFontColor<> #cccccc\n" + - " skinparam RectangleBorderStyle<> dashed\n" + - "\n" + - " rectangle \"Server 1\\n[Deployment Node]\" <> as Default.Server1 {\n" + - " rectangle \"Group 2\" <> {\n" + - " skinparam RectangleBorderColor<> #cccccc\n" + - " skinparam RectangleFontColor<> #cccccc\n" + - " skinparam RectangleBorderStyle<> dashed\n" + - "\n" + - " rectangle \"==Infrastructure Node 2\\n[Infrastructure Node]\" <> as Default.Server1.InfrastructureNode2\n" + - " rectangle \"==Software System\\n[Software System]\" <> as Default.Server1.SoftwareSystem_1\n" + - " }\n" + - "\n" + - " rectangle \"==Infrastructure Node 1\\n[Infrastructure Node]\" <> as Default.Server1.InfrastructureNode1\n" + - " }\n" + - "\n" + - "}\n" + - "\n" + - "@enduml"; + String expectedResult = """ + @startuml + set separator none + title Deployment - Default + + top to bottom direction + + skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 + } + + hide stereotype + + skinparam rectangle<> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false + } + skinparam rectangle<> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false + } + skinparam rectangle<> { + BackgroundColor #ffffff + FontColor #000000 + BorderColor #888888 + shadowing false + } + skinparam rectangle<> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false + } + + rectangle "Group 1" <> as group1 { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "Server 1\\n[Deployment Node]" <> as Default.Server1 { + rectangle "Group 2" <> as group2 { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "==Infrastructure Node 2\\n[Infrastructure Node]" <> as Default.Server1.InfrastructureNode2 + rectangle "==Software System\\n[Software System]" <> as Default.Server1.SoftwareSystem_1 + } + + rectangle "==Infrastructure Node 1\\n[Infrastructure Node]" <> as Default.Server1.InfrastructureNode1 + } + + } + + @enduml"""; StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); Diagram diagram = exporter.export(view); @@ -1026,6 +1029,54 @@ public void test_ElementInstanceUrl() { rectangle "==A\\n[Software System]" <> as Default.Node.A_1 [[https://example.com/url2]] } -@enduml""", new StructurizrPlantUMLExporter().export(view).getDefinition()); } +@enduml""", new StructurizrPlantUMLExporter().export(view).getDefinition()); + + } + + @Test + void groupAndSoftwareSystemNameAreTheSame() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + softwareSystem.setGroup("Name"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.add(softwareSystem); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + set separator none + title System Landscape + + top to bottom direction + + skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 + } + + hide stereotype + + skinparam rectangle<> { + BackgroundColor #dddddd + FontColor #000000 + BorderColor #9a9a9a + shadowing false + } + + rectangle "Name" <> as group1 { + skinparam RectangleBorderColor<> #cccccc + skinparam RectangleFontColor<> #cccccc + skinparam RectangleBorderStyle<> dashed + + rectangle "==Name\\n[Software System]" <> as Name + } + + + @enduml""", diagram.getDefinition()); + } } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml index d4f946a23..5b0dc94ec 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml @@ -38,7 +38,7 @@ skinparam person<> { shadowing false } -rectangle "Big Bank plc" <> { +rectangle "Big Bank plc" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml index b042bb6a5..0518cb173 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml @@ -56,7 +56,7 @@ skinparam person<> { shadowing false } -rectangle "Big Bank plc" <> { +rectangle "Big Bank plc" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml index bd0fade3d..304b863f3 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml @@ -37,7 +37,7 @@ skinparam rectangle<> { } rectangle "A\n[Container]" <> { - rectangle "Group 1" <> { + rectangle "Group 1" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed @@ -48,7 +48,7 @@ rectangle "A\n[Container]" <> { } rectangle "B\n[Container]" <> { - rectangle "Group 2" <> { + rectangle "Group 2" <> as group2 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml index 13cc2141b..a430197fb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml @@ -37,7 +37,7 @@ skinparam rectangle<> { } rectangle "A\n[Software System]" <> { - rectangle "Group 1" <> { + rectangle "Group 1" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed @@ -48,7 +48,7 @@ rectangle "A\n[Software System]" <> { } rectangle "B\n[Software System]" <> { - rectangle "Group 2" <> { + rectangle "Group 2" <> as group2 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml index 5b7656db8..8dc424024 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml @@ -26,7 +26,7 @@ skinparam rectangle<> { shadowing false } -rectangle "Group 1" <> { +rectangle "Group 1" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed @@ -34,7 +34,7 @@ rectangle "Group 1" <> { rectangle "==A\n[Software System]" <> as A } -rectangle "Group 2" <> { +rectangle "Group 2" <> as group2 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml index 8406d0994..997f1738d 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml @@ -32,7 +32,7 @@ skinparam rectangle<> { shadowing false } -rectangle "Group 1\n\n" <> { +rectangle "Group 1\n\n" <> as group1 { skinparam RectangleBorderColor<> #111111 skinparam RectangleFontColor<> #111111 skinparam RectangleBorderStyle<> dashed @@ -40,7 +40,7 @@ rectangle "Group 1\n\n" <> rectangle "==User 1\n[Person]" <> as User1 } -rectangle "Group 2\n\n" <> { +rectangle "Group 2\n\n" <> as group2 { skinparam RectangleBorderColor<> #222222 skinparam RectangleFontColor<> #222222 skinparam RectangleBorderStyle<> dashed @@ -48,7 +48,7 @@ rectangle "Group 2\n\n" <> rectangle "==User 2\n[Person]" <> as User2 } -rectangle "Group 3" <> { +rectangle "Group 3" <> as group3 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml index 5c51fe4d3..82c0a1b75 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml @@ -32,7 +32,7 @@ skinparam rectangle<> { shadowing false } -rectangle "Group 1\n\n" <> { +rectangle "Group 1\n\n" <> as group1 { skinparam RectangleBorderColor<> #111111 skinparam RectangleFontColor<> #111111 skinparam RectangleBorderStyle<> dashed @@ -40,7 +40,7 @@ rectangle "Group 1\n\n" <> rectangle "==User 1\n[Person]" <> as User1 } -rectangle "Group 2\n\n" <> { +rectangle "Group 2\n\n" <> as group2 { skinparam RectangleBorderColor<> #222222 skinparam RectangleFontColor<> #222222 skinparam RectangleBorderStyle<> dashed @@ -48,7 +48,7 @@ rectangle "Group 2\n\n" <> rectangle "==User 2\n[Person]" <> as User2 } -rectangle "Group 3" <> { +rectangle "Group 3" <> as group3 { skinparam RectangleBorderColor<> #aabbcc skinparam RectangleFontColor<> #aabbcc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml index 04af47b24..8528285cb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml @@ -40,7 +40,7 @@ skinparam rectangle<> { rectangle "==C\n[Software System]" <> as C rectangle "F\n[Container]" <> { - rectangle "Group 5" <> { + rectangle "Group 5" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml index f7d532695..fe0ad43b9 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml @@ -40,7 +40,7 @@ skinparam rectangle<> { rectangle "==C\n[Software System]" <> as C rectangle "D\n[Software System]" <> { - rectangle "Group 4" <> { + rectangle "Group 4" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml index c967286be..7c398d745 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml @@ -38,7 +38,7 @@ skinparam rectangle<> { shadowing false } -rectangle "Group 1" <> { +rectangle "Group 1" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed @@ -46,13 +46,13 @@ rectangle "Group 1" <> { rectangle "==B\n[Software System]" <> as B } -rectangle "Group 2" <> { +rectangle "Group 2" <> as group2 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed rectangle "==C\n[Software System]" <> as C - rectangle "Group 3" <> { + rectangle "Group 3" <> as group3 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml index 3c3d12a53..04e94c3e8 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml @@ -44,19 +44,19 @@ skinparam rectangle<> { shadowing false } -rectangle "Organisation 1" <> { +rectangle "Organisation 1" <> as group1 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed rectangle "==Organisation 1\n[Software System]" <> as Organisation1 - rectangle "Department 1" <> { + rectangle "Department 1" <> as group2 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed rectangle "==Department 1\n[Software System]" <> as Department1 - rectangle "Team 1" <> { + rectangle "Team 1" <> as group3 { skinparam RectangleBorderColor<> #ff0000 skinparam RectangleFontColor<> #ff0000 skinparam RectangleBorderStyle<> dashed @@ -64,7 +64,7 @@ rectangle "Organisation 1" <> { rectangle "==Team 1\n[Software System]" <> as Team1 } - rectangle "Team 2" <> { + rectangle "Team 2" <> as group4 { skinparam RectangleBorderColor<> #0000ff skinparam RectangleFontColor<> #0000ff skinparam RectangleBorderStyle<> dashed @@ -76,7 +76,7 @@ rectangle "Organisation 1" <> { } -rectangle "Organisation 2" <> { +rectangle "Organisation 2" <> as group5 { skinparam RectangleBorderColor<> #cccccc skinparam RectangleFontColor<> #cccccc skinparam RectangleBorderStyle<> dashed From 06d25b89011b82bf975524a73d3809adccfda06c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 1 Mar 2024 17:16:27 +0000 Subject: [PATCH 200/418] Updates version number. --- build.gradle | 2 +- changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e939207e8..c1f5fd9f1 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.0.0' + version = '2.1.0' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index 812dc8f25..a7ed9887a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## unreleased +## 2.1.0 (unreleased) - structurizr-core: `ViewSet.isEmpty()` was missing a check for image views. - structurizr-client: Fixes https://github.com/structurizr/java/issues/257 (Serialization to JSON is not deterministic). From b80802d0e6643d8981d895ea0b1552af6d982d3c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 2 Mar 2024 08:50:01 +0000 Subject: [PATCH 201/418] Promotes `ModelView.copyLayoutInformationFrom()` to be public, to allow individual view layout information to be merged. --- changelog.md | 1 + .../src/main/java/com/structurizr/view/ModelView.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index a7ed9887a..9d0f97b0b 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## 2.1.0 (unreleased) - structurizr-core: `ViewSet.isEmpty()` was missing a check for image views. +- structurizr-core: Promotes `ModelView.copyLayoutInformationFrom()` to be public, to allow individual view layout information to be merged. - structurizr-client: Fixes https://github.com/structurizr/java/issues/257 (Serialization to JSON is not deterministic). - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/252 (DSL parser does not seem to handle curly brackets balance). - structurizr-dsl: Deprecates `!constant`, adds `!const` and `!var` (see https://github.com/structurizr/java/issues/253). diff --git a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java index 98f5a446a..1128dcd9b 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java @@ -369,7 +369,7 @@ public void setLayoutMergeStrategy(LayoutMergeStrategy layoutMergeStrategy) { * * @param source the source View */ - void copyLayoutInformationFrom(@Nonnull ModelView source) { + public void copyLayoutInformationFrom(@Nonnull ModelView source) { layoutMergeStrategy.copyLayoutInformation(source, this); } From f9be616ee105d1cc0d25074905387a1ce1912b52 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 2 Mar 2024 09:06:24 +0000 Subject: [PATCH 202/418] Updated release date. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 9d0f97b0b..def4a45e0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 2.1.0 (unreleased) +## 2.1.0 (2nd March 2024) - structurizr-core: `ViewSet.isEmpty()` was missing a check for image views. - structurizr-core: Promotes `ModelView.copyLayoutInformationFrom()` to be public, to allow individual view layout information to be merged. From b3fd678f3f2e47be9c1aeba927f17390febb9071 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 3 Mar 2024 09:28:37 +0000 Subject: [PATCH 203/418] structurizr-core: Fixes problem with ordering of relationship view vertices. --- build.gradle | 2 +- changelog.md | 4 ++++ .../java/com/structurizr/view/RelationshipView.java | 11 ++++------- .../src/main/java/com/structurizr/view/Vertex.java | 12 +----------- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index c1f5fd9f1..b877748c2 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.1.0' + version = '2.1.1' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index def4a45e0..17077fa59 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 2.1.1 (3rd March 2024) + +- structurizr-core: Fixes problem with ordering of relationship view vertices. + ## 2.1.0 (2nd March 2024) - structurizr-core: `ViewSet.isEmpty()` was missing a check for image views. diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java index 38b0311fd..29d96c2e1 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java @@ -6,10 +6,7 @@ import com.structurizr.util.StringUtils; import com.structurizr.util.Url; -import java.util.Collection; -import java.util.LinkedList; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; /** * This class represents an instance of a Relationship on a View. @@ -25,7 +22,7 @@ public final class RelationshipView implements Comparable { private String url; private String order; private Boolean response; - private Set vertices = new TreeSet<>(); + private List vertices = new ArrayList<>(); @JsonInclude(value = JsonInclude.Include.NON_NULL) private Routing routing; @@ -158,7 +155,7 @@ void setResponse(Boolean response) { * @return a collection of Vertex objects */ public Collection getVertices() { - return new LinkedList<>(vertices); + return new ArrayList<>(vertices); } /** @@ -168,7 +165,7 @@ public Collection getVertices() { */ public void setVertices(Collection vertices) { if (vertices != null) { - this.vertices = new TreeSet<>(vertices); + this.vertices = new ArrayList<>(vertices); } } diff --git a/structurizr-core/src/main/java/com/structurizr/view/Vertex.java b/structurizr-core/src/main/java/com/structurizr/view/Vertex.java index 84e2a30c8..eda796e23 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Vertex.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Vertex.java @@ -5,7 +5,7 @@ /** * The X, Y coordinate of a bend in a line. */ -public final class Vertex implements Comparable { +public final class Vertex { private int x; private int y; @@ -63,14 +63,4 @@ public int hashCode() { return Objects.hash(x, y); } - @Override - public int compareTo(Vertex vertex) { - int result = getX() - vertex.getX(); - if (result == 0) { - result = getY() - vertex.getY(); - } - - return result; - } - } \ No newline at end of file From 5ae28b0cca6239f7a7c07d6cf33b35f9949bd793 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 15 Mar 2024 15:04:35 +0000 Subject: [PATCH 204/418] Adds some code to deal with old workspaces, or those created by third party tooling that are missing view `order` properties. --- .../api/BackwardsCompatibilityTests.java | 8 ++++++ .../views-without-order.json | 27 +++++++++++++++++++ .../main/java/com/structurizr/view/View.java | 7 ++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 structurizr-client/src/integrationTest/resources/backwardsCompatibility/views-without-order.json diff --git a/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java index e029b5eca..6ae4cb020 100644 --- a/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java +++ b/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java @@ -48,4 +48,12 @@ void documentation() throws Exception { {"configuration":{},"description":"Description","documentation":{"sections":[{"content":"## Heading 1","format":"Markdown","order":1,"title":""}]},"id":0,"model":{},"name":"Name","views":{"configuration":{"branding":{},"styles":{},"terminology":{}}}}""", WorkspaceUtils.toJson(workspace, false)); } + @Test + void viewsWithoutOrderProperties() throws Exception { + File file = new File(PATH_TO_WORKSPACE_FILES, "views-without-order.json"); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(file); + + assertEquals(2, workspace.getViews().getSystemLandscapeViews().size()); + } + } \ No newline at end of file diff --git a/structurizr-client/src/integrationTest/resources/backwardsCompatibility/views-without-order.json b/structurizr-client/src/integrationTest/resources/backwardsCompatibility/views-without-order.json new file mode 100644 index 000000000..c84331f31 --- /dev/null +++ b/structurizr-client/src/integrationTest/resources/backwardsCompatibility/views-without-order.json @@ -0,0 +1,27 @@ +{ + "description" : "Description", + "id" : 0, + "model" : { + "people" : [ { + "id" : "1", + "name" : "User", + "tags" : "Element,Person" + } ] + }, + "name" : "Name", + "views" : { + "systemLandscapeViews" : [ { + "elements" : [ { + "id" : "1" + } ], + "enterpriseBoundaryVisible" : true, + "key" : "SystemLandscape-001" + }, { + "elements" : [ { + "id" : "1" + } ], + "enterpriseBoundaryVisible" : true, + "key" : "SystemLandscape-002" + } ] + } +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/View.java b/structurizr-core/src/main/java/com/structurizr/view/View.java index 510437490..c34e2bf58 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/View.java +++ b/structurizr-core/src/main/java/com/structurizr/view/View.java @@ -162,7 +162,12 @@ void setProperties(Map properties) { @Override public int compareTo(View view) { - return getOrder() - view.getOrder(); + int result = getOrder() - view.getOrder(); + if (result == 0) { + result = getKey().compareToIgnoreCase(view.getKey()); + } + + return result; } } \ No newline at end of file From 03205d6b6416800fed5dd65f00eeac8c4873ace5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 23 Mar 2024 09:37:57 +0000 Subject: [PATCH 205/418] C4-PlantUML borderStyle now lower case (#263). --- changelog.md | 4 ++++ .../export/plantuml/C4PlantUMLExporter.java | 11 +++++----- .../C4PlantUMLDiagramExporterTests.java | 2 +- .../plantuml/c4plantuml/36141-Components.puml | 12 +++++------ .../plantuml/c4plantuml/36141-Containers.puml | 14 ++++++------- .../36141-DevelopmentDeployment.puml | 10 +++++----- .../c4plantuml/36141-LiveDeployment.puml | 14 ++++++------- .../c4plantuml/36141-SignIn-sequence.puml | 6 +++--- .../plantuml/c4plantuml/36141-SignIn.puml | 8 ++++---- .../c4plantuml/36141-SystemContext.puml | 8 ++++---- .../c4plantuml/36141-SystemLandscape.puml | 10 +++++----- ...-AmazonWebServicesDeployment-WithTags.puml | 20 +++++++++---------- .../plantuml/c4plantuml/group-styles-1.puml | 6 +++--- .../plantuml/c4plantuml/group-styles-2.puml | 6 +++--- .../c4plantuml/groups-Components.puml | 2 +- .../c4plantuml/groups-Containers.puml | 2 +- .../c4plantuml/groups-SystemLandscape.puml | 6 +++--- .../nested-groups-with-dot-separator.puml | 6 +++--- .../plantuml/c4plantuml/nested-groups.puml | 10 +++++----- 19 files changed, 80 insertions(+), 77 deletions(-) diff --git a/changelog.md b/changelog.md index 17077fa59..a28edc075 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +- structurizr-export: https://github.com/structurizr/java/issues/263 (Fixes C4PlantUMLExporter not following C4-PlantUML best practices with c4plantuml.tags true) + ## 2.1.1 (3rd March 2024) - structurizr-core: Fixes problem with ordering of relationship view vertices. diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index fa74211fe..d35ea3755 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -194,7 +194,7 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { elementStyle.getColor(), sprite, elementStyle.getProperties().getOrDefault(C4PLANTUML_SHADOW, ""), - elementStyle.getBorder(), + elementStyle.getBorder().toString().toLowerCase(), borderThickness )); } @@ -241,7 +241,7 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { elementStyle.getStroke(), elementStyle.getStroke(), elementStyle.getProperties().getOrDefault(C4PLANTUML_SHADOW, ""), - elementStyle.getBorder(), + elementStyle.getBorder().toString().toLowerCase(), borderThickness )); } @@ -288,7 +288,6 @@ protected void startGroupBoundary(ModelView view, String group, IndentingWriter } String color = "#cccccc"; - String borderStyle = "Dashed"; int borderThickness = 1; // String icon = ""; @@ -302,9 +301,9 @@ protected void startGroupBoundary(ModelView view, String group, IndentingWriter } if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getStroke())) { - borderStyle = elementStyleForGroup.getStroke(); + color = elementStyleForGroup.getStroke(); } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getStroke())) { - borderStyle = elementStyleForAllGroups.getStroke(); + color = elementStyleForAllGroups.getStroke(); } if (elementStyleForGroup != null && elementStyleForGroup.getStrokeWidth() != null) { @@ -330,7 +329,7 @@ protected void startGroupBoundary(ModelView view, String group, IndentingWriter group, color, color, - borderStyle, + Border.Dashed.toString().toLowerCase(), borderThickness) ); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index 8066396c2..49fae0fe2 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -717,7 +717,7 @@ public void borderStyling() { "!include \n" + "!include \n" + "\n" + - "AddElementTag(\"Element\", $bgColor=\"#dddddd\", $borderColor=\"#008000\", $fontColor=\"#000000\", $sprite=\"\", $shadowing=\"\", $borderStyle=\"Dashed\", $borderThickness=\"2\")\n" + + "AddElementTag(\"Element\", $bgColor=\"#dddddd\", $borderColor=\"#008000\", $fontColor=\"#000000\", $sprite=\"\", $shadowing=\"\", $borderStyle=\"dashed\", $borderThickness=\"2\")\n" + "\n" + "System(Name, \"Name\", $descr=\"\", $tags=\"Element\", $link=\"\")\n" + "\n" + diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml index a0d351686..90f8f4f7c 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml @@ -9,15 +9,15 @@ top to bottom direction !include !include -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid", $borderThickness="1") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml index 44fc2b9c6..2107ee2ae 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml @@ -8,16 +8,16 @@ top to bottom direction !include !include -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="solid", $borderThickness="1") Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml index 68f04bd91..556bd25a6 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml @@ -9,11 +9,11 @@ top to bottom direction !include !include -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml index decda55a1..f5a085b48 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml @@ -9,13 +9,13 @@ top to bottom direction !include !include -AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml index 3ee74b0c7..77915aea0 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml @@ -4,9 +4,9 @@ title API Application - Dynamic !include -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml index b86a5b2eb..c06a5ce77 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml @@ -9,13 +9,13 @@ top to bottom direction !include !include -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid", $borderThickness="1") Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml index eb64d4554..94b45a62f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml @@ -7,13 +7,13 @@ top to bottom direction !include !include -AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml index 6a4972739..60fa50dbe 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml @@ -7,14 +7,14 @@ top to bottom direction !include !include -AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { Person(CustomerServiceStaff, "Customer Service Staff", $descr="Customer service staff within the bank.", $tags="Person,Bank Staff", $link="") Person(BackOfficeStaff, "Back Office Staff", $descr="Administration and support staff within the bank.", $tags="Person,Bank Staff", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml index db81378bc..32852ce35 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml @@ -9,16 +9,16 @@ left to right direction !include !include -AddElementTag("Container,Application", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="Solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="Solid", $borderThickness="1") +AddElementTag("Container,Application", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Database", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid", $borderThickness="1") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml index 94c6a07ac..05d0c97eb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml @@ -7,17 +7,17 @@ top to bottom direction !include !include -AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Group 1", $tags="Group 1") { Person(User1, "User 1", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed", $borderThickness="1") Boundary(group_2, "Group 2", $tags="Group 2") { Person(User2, "User 2", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_3, "Group 3", $tags="Group 3") { Person(User3, "User 3", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml index 085b61b85..94d7e5c81 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml @@ -7,17 +7,17 @@ top to bottom direction !include !include -AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Group 1", $tags="Group 1") { Person(User1, "User 1", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed", $borderThickness="1") Boundary(group_2, "Group 2", $tags="Group 2") { Person(User2, "User 2", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 3", $borderColor="#aabbcc", $fontColor="#aabbcc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 3", $borderColor="#aabbcc", $fontColor="#aabbcc", $borderStyle="dashed", $borderThickness="1") Boundary(group_3, "Group 3", $tags="Group 3") { Person(User3, "User 3", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml index b09edaf21..4cb63cacb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml @@ -11,7 +11,7 @@ top to bottom direction System(C, "C", $descr="", $tags="", $link="") Container_Boundary("D.F_boundary", "F", $tags="") { - AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Group 5", $tags="Group 5") { Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml index 2b7569d6b..76539e198 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml @@ -11,7 +11,7 @@ top to bottom direction System(C, "C", $descr="", $tags="", $link="") System_Boundary("D_boundary", "D", $tags="") { - AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Group 4", $tags="Group 4") { Container(D.F, "F", $techn="", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml index a1620f49f..3aee39e27 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml @@ -7,15 +7,15 @@ top to bottom direction !include !include -AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Group 1", $tags="Group 1") { System(B, "B", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_2, "Group 2", $tags="Group 2") { System(C, "C", $descr="", $tags="", $link="") - AddBoundaryTag("Group 2/Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Group 2/Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_3, "Group 3", $tags="Group 2/Group 3") { System(D, "D", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml index cda61f1b3..a22d44fe2 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml @@ -7,11 +7,11 @@ top to bottom direction !include !include -AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Organisation 1", $tags="Organisation 1") { - AddBoundaryTag("Organisation 1.Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1.Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_2, "Department 1", $tags="Organisation 1.Department 1") { - AddBoundaryTag("Organisation 1.Department 1.Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1.Department 1.Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_3, "Team 1", $tags="Organisation 1.Department 1.Team 1") { System(Team1, "Team 1", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml index 783dc9fb8..7ad912df6 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml @@ -7,18 +7,18 @@ top to bottom direction !include !include -AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_1, "Organisation 1", $tags="Organisation 1") { System(Organisation1, "Organisation 1", $descr="", $tags="", $link="") - AddBoundaryTag("Organisation 1/Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1/Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_2, "Department 1", $tags="Organisation 1/Department 1") { System(Department1, "Department 1", $descr="", $tags="", $link="") - AddBoundaryTag("Organisation 1/Department 1/Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1/Department 1/Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_3, "Team 1", $tags="Organisation 1/Department 1/Team 1") { System(Team1, "Team 1", $descr="", $tags="", $link="") } - AddBoundaryTag("Organisation 1/Department 1/Team 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1/Department 1/Team 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_4, "Team 2", $tags="Organisation 1/Department 1/Team 2") { System(Team2, "Team 2", $descr="", $tags="", $link="") } @@ -27,7 +27,7 @@ Boundary(group_1, "Organisation 1", $tags="Organisation 1") { } -AddBoundaryTag("Organisation 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="Dashed", $borderThickness="1") +AddBoundaryTag("Organisation 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") Boundary(group_5, "Organisation 2", $tags="Organisation 2") { System(Organisation2, "Organisation 2", $descr="", $tags="", $link="") } From 39bf3d0890afc4caa52cf3302269a12fc2a4236b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 23 Mar 2024 09:43:34 +0000 Subject: [PATCH 206/418] Suppresses $borderThickness="1" (#263). --- .../export/plantuml/C4PlantUMLExporter.java | 22 +++++++++++++------ .../plantuml/c4plantuml/36141-Components.puml | 12 +++++----- .../plantuml/c4plantuml/36141-Containers.puml | 14 ++++++------ .../36141-DevelopmentDeployment.puml | 10 ++++----- .../c4plantuml/36141-LiveDeployment.puml | 14 ++++++------ .../c4plantuml/36141-SignIn-sequence.puml | 6 ++--- .../plantuml/c4plantuml/36141-SignIn.puml | 8 +++---- .../c4plantuml/36141-SystemContext.puml | 8 +++---- .../c4plantuml/36141-SystemLandscape.puml | 10 ++++----- ...-AmazonWebServicesDeployment-WithTags.puml | 20 ++++++++--------- .../plantuml/c4plantuml/group-styles-1.puml | 6 ++--- .../plantuml/c4plantuml/group-styles-2.puml | 6 ++--- .../c4plantuml/groups-Components.puml | 2 +- .../c4plantuml/groups-Containers.puml | 2 +- .../c4plantuml/groups-SystemLandscape.puml | 6 ++--- .../nested-groups-with-dot-separator.puml | 6 ++--- .../plantuml/c4plantuml/nested-groups.puml | 10 ++++----- 17 files changed, 85 insertions(+), 77 deletions(-) diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index d35ea3755..7566c7b32 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -187,7 +187,7 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { borderThickness = elementStyle.getStrokeWidth(); } - writer.writeLine(String.format("AddElementTag(\"%s\", $bgColor=\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $sprite=\"%s\", $shadowing=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", + String line = String.format("AddElementTag(\"%s\", $bgColor=\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $sprite=\"%s\", $shadowing=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", tagList, elementStyle.getBackground(), elementStyle.getStroke(), @@ -196,7 +196,10 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { elementStyle.getProperties().getOrDefault(C4PLANTUML_SHADOW, ""), elementStyle.getBorder().toString().toLowerCase(), borderThickness - )); + ); + + line = line.replace(", $borderThickness=\"1\")", ")"); + writer.writeLine(line); } } @@ -235,7 +238,7 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { borderThickness = elementStyle.getStrokeWidth(); } - writer.writeLine(String.format("AddBoundaryTag(\"%s\", $bgColor=\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $shadowing=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", + String line = String.format("AddBoundaryTag(\"%s\", $bgColor=\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $shadowing=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", tagList, "#ffffff", elementStyle.getStroke(), @@ -243,7 +246,10 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { elementStyle.getProperties().getOrDefault(C4PLANTUML_SHADOW, ""), elementStyle.getBorder().toString().toLowerCase(), borderThickness - )); + ); + + line = line.replace(", $borderThickness=\"1\")", ")"); + writer.writeLine(line); } } } @@ -325,13 +331,15 @@ protected void startGroupBoundary(ModelView view, String group, IndentingWriter // icon = "\\n\\n"; // } - writer.writeLine(String.format("AddBoundaryTag(\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", + String line = String.format("AddBoundaryTag(\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", group, color, color, Border.Dashed.toString().toLowerCase(), - borderThickness) - ); + borderThickness); + + line = line.replace(", $borderThickness=\"1\")", ")"); + writer.writeLine(line); writer.writeLine(String.format("Boundary(group_%s, \"%s\", $tags=\"%s\") {", groupId, groupName, group)); writer.indent(); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml index 90f8f4f7c..7bce7a631 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml @@ -9,15 +9,15 @@ top to bottom direction !include !include -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml index 2107ee2ae..789bfdee0 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml @@ -8,16 +8,16 @@ top to bottom direction !include !include -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="solid") Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml index 556bd25a6..0158ada25 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml @@ -9,11 +9,11 @@ top to bottom direction !include !include -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml index f5a085b48..177f25979 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml @@ -9,13 +9,13 @@ top to bottom direction !include !include -AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml index 77915aea0..306185a43 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml @@ -4,9 +4,9 @@ title API Application - Dynamic !include -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml index c06a5ce77..2ed68bacd 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml @@ -9,13 +9,13 @@ top to bottom direction !include !include -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml index 94b45a62f..5151c41db 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml @@ -7,13 +7,13 @@ top to bottom direction !include !include -AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml index 60fa50dbe..c49923d47 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml @@ -7,14 +7,14 @@ top to bottom direction !include !include -AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") -AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { Person(CustomerServiceStaff, "Customer Service Staff", $descr="Customer service staff within the bank.", $tags="Person,Bank Staff", $link="") Person(BackOfficeStaff, "Back Office Staff", $descr="Administration and support staff within the bank.", $tags="Person,Bank Staff", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml index 32852ce35..420931723 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml @@ -9,16 +9,16 @@ left to right direction !include !include -AddElementTag("Container,Application", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Container,Database", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid", $borderThickness="1") -AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid", $borderThickness="1") +AddElementTag("Container,Application", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}", $shadowing="", $borderStyle="solid") +AddElementTag("Container,Database", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml index 05d0c97eb..180f8c3aa 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml @@ -7,17 +7,17 @@ top to bottom direction !include !include -AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed") Boundary(group_1, "Group 1", $tags="Group 1") { Person(User1, "User 1", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed") Boundary(group_2, "Group 2", $tags="Group 2") { Person(User2, "User 2", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_3, "Group 3", $tags="Group 3") { Person(User3, "User 3", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml index 94d7e5c81..ff23c0a76 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml @@ -7,17 +7,17 @@ top to bottom direction !include !include -AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed") Boundary(group_1, "Group 1", $tags="Group 1") { Person(User1, "User 1", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed") Boundary(group_2, "Group 2", $tags="Group 2") { Person(User2, "User 2", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 3", $borderColor="#aabbcc", $fontColor="#aabbcc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 3", $borderColor="#aabbcc", $fontColor="#aabbcc", $borderStyle="dashed") Boundary(group_3, "Group 3", $tags="Group 3") { Person(User3, "User 3", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml index 4cb63cacb..ff496fc56 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml @@ -11,7 +11,7 @@ top to bottom direction System(C, "C", $descr="", $tags="", $link="") Container_Boundary("D.F_boundary", "F", $tags="") { - AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_1, "Group 5", $tags="Group 5") { Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml index 76539e198..2f92cc66f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml @@ -11,7 +11,7 @@ top to bottom direction System(C, "C", $descr="", $tags="", $link="") System_Boundary("D_boundary", "D", $tags="") { - AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_1, "Group 4", $tags="Group 4") { Container(D.F, "F", $techn="", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml index 3aee39e27..0e045ac3d 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml @@ -7,15 +7,15 @@ top to bottom direction !include !include -AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_1, "Group 1", $tags="Group 1") { System(B, "B", $descr="", $tags="", $link="") } -AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_2, "Group 2", $tags="Group 2") { System(C, "C", $descr="", $tags="", $link="") - AddBoundaryTag("Group 2/Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Group 2/Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_3, "Group 3", $tags="Group 2/Group 3") { System(D, "D", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml index a22d44fe2..25c92424c 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml @@ -7,11 +7,11 @@ top to bottom direction !include !include -AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_1, "Organisation 1", $tags="Organisation 1") { - AddBoundaryTag("Organisation 1.Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1.Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_2, "Department 1", $tags="Organisation 1.Department 1") { - AddBoundaryTag("Organisation 1.Department 1.Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1.Department 1.Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_3, "Team 1", $tags="Organisation 1.Department 1.Team 1") { System(Team1, "Team 1", $descr="", $tags="", $link="") } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml index 7ad912df6..17b40ad7c 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml @@ -7,18 +7,18 @@ top to bottom direction !include !include -AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_1, "Organisation 1", $tags="Organisation 1") { System(Organisation1, "Organisation 1", $descr="", $tags="", $link="") - AddBoundaryTag("Organisation 1/Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1/Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_2, "Department 1", $tags="Organisation 1/Department 1") { System(Department1, "Department 1", $descr="", $tags="", $link="") - AddBoundaryTag("Organisation 1/Department 1/Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1/Department 1/Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_3, "Team 1", $tags="Organisation 1/Department 1/Team 1") { System(Team1, "Team 1", $descr="", $tags="", $link="") } - AddBoundaryTag("Organisation 1/Department 1/Team 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") + AddBoundaryTag("Organisation 1/Department 1/Team 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_4, "Team 2", $tags="Organisation 1/Department 1/Team 2") { System(Team2, "Team 2", $descr="", $tags="", $link="") } @@ -27,7 +27,7 @@ Boundary(group_1, "Organisation 1", $tags="Organisation 1") { } -AddBoundaryTag("Organisation 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed", $borderThickness="1") +AddBoundaryTag("Organisation 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") Boundary(group_5, "Organisation 2", $tags="Organisation 2") { System(Organisation2, "Organisation 2", $descr="", $tags="", $link="") } From e509974d61f13385845485a70a479f75f9b93c64 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 30 Apr 2024 15:31:57 +0100 Subject: [PATCH 207/418] Updated to reflect release. --- build.gradle | 2 +- changelog.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b877748c2..15453d377 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.1.1' + version = '2.1.2' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index a28edc075..739b77a11 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,9 @@ # Changelog -## unreleased +## 2.1.2 (30th April 2024) -- structurizr-export: https://github.com/structurizr/java/issues/263 (Fixes C4PlantUMLExporter not following C4-PlantUML best practices with c4plantuml.tags true) +- structurizr-core: Adds better backwards compatibility to deal with old workspaces and those created by third party tooling that are missing view `order` property on views. +- structurizr-export: Fixes https://github.com/structurizr/java/issues/263 (C4PlantUMLExporter not following C4-PlantUML best practices with c4plantuml.tags true). ## 2.1.1 (3rd March 2024) From f5780f2e0c2a95cec314771bd5d8baf022f129b3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 30 May 2024 10:49:01 +0100 Subject: [PATCH 208/418] Fixes #298 --- changelog.md | 4 ++++ .../src/main/java/com/structurizr/view/Terminology.java | 3 +++ .../src/test/java/com/structurizr/view/TerminologyTests.java | 2 ++ 3 files changed, 9 insertions(+) diff --git a/changelog.md b/changelog.md index 739b77a11..8c1760d4e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +- structurizr-core: Fixes https://github.com/structurizr/java/issues/298 (Unknown model item type on 'element') + ## 2.1.2 (30th April 2024) - structurizr-core: Adds better backwards compatibility to deal with old workspaces and those created by third party tooling that are missing view `order` property on views. diff --git a/structurizr-core/src/main/java/com/structurizr/view/Terminology.java b/structurizr-core/src/main/java/com/structurizr/view/Terminology.java index b8daf3e1e..575a16a5e 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Terminology.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Terminology.java @@ -134,6 +134,9 @@ public String findTerminology(ModelItem modelItem) { return !StringUtils.isNullOrEmpty(getDeploymentNode()) ? getDeploymentNode() : "Deployment Node"; } else if (modelItem instanceof InfrastructureNode) { return !StringUtils.isNullOrEmpty(getInfrastructureNode()) ? getInfrastructureNode() : "Infrastructure Node"; + } else if (modelItem instanceof CustomElement) { + String terminology = ((CustomElement)modelItem).getMetadata(); + return !StringUtils.isNullOrEmpty(terminology) ? terminology : "Element"; } throw new IllegalArgumentException("Unknown model item type."); diff --git a/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java b/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java index 7f19c8959..316a23bb7 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java @@ -12,6 +12,7 @@ public class TerminologyTests { void findTerminology() { Workspace workspace = new Workspace("Name", "Description"); Terminology terminology = workspace.getViews().getConfiguration().getTerminology(); + CustomElement element = workspace.getModel().addCustomElement("Element", "Hardware Device", "Description"); Person person = workspace.getModel().addPerson("Name"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); Container container = softwareSystem.addContainer("Container"); @@ -22,6 +23,7 @@ void findTerminology() { ContainerInstance containerInstance = deploymentNode.add(container); Relationship relationship = person.uses(softwareSystem, "Uses"); + assertEquals("Hardware Device", terminology.findTerminology(element)); assertEquals("Person", terminology.findTerminology(person)); assertEquals("Software System", terminology.findTerminology(softwareSystem)); assertEquals("Container", terminology.findTerminology(container)); From 746020021b10b2ae429a457491e4827c46f70561 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 16 Jun 2024 13:51:47 +0100 Subject: [PATCH 209/418] Updated to reflect release. --- build.gradle | 2 +- changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 15453d377..8420a6958 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.1.2' + version = '2.1.3' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index 8c1760d4e..f5366e144 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## unreleased +## 2.1.3 (16th June 2024) - structurizr-core: Fixes https://github.com/structurizr/java/issues/298 (Unknown model item type on 'element') From b4211a9674fdf681f87fca2584d5facd57c7dedf Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 18 Jun 2024 14:25:28 +0100 Subject: [PATCH 210/418] Fixes https://github.com/structurizr/java/issues/306 --- build.gradle | 2 +- changelog.md | 7 ++++++- .../main/java/com/structurizr/model/Model.java | 5 +---- .../test/java/com/structurizr/WorkspaceTests.java | 15 +++++++++++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 8420a6958..8588325a7 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.1.3' + version = '2.1.4' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index f5366e144..82310e5f6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,13 @@ # Changelog + +## 2.1.4 (18th June 2024) + +- structurizr-core: Fixes https://github.com/structurizr/java/issues/306 (Workspace.trim() is not correctly removing relationships when the destination of a relationship is removed from the workspace). + ## 2.1.3 (16th June 2024) -- structurizr-core: Fixes https://github.com/structurizr/java/issues/298 (Unknown model item type on 'element') +- structurizr-core: Fixes https://github.com/structurizr/java/issues/298 (Unknown model item type on 'element'). ## 2.1.2 (30th April 2024) diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 3e49dd9e0..25d6c9d2d 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -1149,12 +1149,9 @@ private void removeElement(Element element) { // remove any relationships to/from the element for (Relationship relationship : getRelationships()) { - if (relationship.getSource() == element) { + if (relationship.getSource() == element || relationship.getDestination() == element) { removeRelationshipFromInternalStructures(relationship); relationship.getSource().remove(relationship); - } else if (relationship.getDestination() == element) { - removeRelationshipFromInternalStructures(relationship); - relationship.getDestination().remove(relationship); } } diff --git a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java index b5b741da1..c22b582be 100644 --- a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java @@ -285,4 +285,19 @@ void trim_WhenSomeElementsAreUnused() { assertTrue(workspace.getModel().contains(bc)); } + @Test + void trim_WhenTheDestinationOfAnElementIsRemoved() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + a.uses(b, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.add(a); + + workspace.trim(); + + assertEquals(0, a.getRelationships().size()); + } + } \ No newline at end of file From d455e142aa6d3a429abc6e9c9aba69ebd6b6612a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 27 Jun 2024 18:35:44 +0100 Subject: [PATCH 211/418] Adds support for element/relationship property expressions (closes #297). --- build.gradle | 2 +- changelog.md | 3 + .../java/com/structurizr/model/ModelItem.java | 11 ++ .../dsl/AbstractExpressionParser.java | 42 ++++++++ .../dsl/StructurizrDslExpressions.java | 2 + .../dsl/AbstractExpressionParserTests.java | 102 ++++++++++-------- .../com/structurizr/dsl/AbstractTests.java | 4 +- 7 files changed, 119 insertions(+), 47 deletions(-) diff --git a/build.gradle b/build.gradle index 8588325a7..2913f3c76 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.1.4' + version = '2.2.0' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index 82310e5f6..e547a12e5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # Changelog +## 2.2.0 (unreleased) + +- structurizr-dsl: Adds support for element/relationship property expressions (https://github.com/structurizr/java/issues/297). ## 2.1.4 (18th June 2024) diff --git a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java index 7f936d238..3047740ec 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java @@ -104,6 +104,17 @@ public boolean hasTag(String tag) { return getTagsAsSet().contains(tag.trim()); } + /** + * Determines whether this model item has the given property with the given value. + * + * @param name the name of the property + * @param value the value of the property + * @return true if the named property is present with the given value, false otherwise + */ + public boolean hasProperty(String name, String value) { + return getProperties().containsKey(name) && getProperties().get(name).equals(value); + } + /** * Gets the URL where more information about this item can be found. * diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java index 07f7a36c1..181d51db8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java @@ -24,11 +24,13 @@ static boolean isExpression(String token) { token.startsWith(ELEMENT_TYPE_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(ELEMENT_TAG_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(ELEMENT_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION) || token.startsWith(ELEMENT_PARENT_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(RELATIONSHIP) || token.endsWith(RELATIONSHIP) || token.contains(RELATIONSHIP) || token.startsWith(ELEMENT_EQUALS_EXPRESSION) || token.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.matches(RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION) || token.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(RELATIONSHIP_EQUALS_EXPRESSION); @@ -169,6 +171,15 @@ private Set evaluateExpression(String expr, DslContext context) { modelItems.add(element); } }); + } else if (expr.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION)) { + String propertyName = expr.substring(expr.indexOf("[")+1, expr.indexOf("]")); + String propertyValue = expr.substring(expr.indexOf("==")+2); + + context.getWorkspace().getModel().getElements().forEach(element -> { + if (hasProperty(element, propertyName, propertyValue)) { + modelItems.add(element); + } + }); } else if (expr.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION)) { String[] tags = expr.substring(RELATIONSHIP_TAG_EQUALS_EXPRESSION.length()).split(","); context.getWorkspace().getModel().getRelationships().forEach(relationship -> { @@ -183,6 +194,15 @@ private Set evaluateExpression(String expr, DslContext context) { modelItems.add(relationship); } }); + } else if (expr.matches(RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION)) { + String propertyName = expr.substring(expr.indexOf("[")+1, expr.indexOf("]")); + String propertyValue = expr.substring(expr.indexOf("==")+2); + + context.getWorkspace().getModel().getRelationships().forEach(relationship -> { + if (hasProperty(relationship, propertyName, propertyValue)) { + modelItems.add(relationship); + } + }); } else if (expr.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION)) { String identifier = expr.substring(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.length()); Set sourceElements = new HashSet<>(); @@ -276,6 +296,28 @@ private boolean hasAllTags(ModelItem modelItem, String[] tags) { return result; } + private boolean hasProperty(ModelItem modelItem, String name, String value) { + boolean result = modelItem.hasProperty(name, value); + + if (!result) { + // perhaps the property is instead on a related model item? + if (modelItem instanceof StaticStructureElementInstance) { + StaticStructureElementInstance elementInstance = (StaticStructureElementInstance)modelItem; + result = elementInstance.getElement().hasProperty(name, value); + } else if (modelItem instanceof Relationship) { + Relationship relationship = (Relationship)modelItem; + if (!StringUtils.isNullOrEmpty(relationship.getLinkedRelationshipId())) { + Relationship linkedRelationship = relationship.getModel().getRelationship(relationship.getLinkedRelationshipId()); + if (linkedRelationship != null) { + result = linkedRelationship.hasProperty(name, value); + } + } + } + } + + return result; + } + protected abstract Set findAfferentCouplings(Element element); protected Set findAfferentCouplings(Element element, Class typeOfElement) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java index 8b205bf28..4626e5934 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java @@ -5,12 +5,14 @@ class StructurizrDslExpressions { static final String ELEMENT_TYPE_EQUALS_EXPRESSION = "element.type=="; static final String ELEMENT_TAG_EQUALS_EXPRESSION = "element.tag=="; static final String ELEMENT_TAG_NOT_EQUALS_EXPRESSION = "element.tag!="; + static final String ELEMENT_PROPERTY_EQUALS_EXPRESSION = "element\\.properties\\[.*]==.*"; static final String ELEMENT_EQUALS_EXPRESSION = "element=="; static final String ELEMENT_PARENT_EQUALS_EXPRESSION = "element.parent=="; static final String RELATIONSHIP_TAG_EQUALS_EXPRESSION = "relationship.tag=="; static final String RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION = "relationship.tag!="; + static final String RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION = "relationship\\.properties\\[.*]==.*"; static final String RELATIONSHIP_SOURCE_EQUALS_EXPRESSION = "relationship.source=="; static final String RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION = "relationship.destination=="; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java index 7a81051ce..32bf6fdfb 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java @@ -12,7 +12,7 @@ class AbstractExpressionParserTests extends AbstractTests { - private StaticViewExpressionParser parser = new StaticViewExpressionParser(); + private final StaticViewExpressionParser parser = new StaticViewExpressionParser(); @Test void test_parseExpression_ThrowsAnException_WhenTheRelationshipSourceIsSpecifiedUsingLongSyntaxButDoesNotExist() { @@ -385,35 +385,36 @@ void test_parseExpression_ReturnsAllRelationships_WhenUsingTheWildcardRelationsh } @Test - void test_parseExpression_ReturnsElements_WhenUsingAnElementTagExpression() { + void test_parseExpression_ReturnsElementsAndElementInstances_WhenUsingAnElementTagEqualsExpression() { model.addPerson("User"); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); - - SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); - SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); - context.setWorkspace(workspace); - context.setIdentifierRegister(new IdentifiersRegister()); + SoftwareSystem ss = model.addSoftwareSystem("Software System"); + SoftwareSystemInstance ssi = model.addDeploymentNode("DN").add(ss); - Set elements = parser.parseExpression("element.tag==Software System", context); - assertEquals(1, elements.size()); - assertTrue(elements.contains(softwareSystem)); + Set elements = parser.parseExpression("element.tag==Software System", context()); + assertEquals(2, elements.size()); + assertTrue(elements.contains(ss)); // this is tagged "Software System" + assertTrue(elements.contains(ssi)); // this is not tagged "Software System", but the element it's based upon is } @Test - void test_parseExpression_ReturnsElementInstances_WhenUsingAnElementTagExpression() { - model.addPerson("User"); - SoftwareSystem ss = model.addSoftwareSystem("Software System"); - SoftwareSystemInstance ssi = model.addDeploymentNode("DN").add(ss); + void test_parseExpression_ReturnsElementsAndElementInstances_WhenUsingAnElementPropertyEqualsExpression() { + SoftwareSystem a = model.addSoftwareSystem("A"); + a.addProperty("Technical Debt", "Low"); + SoftwareSystem b = model.addSoftwareSystem("B"); + b.addProperty("Technical Debt", "Medium"); + SoftwareSystem c = model.addSoftwareSystem("C"); + c.addProperty("Technical Debt", "High"); + SoftwareSystem d = model.addSoftwareSystem("D"); - DeploymentView view = views.createDeploymentView("key", "Description"); - DeploymentViewDslContext context = new DeploymentViewDslContext(view); - context.setWorkspace(workspace); - context.setIdentifierRegister(new IdentifiersRegister()); + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + SoftwareSystemInstance ai = deploymentNode.add(a); + SoftwareSystemInstance bi = deploymentNode.add(b); + SoftwareSystemInstance ci = deploymentNode.add(c); - Set elements = parser.parseExpression("element.tag==Software System", context); + Set elements = parser.parseExpression("element.properties[Technical Debt]==High", context()); assertEquals(2, elements.size()); - assertTrue(elements.contains(ss)); // this is tagged "Software System" - assertTrue(elements.contains(ssi)); // this is not tagged "Software System", but the element it's based upon is + assertTrue(elements.contains(c)); // this has the property + assertTrue(elements.contains(ci)); // this doesn't have the property, but the element it's based upon does } @Test @@ -465,42 +466,53 @@ void test_parseExpression_ReturnsElements_WhenUsingAnElementParentExpression() { } @Test - void test_parseExpression_ReturnsRelationships_WhenUsingARelationshipTagExpression() { + void test_parseExpression_ReturnsRelationshipsAndImpliedRelationships_WhenUsingARelationshipTagEqualsExpression() { + Person user = model.addPerson("User"); SoftwareSystem a = model.addSoftwareSystem("A"); + Container aa = a.addContainer("AA"); SoftwareSystem b = model.addSoftwareSystem("B"); - Relationship r = a.uses(b, "Uses"); - r.addTags("Tag 1"); + Container bb = b.addContainer("BB"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Container cc = c.addContainer("CC"); - SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); - SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); - context.setWorkspace(workspace); - context.setIdentifierRegister(new IdentifiersRegister()); + Relationship r1 = user.uses(aa, "Uses"); + r1.addTags("Tag 1"); + Relationship r2 = user.uses(bb, "Uses"); + r2.addTags("Tag 2"); + Relationship r3 = user.uses(cc, "Uses"); - Set relationships = parser.parseExpression("relationship.tag==Tag 1", context); - assertEquals(1, relationships.size()); - assertTrue(relationships.contains(r)); + Set relationships = parser.parseExpression("relationship.tag==Tag 1", context()); + assertEquals(2, relationships.size()); + assertTrue(relationships.contains(r1)); + + Relationship impliedRelationship = user.getEfferentRelationshipWith(a); + assertTrue(relationships.contains(impliedRelationship)); } @Test - void test_parseExpression_ReturnsRelationships_WhenUsingARelationshipTagExpressionAndTheTagIsSetOnTheLinkedRelationship() { + void test_parseExpression_ReturnsRelationshipsAndImpliedRelationships_WhenUsingARelationshipPropertyEqualsExpression() { + Person user = model.addPerson("User"); SoftwareSystem a = model.addSoftwareSystem("A"); + Container aa = a.addContainer("AA"); SoftwareSystem b = model.addSoftwareSystem("B"); - Relationship r = a.uses(b, "Uses"); - r.addTags("Tag 1"); + Container bb = b.addContainer("BB"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Container cc = c.addContainer("CC"); - DeploymentNode dn = model.addDeploymentNode("DN"); - SoftwareSystemInstance ai = dn.add(a); - SoftwareSystemInstance bi = dn.add(b); + Relationship r1 = user.uses(aa, "Uses"); + r1.addProperty("Secure", "Yes"); + Relationship r2 = user.uses(bb, "Uses"); + r2.addProperty("Secure", "No"); + Relationship r3 = user.uses(cc, "Uses"); - DeploymentView view = views.createDeploymentView("key", "Description"); - DeploymentViewDslContext context = new DeploymentViewDslContext(view); - context.setWorkspace(workspace); - context.setIdentifierRegister(new IdentifiersRegister()); + assertEquals(6, model.getRelationships().size()); - Set relationships = parser.parseExpression("relationship.tag==Tag 1", context); + Set relationships = parser.parseExpression("relationship.properties[Secure]==Yes", context()); assertEquals(2, relationships.size()); - assertTrue(relationships.contains(r)); - assertTrue(relationships.contains(ai.getRelationships().iterator().next())); + assertTrue(relationships.contains(r1)); + + Relationship impliedRelationship = user.getEfferentRelationshipWith(a); + assertTrue(relationships.contains(impliedRelationship)); } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java index 3408890d4..005d8aca8 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java @@ -14,9 +14,11 @@ abstract class AbstractTests { protected Model model = workspace.getModel(); protected ViewSet views = workspace.getViews(); - protected ModelDslContext context() { + AbstractTests() { model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + } + protected ModelDslContext context() { ModelDslContext context = new ModelDslContext(); context.setWorkspace(workspace); From 17964c534fe72ec74627d236108e119396a881d6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 30 Jun 2024 10:27:03 +0100 Subject: [PATCH 212/418] Typo. --- .../src/main/java/com/structurizr/view/ViewSet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java index cab1d8a44..ff395e202 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -54,7 +54,7 @@ public final class ViewSet { } /** - * Creates a custom view view. + * Creates a custom view. * * @param key the key for the view (must be unique) * @param title a title of the view From 1366d3d4615b3b13165b809c2e5b20f784efa920 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 30 Jun 2024 13:58:39 +0100 Subject: [PATCH 213/418] structurizr-dsl: Adds a way to specify the implied relationships strategy by a fully qualified class name via `!impliedRelationships`. --- changelog.md | 1 + .../dsl/ImpliedRelationshipsParser.java | 48 +++++++++++++++---- .../structurizr/dsl/StructurizrDslParser.java | 2 +- .../dsl/ImpliedRelationshipsParserTests.java | 48 ++++++++++++++++--- .../src/test/resources/dsl/test.dsl | 4 ++ 5 files changed, 86 insertions(+), 17 deletions(-) diff --git a/changelog.md b/changelog.md index e547a12e5..fb07a4e31 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## 2.2.0 (unreleased) - structurizr-dsl: Adds support for element/relationship property expressions (https://github.com/structurizr/java/issues/297). +- structurizr-dsl: Adds a way to specify the implied relationships strategy by a fully qualified class name via `!impliedRelationships`. ## 2.1.4 (18th June 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java index f66785ffe..14e781495 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java @@ -2,32 +2,60 @@ import com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy; import com.structurizr.model.DefaultImpliedRelationshipsStrategy; +import com.structurizr.model.ImpliedRelationshipsStrategy; -import java.util.ArrayList; -import java.util.List; +import java.io.File; +import java.lang.reflect.Constructor; +import java.util.Set; final class ImpliedRelationshipsParser extends AbstractParser { - private static final String GRAMMAR = "!impliedRelationships "; + private static final String GRAMMAR = "!impliedRelationships "; - private static final int FLAG_INDEX = 1; + private static final Set BUILT_IN_IMPLIED_RELATIONSHIPS_STRATEGIES = Set.of( + "com.structurizr.model.DefaultImpliedRelationshipsStrategy", + "com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", + "com.structurizr.model.CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy" + ); + + private static final int OPTION_INDEX = 1; + private static final String TRUE = "true"; private static final String FALSE = "false"; - void parse(DslContext context, Tokens tokens) { - // impliedRelationships + void parse(DslContext context, Tokens tokens, File dslFile, boolean restricted) { + // impliedRelationships - if (tokens.hasMoreThan(FLAG_INDEX)) { + if (tokens.hasMoreThan(OPTION_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); } - if (!tokens.includes(FLAG_INDEX)) { + if (!tokens.includes(OPTION_INDEX)) { throw new RuntimeException("Expected: " + GRAMMAR); } - if (tokens.get(FLAG_INDEX).equalsIgnoreCase(FALSE)) { + String option = tokens.get(OPTION_INDEX); + + if (option.equalsIgnoreCase(FALSE)) { context.getWorkspace().getModel().setImpliedRelationshipsStrategy(new DefaultImpliedRelationshipsStrategy()); - } else { + } else if (option.equalsIgnoreCase(TRUE)) { context.getWorkspace().getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + } else { + if (restricted) { + if (!BUILT_IN_IMPLIED_RELATIONSHIPS_STRATEGIES.contains(option)) { + throw new RuntimeException("The implied relationships strategy " + option + " is not available when the DSL parser is running in restricted mode"); + } + } + + try { + Class impliedRelationshipsStrategyClass = context.loadClass(option, dslFile); + Constructor constructor = impliedRelationshipsStrategyClass.getDeclaredConstructor(); + ImpliedRelationshipsStrategy impliedRelationshipsStrategy = (ImpliedRelationshipsStrategy)constructor.newInstance(); + context.getWorkspace().getModel().setImpliedRelationshipsStrategy(impliedRelationshipsStrategy); + } catch (ClassNotFoundException cnfe) { + throw new RuntimeException("Error loading implied relationships strategy: " + option + " was not found"); + } catch (Exception e) { + throw new RuntimeException("Error loading implied relationships strategy: " + e.getMessage()); + } } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 080f52f23..ca6edc3a6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -492,7 +492,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr startContext(new WorkspaceDslContext()); parsedTokens.add(WORKSPACE_TOKEN); } else if (IMPLIED_RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) || IMPLIED_RELATIONSHIPS_TOKEN.substring(1).equalsIgnoreCase(firstToken)) { - new ImpliedRelationshipsParser().parse(getContext(), tokens); + new ImpliedRelationshipsParser().parse(getContext(), tokens, dslFile, restricted); } else if (NAME_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { new WorkspaceParser().parseName(getContext(), tokens); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java index a8e4e8585..eb7ded0f6 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java @@ -2,6 +2,8 @@ import org.junit.jupiter.api.Test; +import java.io.File; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -12,35 +14,69 @@ class ImpliedRelationshipsParserTests extends AbstractTests { @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(), tokens("!impliedRelationships", "boolean", "extra")); + parser.parse(context(), tokens("!impliedRelationships", "boolean", "extra"), null, false); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: !impliedRelationships ", e.getMessage()); + assertEquals("Too many tokens, expected: !impliedRelationships ", e.getMessage()); } } @Test void test_parse_ThrowsAnException_WhenNoFlagIsSpecified() { try { - parser.parse(context(), tokens("!impliedRelationships")); + parser.parse(context(), tokens("!impliedRelationships"), null, false); fail(); } catch (Exception e) { - assertEquals("Expected: !impliedRelationships ", e.getMessage()); + assertEquals("Expected: !impliedRelationships ", e.getMessage()); } } @Test void test_parse_SetsTheStrategy_WhenFalseIsSpecified() { - parser.parse(context(), tokens("!impliedRelationships", "false")); + parser.parse(context(), tokens("!impliedRelationships", "false"), null, false); assertEquals("com.structurizr.model.DefaultImpliedRelationshipsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); } @Test void test_parse_SetsTheStrategy_WhenTrueIsSpecified() { - parser.parse(context(), tokens("!impliedRelationships", "true")); + parser.parse(context(), tokens("!impliedRelationships", "true"), null, false); assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); } + @Test + void test_parse_SetsTheStrategy_WhenABuiltInStrategyIsUsedInUnrestrictedMode() { + parser.parse(context(), tokens("!impliedRelationships", "com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy"), new File("."), false); + + assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); + } + + @Test + void test_parse_SetsTheStrategy_WhenABuiltInStrategyIsUsedInRestrictedMode() { + parser.parse(context(), tokens("!impliedRelationships", "com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy"), new File("."), true); + + assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); + } + + @Test + void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedInRestrictedMode() { + try { + parser.parse(context(), tokens("!impliedRelationships", "com.example.CustomImpliedRelationshipsStrategy"), new File("."), true); + fail(); + } catch (Exception e) { + assertEquals("The implied relationships strategy com.example.CustomImpliedRelationshipsStrategy is not available when the DSL parser is running in restricted mode", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedInUnrestrictedModeButCannotBeLoaded() { + try { + parser.parse(context(), tokens("!impliedRelationships", "com.example.CustomImpliedRelationshipsStrategy"), new File("."), false); + fail(); + } catch (Exception e) { + assertEquals("Error loading implied relationships strategy: com.example.CustomImpliedRelationshipsStrategy was not found", e.getMessage()); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 3fe2f64d1..26fb5761c 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -23,6 +23,10 @@ workspace "Name" "Description" { // single line comment model { + !impliedRelationships false + !impliedRelationships "com.structurizr.model.CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy" + !impliedRelationships true + properties { "Name" "Value" } From e0440ddd04361a4782d55cdd7f9236a15c75c5f3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 2 Jul 2024 09:29:24 +0100 Subject: [PATCH 214/418] Adds the ability to include single files as documentation (closes #303). --- changelog.md | 1 + .../java/com/structurizr/dsl/DocsParser.java | 6 +--- .../java/com/structurizr/dsl/DslTests.java | 33 +++++++++++++++++-- .../resources/dsl/docs/docs/01-context.md | 5 --- .../dsl/docs/docs/softwaresystem/1.md | 3 ++ .../docs/docs/softwaresystem/container/1.md | 3 ++ .../softwaresystem/container/component/1.md | 3 ++ .../resources/dsl/docs/docs/workspace/1.md | 3 ++ .../src/test/resources/dsl/docs/workspace.dsl | 8 ++--- 9 files changed, 48 insertions(+), 17 deletions(-) delete mode 100644 structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md create mode 100644 structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/1.md create mode 100644 structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/1.md create mode 100644 structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/component/1.md create mode 100644 structurizr-dsl/src/test/resources/dsl/docs/docs/workspace/1.md diff --git a/changelog.md b/changelog.md index fb07a4e31..9bd3d7285 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - structurizr-dsl: Adds support for element/relationship property expressions (https://github.com/structurizr/java/issues/297). - structurizr-dsl: Adds a way to specify the implied relationships strategy by a fully qualified class name via `!impliedRelationships`. +- structurizr-dsl: Adds the ability to include single files as documentation (https://github.com/structurizr/java/issues/303). ## 2.1.4 (18th June 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java index 4eaba7a23..c758781f9 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java @@ -55,17 +55,13 @@ private void parse(DslContext context, Documentable documentable, File dslFile, throw new RuntimeException("Documentation path " + path + " does not exist"); } - if (!path.isDirectory()) { - throw new RuntimeException("Documentation path " + path + " is not a directory"); - } - try { Class documentationImporterClass = context.loadClass(fullyQualifiedClassName, dslFile); Constructor constructor = documentationImporterClass.getDeclaredConstructor(); DocumentationImporter documentationImporter = (DocumentationImporter)constructor.newInstance(); documentationImporter.importDocumentation(documentable, path); - if (!tokens.includes(FQN_INDEX)) { + if (!tokens.includes(FQN_INDEX) && path.isDirectory()) { DefaultImageImporter imageImporter = new DefaultImageImporter(); imageImporter.importDocumentation(documentable, path); } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index e8f7fa5c1..45579713f 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1,6 +1,7 @@ package com.structurizr.dsl; import com.structurizr.Workspace; +import com.structurizr.documentation.Section; import com.structurizr.model.*; import com.structurizr.view.*; import org.junit.jupiter.api.Test; @@ -12,6 +13,7 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.Base64; +import java.util.Collection; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -692,9 +694,34 @@ void test_docs() throws Exception { Container container = softwareSystem.getContainerWithName("Container"); Component component = container.getComponentWithName("Component"); - assertEquals(1, parser.getWorkspace().getDocumentation().getSections().size()); - assertEquals(1, softwareSystem.getDocumentation().getSections().size()); - assertEquals(1, container.getDocumentation().getSections().size()); + Collection
sections = parser.getWorkspace().getDocumentation().getSections(); + assertEquals(1, sections.size()); + assertEquals(""" + ## Workspace + + Content...""", sections.iterator().next().getContent()); + + sections = softwareSystem.getDocumentation().getSections(); + assertEquals(1, sections.size()); + assertEquals(""" + ## Software System + + Content...""", sections.iterator().next().getContent()); + + sections = container.getDocumentation().getSections(); + assertEquals(1, sections.size()); + assertEquals(""" + ## Container + + Content...""", sections.iterator().next().getContent()); + + sections = component.getDocumentation().getSections(); + assertEquals(1, sections.size()); + assertEquals(""" + ## Component + + Content...""", sections.iterator().next().getContent()); + assertEquals(1, component.getDocumentation().getSections().size()); } diff --git a/structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md b/structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md deleted file mode 100644 index ddb90b8f7..000000000 --- a/structurizr-dsl/src/test/resources/dsl/docs/docs/01-context.md +++ /dev/null @@ -1,5 +0,0 @@ -## Context - -Here is a description of my software system... - -![](embed:Diagram1) \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/1.md b/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/1.md new file mode 100644 index 000000000..7210af0e9 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/1.md @@ -0,0 +1,3 @@ +## Software System + +Content... \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/1.md b/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/1.md new file mode 100644 index 000000000..7af941d8d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/1.md @@ -0,0 +1,3 @@ +## Container + +Content... \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/component/1.md b/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/component/1.md new file mode 100644 index 000000000..4f7859729 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/docs/docs/softwaresystem/container/component/1.md @@ -0,0 +1,3 @@ +## Component + +Content... \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/docs/docs/workspace/1.md b/structurizr-dsl/src/test/resources/dsl/docs/docs/workspace/1.md new file mode 100644 index 000000000..ae8e89c20 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/docs/docs/workspace/1.md @@ -0,0 +1,3 @@ +## Workspace + +Content... \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl index 937977cc4..1f2a383f0 100644 --- a/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl @@ -1,17 +1,17 @@ workspace { - !docs docs com.structurizr.example.ExampleDocumentationImporter + !docs docs/workspace com.structurizr.example.ExampleDocumentationImporter model { user = person "User" softwareSystem = softwareSystem "Software System" { - !docs docs + !docs docs/softwaresystem container "Container" { - !docs docs + !docs docs/softwaresystem/container component "Component" { - !docs docs + !docs docs/softwaresystem/container/component/1.md } } } From 2cc351d061551630c5c7b690081438beeb2f29aa Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 2 Jul 2024 11:03:24 +0100 Subject: [PATCH 215/418] Updated to reflect release. --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 9bd3d7285..f9f5224db 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,9 @@ # Changelog -## 2.2.0 (unreleased) +## 2.2.0 (2nd July 2024) - structurizr-dsl: Adds support for element/relationship property expressions (https://github.com/structurizr/java/issues/297). -- structurizr-dsl: Adds a way to specify the implied relationships strategy by a fully qualified class name via `!impliedRelationships`. +- structurizr-dsl: Adds a way to specify the implied relationships strategy via a fully qualified class name when using `!impliedRelationships`. - structurizr-dsl: Adds the ability to include single files as documentation (https://github.com/structurizr/java/issues/303). ## 2.1.4 (18th June 2024) From 53519eba7c72114e67abe739086b223e41b281f2 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 19 Jul 2024 11:03:41 +0100 Subject: [PATCH 216/418] Fixes #312. --- changelog.md | 4 ++++ .../java/com/structurizr/dsl/StructurizrDslParser.java | 4 ++++ .../src/test/resources/dsl/workspace-with-bom-model.dsl | 6 ++++++ .../src/test/resources/dsl/workspace-with-bom.dsl | 7 +------ 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/workspace-with-bom-model.dsl diff --git a/changelog.md b/changelog.md index f9f5224db..6b9a9fcdc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). + ## 2.2.0 (2nd July 2024) - structurizr-dsl: Adds support for element/relationship property expressions (https://github.com/structurizr/java/issues/297). diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index ca6edc3a6..e61738f58 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -248,6 +248,10 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr for (IncludedFile includedFile : context.getFiles()) { List paddedLines = new ArrayList<>(); for (String unpaddedLine : includedFile.getLines()) { + if (unpaddedLine.startsWith(BOM)) { + // this caters for files encoded as "UTF-8 with BOM" + unpaddedLine = unpaddedLine.substring(1); + } paddedLines.add(leadingSpace + unpaddedLine); } diff --git a/structurizr-dsl/src/test/resources/dsl/workspace-with-bom-model.dsl b/structurizr-dsl/src/test/resources/dsl/workspace-with-bom-model.dsl new file mode 100644 index 000000000..e54770cfa --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/workspace-with-bom-model.dsl @@ -0,0 +1,6 @@ + model { + user = person "User" "A user of my software system." + softwareSystem = softwareSystem "Software System" "My software system, code-named \"X\"." + + user -> softwareSystem "Uses" + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl b/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl index c5556fd63..b0f7a64b0 100644 --- a/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl +++ b/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl @@ -1,11 +1,6 @@ workspace "Getting Started" "This is a model of my software system." { - model { - user = person "User" "A user of my software system." - softwareSystem = softwareSystem "Software System" "My software system, code-named \"X\"." - - user -> softwareSystem "Uses" - } + !include workspace-with-bom-model.dsl views { systemContext softwareSystem "SystemContext" "An example of a System Context diagram." { From ca88ead1d34f838060655a3d706f3dee78ad5244 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 20 Jul 2024 17:07:39 +0100 Subject: [PATCH 217/418] Adds a way to explicitly specify the order of relationships in dynamic views. --- changelog.md | 1 + .../structurizr/view/RelationshipView.java | 2 +- .../dsl/DynamicViewContentParser.java | 36 ++++++++++++++----- .../main/java/com/structurizr/dsl/Tokens.java | 4 +++ .../java/com/structurizr/dsl/DslTests.java | 16 ++++++--- .../dsl/DynamicViewContentParserTests.java | 4 +-- .../dynamic-view-with-explicit-ordering.dsl | 19 ++++++++++ 7 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-ordering.dsl diff --git a/changelog.md b/changelog.md index 6b9a9fcdc..5363e0137 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## unreleased - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). +- structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. ## 2.2.0 (2nd July 2024) diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java index 29d96c2e1..97a222738 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java @@ -132,7 +132,7 @@ public String getOrder() { * * @param order the order, as a String */ - void setOrder(String order) { + public void setOrder(String order) { this.order = order; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java index e96f6babf..1843c148e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java @@ -4,13 +4,16 @@ import com.structurizr.model.Element; import com.structurizr.model.Relationship; import com.structurizr.model.StaticStructureElement; +import com.structurizr.util.StringUtils; import com.structurizr.view.DynamicView; import com.structurizr.view.RelationshipView; final class DynamicViewContentParser extends AbstractParser { - private static final String GRAMMAR_1 = " -> [description] [technology]"; - private static final String GRAMMAR_2 = " [description]"; + private static final String GRAMMAR_1 = "[order:] -> [description] [technology]"; + private static final String GRAMMAR_2 = "[order:] [description]"; + + private static final String ORDER_DELIMITER = ":"; private static final int SOURCE_IDENTIFIER_INDEX = 0; private static final int RELATIONSHIP_TOKEN_INDEX = 1; @@ -22,6 +25,15 @@ final class DynamicViewContentParser extends AbstractParser { RelationshipView parseRelationship(DynamicViewDslContext context, Tokens tokens) { DynamicView view = context.getView(); + RelationshipView relationshipView = null; + String order = null; + + if (tokens.size() > 0 && tokens.get(0).endsWith(ORDER_DELIMITER)) { + // the optional [order:] token + order = tokens.get(0); + order = order.substring(0, order.length()-ORDER_DELIMITER.length()); + tokens.remove(0); + } if (tokens.size() > 1 && StructurizrDslTokens.RELATIONSHIP_TOKEN.equals(tokens.get(RELATIONSHIP_TOKEN_INDEX))) { // -> [description] [technology] @@ -65,16 +77,16 @@ RelationshipView parseRelationship(DynamicViewDslContext context, Tokens tokens) } if (sourceElement instanceof StaticStructureElement && destinationElement instanceof StaticStructureElement) { - return view.add((StaticStructureElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); + relationshipView = view.add((StaticStructureElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); } else if (sourceElement instanceof StaticStructureElement && destinationElement instanceof CustomElement) { - return view.add((StaticStructureElement) sourceElement, description, technology, (CustomElement) destinationElement); + relationshipView = view.add((StaticStructureElement) sourceElement, description, technology, (CustomElement) destinationElement); } else if (sourceElement instanceof CustomElement && destinationElement instanceof StaticStructureElement) { - return view.add((CustomElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); + relationshipView = view.add((CustomElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); } else if (sourceElement instanceof CustomElement && destinationElement instanceof CustomElement) { - return view.add((CustomElement) sourceElement, description, technology, (CustomElement) destinationElement); + relationshipView = view.add((CustomElement) sourceElement, description, technology, (CustomElement) destinationElement); } } else { - // [description] [technology] + // [order] [description] [technology] String relationshipId = tokens.get(RELATIONSHIP_IDENTIFIER_INDEX); Relationship relationship = context.getRelationship(relationshipId); @@ -91,7 +103,15 @@ RelationshipView parseRelationship(DynamicViewDslContext context, Tokens tokens) description = tokens.get(RELATIONSHIP_IDENTIFIER_INDEX+1); } - return view.add(relationship, description); + relationshipView = view.add(relationship, description); + } + + if (relationshipView != null) { + if (!StringUtils.isNullOrEmpty(order)) { + relationshipView.setOrder(order); + } + + return relationshipView; } throw new RuntimeException("The specified relationship could not be added"); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java index f00c1cfc7..ba337b3bf 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java @@ -14,6 +14,10 @@ String get(int index) { return tokens.get(index).trim().replaceAll("\\\\\"", "\"").trim().replaceAll("\\\\n", "\n"); } + void remove(int index) { + tokens.remove(index); + } + int size() { return tokens.size(); } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 45579713f..e34e718c1 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -11,10 +11,7 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collection; -import java.util.List; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -866,6 +863,17 @@ void test_dynamicViewWithCustomElements() throws Exception { parser.parse(new File("src/test/resources/dsl/dynamic-view-with-custom-elements.dsl")); } + @Test + void test_dynamicViewWithExplicitOrdering() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/dynamic-view-with-explicit-ordering.dsl")); + DynamicView view = parser.getWorkspace().getViews().getDynamicViews().iterator().next(); + Set relationships = view.getRelationships(); + Iterator it = relationships.iterator(); + assertEquals("2", it.next().getOrder()); + assertEquals("3", it.next().getOrder()); + } + @Test void test_workspaceProperties() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java index 70bba7b50..a94047a89 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java @@ -19,7 +19,7 @@ void test_parseRelationship_ThrowsAnException_WhenThereAreTooManyTokens() { parser.parseRelationship(new DynamicViewDslContext(null), tokens("source", "->", "destination", "description", "technology", "extra")); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: -> [description] [technology]", e.getMessage()); + assertEquals("Too many tokens, expected: [order:] -> [description] [technology]", e.getMessage()); } } @@ -29,7 +29,7 @@ void test_parseRelationship_ThrowsAnException_WhenTheDestinationIdentifierIsMiss parser.parseRelationship(new DynamicViewDslContext(null), tokens("source", "->")); fail(); } catch (Exception e) { - assertEquals("Expected: -> [description] [technology]", e.getMessage()); + assertEquals("Expected: [order:] -> [description] [technology]", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-ordering.dsl b/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-ordering.dsl new file mode 100644 index 000000000..45bed69c8 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/dynamic-view-with-explicit-ordering.dsl @@ -0,0 +1,19 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + c = softwareSystem "C" + + a -> b + b -> c + } + + views { + dynamic * { + 2: a -> b + 3: b -> c + } + } + +} \ No newline at end of file From 9986f3023d789a7402a616f51939942f462477cb Mon Sep 17 00:00:00 2001 From: klu2 <172195+klu2@users.noreply.github.com> Date: Fri, 9 Aug 2024 12:15:53 +0200 Subject: [PATCH 218/418] #318 return an immutable map in all implementations of PropertyHolder --- .../src/main/java/com/structurizr/AbstractWorkspace.java | 3 ++- .../src/main/java/com/structurizr/model/Model.java | 2 +- .../src/main/java/com/structurizr/model/ModelItem.java | 2 +- .../src/main/java/com/structurizr/view/AbstractStyle.java | 3 ++- .../src/main/java/com/structurizr/view/Configuration.java | 7 ++----- .../src/main/java/com/structurizr/view/View.java | 3 ++- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java index 82f750ace..6bb57bbcd 100644 --- a/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java +++ b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java @@ -3,6 +3,7 @@ import com.structurizr.configuration.WorkspaceConfiguration; import java.lang.reflect.Constructor; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -237,7 +238,7 @@ public void clearConfiguration() { * @return a Map (String, String) (empty if there are no properties) */ public Map getProperties() { - return new HashMap<>(properties); + return Collections.unmodifiableMap(properties); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 25d6c9d2d..6ecb81675 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -1022,7 +1022,7 @@ public void setImpliedRelationshipsStrategy(ImpliedRelationshipsStrategy implied * @return a Map (String, String) (empty if there are no properties) */ public Map getProperties() { - return new HashMap<>(properties); + return Collections.unmodifiableMap(properties); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java index 3047740ec..398eacdc1 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java @@ -152,7 +152,7 @@ public void setUrl(String url) { * @return a Map (String, String) (empty if there are no properties) */ public Map getProperties() { - return new HashMap<>(properties); + return Collections.unmodifiableMap(properties); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java index 28ed28f22..2ad6bb291 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java @@ -2,6 +2,7 @@ import com.structurizr.PropertyHolder; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -15,7 +16,7 @@ public abstract class AbstractStyle implements PropertyHolder { * @return a Map (String, String) (empty if there are no properties) */ public Map getProperties() { - return new HashMap<>(properties); + return Collections.unmodifiableMap(properties); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/Configuration.java b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java index 87780a332..267c79ba3 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Configuration.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java @@ -5,10 +5,7 @@ import com.structurizr.PropertyHolder; import com.structurizr.util.Url; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Configuration associated with how information in the workspace is rendered. @@ -203,7 +200,7 @@ public void setViewSortOrder(ViewSortOrder viewSortOrder) { * @return a Map (String, String) (empty if there are no properties) */ public Map getProperties() { - return new HashMap<>(properties); + return Collections.unmodifiableMap(properties); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/View.java b/structurizr-core/src/main/java/com/structurizr/view/View.java index c34e2bf58..f6ee5539d 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/View.java +++ b/structurizr-core/src/main/java/com/structurizr/view/View.java @@ -5,6 +5,7 @@ import com.structurizr.PropertyHolder; import javax.annotation.Nonnull; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -133,7 +134,7 @@ public ViewSet getViewSet() { * @return a Map (String, String) (empty if there are no properties) */ public Map getProperties() { - return new HashMap<>(properties); + return Collections.unmodifiableMap(properties); } /** From ec75900e813105f1417da9a438a3b612560d1e14 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 15 Aug 2024 09:34:44 +0100 Subject: [PATCH 219/418] Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316). --- changelog.md | 2 + .../structurizr/view/RelationshipView.java | 37 ++++++- .../view/RelationshipViewTests.java | 98 +++++++++++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 3 + .../src/test/resources/dsl/test.dsl | 3 + 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 structurizr-core/src/test/java/com/structurizr/view/RelationshipViewTests.java diff --git a/changelog.md b/changelog.md index 5363e0137..fd80cc2b1 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,8 @@ ## unreleased +- structurizr-core: Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316). +- structurizr-dsl: Adds name-value properties to dynamic view relationship views. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java index 97a222738..9bee72b15 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.structurizr.PropertyHolder; import com.structurizr.model.Relationship; import com.structurizr.util.StringUtils; import com.structurizr.util.Url; @@ -11,7 +12,7 @@ /** * This class represents an instance of a Relationship on a View. */ -public final class RelationshipView implements Comparable { +public final class RelationshipView implements PropertyHolder, Comparable { private static final int START_OF_LINE = 0; private static final int END_OF_LINE = 100; @@ -19,6 +20,7 @@ public final class RelationshipView implements Comparable { private Relationship relationship; private String id; private String description; + private Map properties = new HashMap<>(); private String url; private String order; private Boolean response; @@ -87,6 +89,39 @@ void setDescription(String description) { this.description = description; } + /** + * Gets the collection of name-value property pairs associated with this relationship view, as a Map. + * + * @return a Map (String, String) (empty if there are no properties) + */ + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + /** + * Adds a name-value pair property to this relationship view. + * + * @param name the name of the property + * @param value the value of the property + */ + public void addProperty(String name, String value) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A property name must be specified."); + } + + if (value == null || value.trim().length() == 0) { + throw new IllegalArgumentException("A property value must be specified."); + } + + properties.put(name, value); + } + + void setProperties(Map properties) { + if (properties != null) { + this.properties = new HashMap<>(properties); + } + } + /** * Gets the URL where more information about this relationship instance can be found. * diff --git a/structurizr-core/src/test/java/com/structurizr/view/RelationshipViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/RelationshipViewTests.java new file mode 100644 index 000000000..635414970 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/RelationshipViewTests.java @@ -0,0 +1,98 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class RelationshipViewTests extends AbstractWorkspaceTestBase { + + @Test + void getProperties_ReturnsAnEmptyMap_WhenNoPropertiesHaveBeenAdded() { + RelationshipView relationshipView = new RelationshipView(); + assertEquals(0, relationshipView.getProperties().size()); + } + + @Test + void getProperties_ReturnsAnUnmodifiableMap() { + RelationshipView relationshipView = new RelationshipView(); + try { + relationshipView.getProperties().put("name", "value"); + fail(); + } catch (Exception e) { + assertTrue(e instanceof UnsupportedOperationException); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsNull() { + try { + RelationshipView relationshipView = new RelationshipView(); + relationshipView.addProperty(null, "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + try { + RelationshipView relationshipView = new RelationshipView(); + relationshipView.addProperty(" ", "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsNull() { + try { + RelationshipView relationshipView = new RelationshipView(); + relationshipView.addProperty("name", null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + try { + RelationshipView relationshipView = new RelationshipView(); + relationshipView.addProperty("name", " "); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + RelationshipView relationshipView = new RelationshipView(); + relationshipView.addProperty("name", "value"); + assertEquals("value", relationshipView.getProperties().get("name")); + } + + @Test + void setProperties_DoesNothing_WhenNullIsSpecified() { + RelationshipView relationshipView = new RelationshipView(); + relationshipView.setProperties(null); + assertEquals(0, relationshipView.getProperties().size()); + } + + @Test + void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + RelationshipView relationshipView = new RelationshipView(); + Map properties = new HashMap<>(); + properties.put("name", "value"); + relationshipView.setProperties(properties); + assertEquals(1, relationshipView.getProperties().size()); + assertEquals("value", relationshipView.getProperties().get("name")); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index e61738f58..11633d6ec 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -469,6 +469,9 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewDslContext.class)) { startContext(new PropertiesDslContext(getContext(ViewDslContext.class).getView())); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(DynamicViewRelationshipContext.class)) { + startContext(new PropertiesDslContext(getContext((DynamicViewRelationshipContext.class)).getRelationshipView())); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { startContext(new PropertiesDslContext(getContext((ElementStyleDslContext.class)).getStyle())); diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 26fb5761c..b33be93e0 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -237,6 +237,9 @@ workspace "Name" "Description" { user -> homePageController "Requests via web browser" homePageController -> user { url "https://structurizr.com" + properties { + "Name" "Value" + } } autoLayout From 51f90413f59747e99fec26d41266f7fe59b7a2ae Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 11:21:02 +0100 Subject: [PATCH 220/418] Initial rewrite of old structurizr-analysis library. --- build.gradle | 2 +- settings.gradle | 5 +- structurizr-component/README.md | 9 ++ structurizr-component/build.gradle | 11 ++ .../component/ComponentFinder.java | 133 ++++++++++++++++++ .../component/ComponentFinderBuilder.java | 68 +++++++++ .../component/ComponentFinderStrategy.java | 54 +++++++ .../ComponentFinderStrategyBuilder.java | 56 ++++++++ .../component/DiscoveredComponent.java | 70 +++++++++ .../java/com/structurizr/component/Type.java | 93 ++++++++++++ .../structurizr/component/TypeRepository.java | 22 +++ .../component/filter/DefaultTypeFilter.java | 14 ++ .../ExcludeAbstractClassTypeFilter.java | 14 ++ .../filter/ExcludeTypesByRegexFilter.java | 27 ++++ .../component/filter/TypeFilter.java | 12 ++ .../matcher/AbstractTypeMatcher.java | 19 +++ .../matcher/AnnotationTypeMatcher.java | 48 +++++++ .../component/matcher/ExtendsTypeMatcher.java | 40 ++++++ .../matcher/ImplementsTypeMatcher.java | 32 +++++ .../matcher/NameSuffixTypeMatcher.java | 27 ++++ .../component/matcher/RegexTypeMatcher.java | 43 ++++++ .../component/matcher/TypeMatcher.java | 14 ++ .../CommonsLangCamelCaseNamingStrategy.java | 17 +++ .../naming/DefaultNamingStrategy.java | 14 ++ .../naming/FullyQualifiedNamingStrategy.java | 15 ++ .../component/naming/NamingStrategy.java | 12 ++ .../provider/DirectoryTypeProvider.java | 64 +++++++++ .../provider/JarFileTypeProvider.java | 61 ++++++++ .../provider/JavadocCommentFilter.java | 35 +++++ .../provider/SourceCodeTypeProvider.java | 86 +++++++++++ .../component/provider/TypeProvider.java | 14 ++ ...sInSamePackageSupportingTypesStrategy.java | 22 +++ ...eferencedTypesSupportingTypesStrategy.java | 17 +++ .../DefaultSupportingTypesStrategy.java | 18 +++ .../supporting/SupportingTypesStrategy.java | 14 ++ .../AbstractWorkspaceTestBase.java | 12 ++ .../component/example/Example.java | 50 +++++++ .../controller/CustomerController.java | 19 +++ .../component/example/domain/Customer.java | 4 + .../repository/CustomerRepository.java | 14 ++ .../repository/CustomerRepositoryImpl.java | 15 ++ .../provider/JavadocCommentFilterTests.java | 60 ++++++++ 42 files changed, 1373 insertions(+), 3 deletions(-) create mode 100644 structurizr-component/README.md create mode 100644 structurizr-component/build.gradle create mode 100644 structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/Type.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/filter/TypeFilter.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/naming/FullyQualifiedNamingStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/provider/DirectoryTypeProvider.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/provider/JarFileTypeProvider.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/provider/SourceCodeTypeProvider.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/provider/TypeProvider.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java create mode 100644 structurizr-component/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/Example.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java diff --git a/build.gradle b/build.gradle index 2913f3c76..1f96c1f5e 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.2.0' + version = '2.3.0' repositories { mavenCentral() diff --git a/settings.gradle b/settings.gradle index 70e6822a8..57df82f6d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,10 @@ rootProject.name = 'structurizr-java' -include 'structurizr-inspection' +include 'structurizr-autolayout' include 'structurizr-client' +include 'structurizr-component' include 'structurizr-core' include 'structurizr-dsl' include 'structurizr-export' -include 'structurizr-autolayout' include 'structurizr-import' +include 'structurizr-inspection' \ No newline at end of file diff --git a/structurizr-component/README.md b/structurizr-component/README.md new file mode 100644 index 000000000..0e319527b --- /dev/null +++ b/structurizr-component/README.md @@ -0,0 +1,9 @@ +# structurizr-component + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-component.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-component) + +This library provides a facility to discover components in a Java codebase, via a combination of +[Apache Commons BCEL](https://commons.apache.org/proper/commons-bcel/) and [JavaParser](https://javaparser.org), +using a pluggable and customisable set of matching and filtering rules. + +__Unreleased, experimental, and potentially subject to change - see tests for an example.__ \ No newline at end of file diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle new file mode 100644 index 000000000..3a2fd6c68 --- /dev/null +++ b/structurizr-component/build.gradle @@ -0,0 +1,11 @@ +dependencies { + + api project(':structurizr-core') + implementation 'org.apache.bcel:bcel:6.8.1' + implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.1' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + +} + +description = 'Component finder for Java code' \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java new file mode 100644 index 000000000..4abd0dc99 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -0,0 +1,133 @@ +package com.structurizr.component; + +import com.structurizr.component.provider.TypeProvider; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.util.StringUtils; +import org.apache.bcel.Repository; +import org.apache.bcel.classfile.ConstantPool; +import org.apache.bcel.classfile.Method; +import org.apache.bcel.generic.*; + +import java.util.*; + +/** + * Allows you to find components in a Java codebase based upon a set of pluggable and customisable rules. + * Use the {@link ComponentFinderBuilder} to create an instance of this class. + */ +public final class ComponentFinder { + + private static final String COMPONENT_TYPE_PROPERTY_NAME = "component.type"; + private static final String COMPONENT_SOURCE_PROPERTY_NAME = "component.src"; + + private final TypeRepository typeRepository = new TypeRepository(); + private final Container container; + private final List componentFinderStrategies = new ArrayList<>(); + + ComponentFinder(Container container, Collection typeProviders, List componentFinderStrategies) { + if (container == null) { + throw new IllegalArgumentException("A container must be specified."); + } + + this.container = container; + + for (TypeProvider typeProvider : typeProviders) { + Set types = typeProvider.getTypes(); + for (com.structurizr.component.Type type : types) { + if (type.getJavaClass() != null) { + // this is the BCEL identified type + typeRepository.add(type); + } else { + // this is the source code identified type + com.structurizr.component.Type bcelType = typeRepository.getType(type.getFullyQualifiedName()); + if (bcelType != null) { + bcelType.setDescription(type.getDescription()); + bcelType.setSource(type.getSource()); + } + } + } + } + + Repository.clearCache(); + for (com.structurizr.component.Type type : typeRepository.getTypes()) { + if (type.getJavaClass() != null) { + Repository.addClass(type.getJavaClass()); + findDependencies(type); + } + } + + this.componentFinderStrategies.addAll(componentFinderStrategies); + } + + private void findDependencies(com.structurizr.component.Type type) { + ConstantPool cp = type.getJavaClass().getConstantPool(); + ConstantPoolGen cpg = new ConstantPoolGen(cp); + for (Method m : type.getJavaClass().getMethods()) { + MethodGen mg = new MethodGen(m, type.getJavaClass().getClassName(), cpg); + InstructionList il = mg.getInstructionList(); + if (il == null) { + continue; + } + + InstructionHandle[] instructionHandles = il.getInstructionHandles(); + for (InstructionHandle instructionHandle : instructionHandles) { + Instruction instruction = instructionHandle.getInstruction(); + if (!(instruction instanceof InvokeInstruction)) { + continue; + } + + InvokeInstruction invokeInstruction = (InvokeInstruction)instruction; + ReferenceType referenceType = invokeInstruction.getReferenceType(cpg); + if (!(referenceType instanceof ObjectType)) { + continue; + } + + ObjectType objectType = (ObjectType)referenceType; + String referencedClassName = objectType.getClassName(); + com.structurizr.component.Type referencedType = typeRepository.getType(referencedClassName); + if (referencedType != null) { + type.addDependency(referencedType); + } + } + } + } + + /** + * Find components, using all configured rules, in the order they were added. + */ + public void findComponents() { + Set discoveredComponents = new HashSet<>(); + Map componentMap = new HashMap<>(); + + for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { + discoveredComponents.addAll(componentFinderStrategy.findComponents(typeRepository)); + } + + for (DiscoveredComponent discoveredComponent : discoveredComponents) { + Component component = container.addComponent(discoveredComponent.getName()); + component.addProperty(COMPONENT_TYPE_PROPERTY_NAME, discoveredComponent.getPrimaryType().getFullyQualifiedName()); + if (!StringUtils.isNullOrEmpty(discoveredComponent.getPrimaryType().getSource())) { + component.addProperty(COMPONENT_SOURCE_PROPERTY_NAME, discoveredComponent.getPrimaryType().getSource()); + } + component.setDescription(discoveredComponent.getDescription()); + component.setTechnology(discoveredComponent.getTechnology()); + componentMap.put(discoveredComponent, component); + } + + // find dependencies between all components + for (DiscoveredComponent discoveredComponent : discoveredComponents) { + Set typeDependencies = discoveredComponent.getAllDependencies(); + for (Type typeDependency : typeDependencies) { + for (DiscoveredComponent c : discoveredComponents) { + if (c != discoveredComponent) { + if (c.getAllTypes().contains(typeDependency)) { + Component componentDependency = componentMap.get(c); + componentMap.get(discoveredComponent).uses(componentDependency, ""); + } + } + } + } + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java new file mode 100644 index 000000000..f685451b5 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java @@ -0,0 +1,68 @@ +package com.structurizr.component; + +import com.structurizr.component.provider.DirectoryTypeProvider; +import com.structurizr.component.provider.JarFileTypeProvider; +import com.structurizr.component.provider.SourceCodeTypeProvider; +import com.structurizr.component.provider.TypeProvider; +import com.structurizr.model.Container; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides a way to create a {@link ComponentFinder} instance. + */ +public class ComponentFinderBuilder { + + private Container container; + private final List typeProviders = new ArrayList<>(); + private final List componentFinderStrategies = new ArrayList<>(); + + public ComponentFinderBuilder forContainer(Container container) { + this.container = container; + + return this; + } + + public ComponentFinderBuilder fromJarFile(String filename) { + return fromJarFile(new File(filename)); + } + + public ComponentFinderBuilder fromJarFile(File file) { + this.typeProviders.add(new JarFileTypeProvider(file)); + + return this; + } + + public ComponentFinderBuilder fromDirectory(String path) { + return fromDirectory(new File(path)); + } + + public ComponentFinderBuilder fromDirectory(File path) { + this.typeProviders.add(new DirectoryTypeProvider(path)); + + return this; + } + + public ComponentFinderBuilder fromSourceCode(String path) { + return fromSourceCode(new File(path)); + } + + public ComponentFinderBuilder fromSourceCode(File path) { + this.typeProviders.add(new SourceCodeTypeProvider(path)); + + return this; + } + + public ComponentFinderBuilder withStrategy(ComponentFinderStrategy componentFinderStrategy) { + this.componentFinderStrategies.add(componentFinderStrategy); + + return this; + } + + public ComponentFinder build() { + return new ComponentFinder(container, typeProviders, componentFinderStrategies); + } + +} diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java new file mode 100644 index 000000000..c6273bc91 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -0,0 +1,54 @@ +package com.structurizr.component; + +import com.structurizr.component.filter.TypeFilter; +import com.structurizr.component.matcher.TypeMatcher; +import com.structurizr.component.naming.NamingStrategy; +import com.structurizr.component.supporting.SupportingTypesStrategy; + +import java.util.HashSet; +import java.util.Set; + +/** + * A component finder strategy is a wrapper for a combination of the following: + * - {@link TypeMatcher} + * - {@link TypeFilter} + * - {@link SupportingTypesStrategy} + * - {@link NamingStrategy} + * + * Use the {@link ComponentFinderStrategyBuilder} to create an instance of this class. + */ +class ComponentFinderStrategy { + + private final TypeMatcher typeMatcher; + private final TypeFilter typeFilter; + private final SupportingTypesStrategy supportingTypesStrategy; + private final NamingStrategy namingStrategy; + + ComponentFinderStrategy(TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy) { + this.typeMatcher = typeMatcher; + this.typeFilter = typeFilter; + this.supportingTypesStrategy = supportingTypesStrategy; + this.namingStrategy = namingStrategy; + } + + Set findComponents(TypeRepository typeRepository) { + Set components = new HashSet<>(); + + Set types = typeRepository.getTypes(); + for (Type type : types) { + if (typeMatcher.matches(type) && typeFilter.accept(type)) { + DiscoveredComponent component = new DiscoveredComponent(namingStrategy.nameOf(type), type); + component.setDescription(type.getDescription()); + component.setTechnology(typeMatcher.getTechnology()); + components.add(component); + + // now find supporting types + Set supportingTypes = supportingTypesStrategy.findSupportingTypes(type); + component.addSupportingTypes(supportingTypes); + } + } + + return components; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java new file mode 100644 index 000000000..87719be1a --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -0,0 +1,56 @@ +package com.structurizr.component; + +import com.structurizr.component.filter.DefaultTypeFilter; +import com.structurizr.component.filter.TypeFilter; +import com.structurizr.component.matcher.TypeMatcher; +import com.structurizr.component.naming.DefaultNamingStrategy; +import com.structurizr.component.naming.NamingStrategy; +import com.structurizr.component.supporting.DefaultSupportingTypesStrategy; +import com.structurizr.component.supporting.SupportingTypesStrategy; + +/** + * Provides a way to create a {@link ComponentFinderStrategy} instance. + */ +public final class ComponentFinderStrategyBuilder { + + private TypeMatcher typeMatcher; + private TypeFilter typeFilter = new DefaultTypeFilter(); + private SupportingTypesStrategy supportingTypesStrategy = new DefaultSupportingTypesStrategy(); + private NamingStrategy namingStrategy = new DefaultNamingStrategy(); + + public ComponentFinderStrategyBuilder() { + } + + public ComponentFinderStrategyBuilder matchedBy(TypeMatcher typeMatcher) { + this.typeMatcher = typeMatcher; + + return this; + } + + public ComponentFinderStrategyBuilder filteredBy(TypeFilter typeFilter) { + this.typeFilter = typeFilter; + + return this; + } + + public ComponentFinderStrategyBuilder supportedBy(SupportingTypesStrategy supportingTypesStrategy) { + this.supportingTypesStrategy = supportingTypesStrategy; + + return this; + } + + public ComponentFinderStrategyBuilder namedBy(NamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + + return this; + } + + public ComponentFinderStrategy build() { + if (typeMatcher == null) { + throw new RuntimeException("A type matcher must be specified"); + } + + return new ComponentFinderStrategy(typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java new file mode 100644 index 000000000..19091412b --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java @@ -0,0 +1,70 @@ +package com.structurizr.component; + +import java.util.HashSet; +import java.util.Set; + +final class DiscoveredComponent { + + private final Type primaryType; + private final String name; + private String description; + private String technology; + private final Set supportingTypes = new HashSet<>(); + + DiscoveredComponent(String name, Type primaryType) { + this.name = name; + this.primaryType = primaryType; + } + + void addSupportingTypes(Set types) { + supportingTypes.addAll(types); + } + + Type getPrimaryType() { + return primaryType; + } + + String getName() { + return this.name; + } + + String getDescription() { + return description; + } + + void setDescription(String description) { + this.description = description; + } + + String getTechnology() { + return technology; + } + + void setTechnology(String technology) { + this.technology = technology; + } + + Set getSupportingTypes() { + return new HashSet<>(supportingTypes); + } + + Set getAllTypes() { + Set types = new HashSet<>(); + + types.add(primaryType); + types.addAll(supportingTypes); + + return types; + } + + Set getAllDependencies() { + Set dependencies = new HashSet<>(); + + for (Type type : getAllTypes()) { + dependencies.addAll(type.getDependencies()); + } + + return dependencies; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java new file mode 100644 index 000000000..33a355248 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -0,0 +1,93 @@ +package com.structurizr.component; + +import org.apache.bcel.classfile.JavaClass; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a Java type (e.g. class or interface) - it's a wrapper around a BCEL JavaClass. + */ +public final class Type { + + private JavaClass javaClass; + private String description; + private String source; + private final Set dependencies = new HashSet<>(); + + private final String fullyQualifiedName; + + public Type(JavaClass javaClass) { + this(javaClass.getClassName()); + + this.javaClass = javaClass; + } + + public Type(String fullyQualifiedName) { + this.fullyQualifiedName = fullyQualifiedName; + } + + public String getFullyQualifiedName() { + return fullyQualifiedName; + } + + public String getName() { + return fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".")+1); + } + + public String getPackageName() { + return getFullyQualifiedName().substring(0, getFullyQualifiedName().lastIndexOf(".")); + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Type type = (Type) o; + return fullyQualifiedName.equals(type.fullyQualifiedName); + } + + @Override + public int hashCode() { + return Objects.hash(fullyQualifiedName); + } + + public JavaClass getJavaClass() { + return this.javaClass; + } + + public void addDependency(Type type) { + this.dependencies.add(type); + } + + public Set getDependencies() { + return new HashSet<>(dependencies); + } + + public boolean isAbstract() { + return javaClass.isAbstract(); + } + + @Override + public String toString() { + return this.fullyQualifiedName; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java new file mode 100644 index 000000000..670e1da44 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java @@ -0,0 +1,22 @@ +package com.structurizr.component; + +import java.util.HashSet; +import java.util.Set; + +final class TypeRepository { + + private final Set types = new HashSet<>(); + + public void add(Type type) { + this.types.add(type); + } + + public Set getTypes() { + return new HashSet<>(types); + } + + Type getType(String fullyQualifiedClassName) { + return types.stream().filter(t -> t.getFullyQualifiedName().equals(fullyQualifiedClassName)).findFirst().orElse(null); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java new file mode 100644 index 000000000..e7c79d302 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java @@ -0,0 +1,14 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; + +/** + * A type filter that accepts all types. + */ +public class DefaultTypeFilter implements TypeFilter { + + public boolean accept(Type type) { + return true; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java new file mode 100644 index 000000000..162b99313 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java @@ -0,0 +1,14 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; + +/** + * A type filter that excludes abstract types. + */ +public class ExcludeAbstractClassTypeFilter implements TypeFilter { + + public boolean accept(Type type) { + return !type.isAbstract(); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java new file mode 100644 index 000000000..497daefd1 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java @@ -0,0 +1,27 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; + +/** + * A type filter that excludes by matching a regex against the fully qualified type name. + */ +public class ExcludeTypesByRegexFilter implements TypeFilter { + + private final String[] regexes; + + public ExcludeTypesByRegexFilter(String... regexes) { + this.regexes = regexes; + } + + @Override + public boolean accept(Type type) { + for (String regex : regexes) { + if (type.getFullyQualifiedName().matches(regex)) { + return false; + } + } + + return true; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/TypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/TypeFilter.java new file mode 100644 index 000000000..4d36b407a --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/TypeFilter.java @@ -0,0 +1,12 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; + +/** + * Determines whether a given type should be accepted or not. + */ +public interface TypeFilter { + + boolean accept(Type type); + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java new file mode 100644 index 000000000..f2296d5e5 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java @@ -0,0 +1,19 @@ +package com.structurizr.component.matcher; + +/** + * A superclass for TypeMatcher implementations. + */ +public abstract class AbstractTypeMatcher implements TypeMatcher { + + private final String technology; + + public AbstractTypeMatcher(String technology) { + this.technology = technology; + } + + @Override + public String getTechnology() { + return technology; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java new file mode 100644 index 000000000..f3eda8594 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java @@ -0,0 +1,48 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; +import org.apache.bcel.classfile.AnnotationEntry; + +import java.lang.annotation.Annotation; + +/** + * Matches types based upon the presence of a type-level annotation. + */ +public class AnnotationTypeMatcher extends AbstractTypeMatcher { + + private final String annotationType; + + public AnnotationTypeMatcher(String annotationType, String technology) { + super(technology); + + if (StringUtils.isNullOrEmpty(annotationType)) { + throw new IllegalArgumentException("An annotation type must be supplied"); + } + + this.annotationType = "L" + annotationType.replace(".", "/") + ";"; + } + + public AnnotationTypeMatcher(Class annotation, String technology) { + super(technology); + + if (annotation == null) { + throw new IllegalArgumentException("An annotation must be supplied"); + } + + this.annotationType = "L" + annotation.getCanonicalName().replace(".", "/") + ";"; + } + + @Override + public boolean matches(Type type) { + AnnotationEntry[] annotationEntries = type.getJavaClass().getAnnotationEntries(); + for (AnnotationEntry annotationEntry : annotationEntries) { + if (annotationType.equals(annotationEntry.getAnnotationType())) { + return true; + } + } + + return false; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java new file mode 100644 index 000000000..bfff7159a --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -0,0 +1,40 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import org.apache.bcel.classfile.JavaClass; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Matches types where the type extends the specified class. + */ +public class ExtendsTypeMatcher extends AbstractTypeMatcher { + + private static final Log log = LogFactory.getLog(ExtendsTypeMatcher.class); + + private final String className; + + public ExtendsTypeMatcher(String className, String technology) { + super(technology); + + this.className = className; + } + + @Override + public boolean matches(Type type) { + JavaClass javaClass = type.getJavaClass(); + try { + Set superClasses = Stream.of(javaClass.getSuperClasses()).map(JavaClass::getClassName).collect(Collectors.toSet()); + return superClasses.contains(className); + } catch (ClassNotFoundException e) { + log.warn("Cannot find super classes of " + type.getFullyQualifiedName(), e); + } + + return false; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java new file mode 100644 index 000000000..918bcd344 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -0,0 +1,32 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import org.apache.bcel.classfile.JavaClass; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.Set; + +/** + * Matches types where the type implements the specified interface. + */ +public class ImplementsTypeMatcher extends AbstractTypeMatcher { + + private static final Log log = LogFactory.getLog(ImplementsTypeMatcher.class); + + private final String interfaceName; + + public ImplementsTypeMatcher(String interfaceName, String technology) { + super(technology); + + this.interfaceName = interfaceName; + } + + @Override + public boolean matches(Type type) { + JavaClass javaClass = type.getJavaClass(); + Set interfaceNames = Set.of(javaClass.getInterfaceNames()); + return interfaceNames.contains(interfaceName); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java new file mode 100644 index 000000000..41bdd76be --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java @@ -0,0 +1,27 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; + +/** + * Matches types where the name of the type ends with the specified suffix. + */ +public class NameSuffixTypeMatcher extends AbstractTypeMatcher { + + private final String suffix; + + public NameSuffixTypeMatcher(String suffix, String technology) { + super(technology); + + if (suffix == null || suffix.trim().length() == 0) { + throw new IllegalArgumentException("A suffix must be supplied"); + } + + this.suffix = suffix; + } + + @Override + public boolean matches(Type type) { + return type.getFullyQualifiedName().endsWith(suffix); + } + +} diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java new file mode 100644 index 000000000..e39bbf737 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java @@ -0,0 +1,43 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; + +import java.util.regex.Pattern; + +/** + * Matches types using a regex against the fully qualified type name. + */ +public class RegexTypeMatcher extends AbstractTypeMatcher { + + private final Pattern regex; + + public RegexTypeMatcher(String regex, String technology) { + super(technology); + + if (regex == null) { + throw new IllegalArgumentException("A regex must be supplied"); + } + + this.regex = Pattern.compile(regex); + } + + public RegexTypeMatcher(Pattern regex, String technology) { + super(technology); + + if (regex == null) { + throw new IllegalArgumentException("A regex must be supplied"); + } + + this.regex = regex; + } + + @Override + public boolean matches(Type type) { + if (type != null && type.getFullyQualifiedName() != null) { + return Pattern.matches(regex.pattern(), type.getFullyQualifiedName()); + } else { + return false; + } + } + +} diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java new file mode 100644 index 000000000..572d159d0 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java @@ -0,0 +1,14 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; + +/** + * Determines whether a given type matches the rules for being identified as a component. + */ +public interface TypeMatcher { + + boolean matches(Type type); + + String getTechnology(); + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java new file mode 100644 index 000000000..5ee05339c --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java @@ -0,0 +1,17 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; + +/** + * Uses Apache commons-lang to split a camel/Pascal cased name into separate words + * (e.g. "CustomerRepository" -> "Customer Repository"). + */ +public class CommonsLangCamelCaseNamingStrategy implements NamingStrategy { + + @Override + public String nameOf(Type type) { + String[] parts = org.apache.commons.lang3.StringUtils.splitByCharacterTypeCamelCase(type.getName()); + return String.join(" ", parts); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java new file mode 100644 index 000000000..5c3d126b2 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java @@ -0,0 +1,14 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; + +/** + * Uses the simple/short name of the type (i.e. without the package name). + */ +public class DefaultNamingStrategy implements NamingStrategy { + + public String nameOf(Type type) { + return type.getName(); + } + +} diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/FullyQualifiedNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/FullyQualifiedNamingStrategy.java new file mode 100644 index 000000000..a1e947497 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/FullyQualifiedNamingStrategy.java @@ -0,0 +1,15 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; + +/** + * Uses the fully qualified type name. + */ +public class FullyQualifiedNamingStrategy implements NamingStrategy { + + @Override + public String nameOf(Type type) { + return type.getFullyQualifiedName(); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java new file mode 100644 index 000000000..35f0efab5 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java @@ -0,0 +1,12 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; + +/** + * Provides a way to map a fully qualified type name to a component name. + */ +public interface NamingStrategy { + + String nameOf(Type type); + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/DirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/DirectoryTypeProvider.java new file mode 100644 index 000000000..05c55f596 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/DirectoryTypeProvider.java @@ -0,0 +1,64 @@ +package com.structurizr.component.provider; + +import com.structurizr.component.Type; +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * A type repository that uses Apache Commons BCEL to load Java classes from a local directory. + */ +public final class DirectoryTypeProvider implements TypeProvider { + + private static final Log log = LogFactory.getLog(DirectoryTypeProvider.class); + private static final String CLASS_FILE_EXTENSION = ".class"; + + private final File directory; + + public DirectoryTypeProvider(File directory) { + this.directory = directory; + } + + public Set getTypes() { + Set types = new HashSet<>(); + + Set files = findClassFiles(directory); + for (File file : files) { + ClassParser parser = new ClassParser(file.getAbsolutePath()); + try { + JavaClass javaClass = parser.parse(); + types.add(new Type(javaClass)); + } catch (IOException e) { + log.warn(e); + } + } + + return types; + } + + private Set findClassFiles(File path) { + Set classFiles = new HashSet<>(); + if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null) { + for (File file : files) { + classFiles.addAll(findClassFiles(file)); + } + } + } else { + if (path.getName().endsWith(CLASS_FILE_EXTENSION)) { + classFiles.add(path); + } + } + + return classFiles; + } + + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/JarFileTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/JarFileTypeProvider.java new file mode 100644 index 000000000..5904bf9a6 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/JarFileTypeProvider.java @@ -0,0 +1,61 @@ +package com.structurizr.component.provider; + +import com.structurizr.component.Type; +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.File; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; + +/** + * A type repository that uses Apache Commons BCEL to load Java classes from a local JAR file. + */ +public final class JarFileTypeProvider implements TypeProvider { + + private static final Log log = LogFactory.getLog(JarFileTypeProvider.class); + private static final String CLASS_FILE_EXTENSION = ".class"; + + private final File jarFile; + + public JarFileTypeProvider(File file) { + this.jarFile = file; + } + + public Set getTypes() { + Set types = new HashSet<>(); + java.util.jar.JarFile jar = null; + try { + jar = new java.util.jar.JarFile(jarFile); + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (!entry.getName().endsWith(CLASS_FILE_EXTENSION)) { + continue; + } + + ClassParser parser = new ClassParser(jarFile.getAbsolutePath(), entry.getName()); + JavaClass javaClass = parser.parse(); + types.add(new Type(javaClass)); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (jar != null) { + try { + jar.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return types; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java b/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java new file mode 100644 index 000000000..c89b3fc34 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java @@ -0,0 +1,35 @@ +package com.structurizr.component.provider; + +/** + * Cleans up Javadoc comments for inclusion in the software architecture model. + */ +class JavadocCommentFilter { + + private final Integer maxCommentLength; + + JavadocCommentFilter(Integer maxCommentLength) { + if (maxCommentLength != null && maxCommentLength < 1) { + throw new IllegalArgumentException("Maximum comment length must be greater than 0."); + } + + this.maxCommentLength = maxCommentLength; + } + + String filterAndTruncate(String s) { + if (s == null) { + return null; + } + + s = s.replaceAll("\\n", " "); + s = s.replaceAll("(?s)<.*?>", ""); + s = s.replaceAll("\\{@link (\\S*)\\}", "$1"); + s = s.replaceAll("\\{@link (\\S*) (.*?)\\}", "$2"); + + if (maxCommentLength != null && s.length() > maxCommentLength) { + return s.substring(0, maxCommentLength-3) + "..."; + } else { + return s; + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceCodeTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceCodeTypeProvider.java new file mode 100644 index 000000000..a512a8bbe --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceCodeTypeProvider.java @@ -0,0 +1,86 @@ +package com.structurizr.component.provider; + +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.comments.JavadocComment; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; +import com.structurizr.component.Type; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * A type provider that uses JavaParser to read Javadoc comments from source code. + */ +public final class SourceCodeTypeProvider implements TypeProvider { + + private static final Log log = LogFactory.getLog(SourceCodeTypeProvider.class); + private static final String JAVA_FILE_EXTENSION = ".java"; + private static final int DEFAULT_DESCRIPTION_LENGTH = 60; + + private final Set types = new HashSet<>(); + + public SourceCodeTypeProvider(File path) { + this(path, DEFAULT_DESCRIPTION_LENGTH); + } + + public SourceCodeTypeProvider(File path, int maximumDescriptionLength) { + StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); + + parse(path, maximumDescriptionLength); + } + + @Override + public Set getTypes() { + return new HashSet<>(types); + } + + private void parse(File path, int maximumDescriptionLength) { + if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null) { + for (File file : files) { + try { + parse(file, maximumDescriptionLength); + } catch (Exception e) { + log.warn("Error parsing " + file.getAbsolutePath(), e); + } + } + } + } else { + if (path.getName().endsWith(JAVA_FILE_EXTENSION)) { + try { + new VoidVisitorAdapter<>() { + @Override + public void visit(ClassOrInterfaceDeclaration n, Object arg) { + if (n.getFullyQualifiedName().isPresent()) { + String fullyQualifiedName = n.getFullyQualifiedName().get(); + Type type = new Type(fullyQualifiedName); + type.setSource(path.getAbsolutePath()); + + if (n.getComment().isPresent() && n.getComment().get() instanceof JavadocComment) { + JavadocComment javadocComment = (JavadocComment) n.getComment().get(); + String description = javadocComment.parse().getDescription().toText(); + + type.setDescription(new JavadocCommentFilter(maximumDescriptionLength).filterAndTruncate(description)); + } + + types.add(type); + } + } + }.visit(StaticJavaParser.parse(path), null); + } catch (IOException e) { + log.warn("Error parsing source code", e); + } + } else { + log.debug("Ignoring " + path.getAbsolutePath()); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/TypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/TypeProvider.java new file mode 100644 index 000000000..4172e4409 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/TypeProvider.java @@ -0,0 +1,14 @@ +package com.structurizr.component.provider; + +import com.structurizr.component.Type; + +import java.util.Set; + +/** + * Provides a way to load Java types. + */ +public interface TypeProvider { + + Set getTypes(); + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategy.java new file mode 100644 index 000000000..5791d7943 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategy.java @@ -0,0 +1,22 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A strategy that finds all referenced types in the same package as the component type. + */ +public class AllReferencedTypesInSamePackageSupportingTypesStrategy implements SupportingTypesStrategy { + + @Override + public Set findSupportingTypes(Type type) { + String packageName = type.getPackageName(); + + return type.getDependencies().stream() + .filter(dependency -> dependency.getPackageName().startsWith(packageName)) + .collect(Collectors.toSet()); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java new file mode 100644 index 000000000..28738a7aa --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java @@ -0,0 +1,17 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; + +import java.util.Set; + +/** + * A strategy that finds all referenced types, irrespective of package. + */ +public class AllReferencedTypesSupportingTypesStrategy implements SupportingTypesStrategy { + + @Override + public Set findSupportingTypes(Type type) { + return type.getDependencies(); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java new file mode 100644 index 000000000..58f1ec8cd --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java @@ -0,0 +1,18 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; + +import java.util.Collections; +import java.util.Set; + +/** + * A strategy that returns an empty set of supporting types. + */ +public class DefaultSupportingTypesStrategy implements SupportingTypesStrategy { + + @Override + public Set findSupportingTypes(Type type) { + return Collections.emptySet(); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java new file mode 100644 index 000000000..1d226daca --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java @@ -0,0 +1,14 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; + +import java.util.Set; + +/** + * Provides a strategy to identify the types that support a component. + */ +public interface SupportingTypesStrategy { + + Set findSupportingTypes(Type type); + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java b/structurizr-component/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java new file mode 100644 index 000000000..91be93b65 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java @@ -0,0 +1,12 @@ +package com.structurizr; + +import com.structurizr.model.Model; +import com.structurizr.view.ViewSet; + +public class AbstractWorkspaceTestBase { + + protected Workspace workspace = new Workspace("Name", "Description"); + protected Model model = workspace.getModel(); + protected ViewSet views = workspace.getViews(); + +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java new file mode 100644 index 000000000..99af51453 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java @@ -0,0 +1,50 @@ +package com.structurizr.component.example; + +import com.structurizr.Workspace; +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.component.ComponentFinderStrategyBuilder; +import com.structurizr.component.matcher.NameSuffixTypeMatcher; +import com.structurizr.component.naming.CommonsLangCamelCaseNamingStrategy; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; + +public class Example { + + public static void main(String[] args) { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + + new ComponentFinderBuilder() + .forContainer(container) + .fromDirectory("structurizr-component/build/classes/java/test") + .fromSourceCode("structurizr-component/src/test/java") + .withStrategy( + new ComponentFinderStrategyBuilder() + .matchedBy(new NameSuffixTypeMatcher("Controller", "Web MVC Controller")) + .namedBy(new CommonsLangCamelCaseNamingStrategy()) + .build() + ) + .withStrategy( + new ComponentFinderStrategyBuilder() + .matchedBy(new NameSuffixTypeMatcher("Repository", "Data Repository")) + .namedBy(new CommonsLangCamelCaseNamingStrategy()) + .build() + ) + .build().findComponents(); + + for (Component component : container.getComponents()) { + System.out.println(component.getName()); + System.out.println(" - Description: " + component.getDescription()); + System.out.println(" - Technology: " + component.getTechnology()); + System.out.println(" - Type: " + component.getProperties().get("component.type")); + System.out.println(" - Source: " + component.getProperties().get("component.src")); + for (Relationship relationship : component.getRelationships()) { + System.out.println(" -> " + relationship.getDestination().getName()); + } + } + } + +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java b/structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java new file mode 100644 index 000000000..409e4dd8c --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java @@ -0,0 +1,19 @@ +package com.structurizr.component.example.controller; + +import com.structurizr.component.example.repository.CustomerRepository; + +/** + * Allows users to view a list of customers. + */ +public class CustomerController { + + private CustomerRepository customerRepository; + + public CustomerController(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + void showCustomersPage() { + customerRepository.getCustomers(); + } +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java b/structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java new file mode 100644 index 000000000..ca9637df3 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java @@ -0,0 +1,4 @@ +package com.structurizr.component.example.domain; + +public class Customer { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java b/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java new file mode 100644 index 000000000..d7040cd2e --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java @@ -0,0 +1,14 @@ +package com.structurizr.component.example.repository; + +import com.structurizr.component.example.domain.Customer; + +import java.util.List; + +/** + * Provides a way to access customer data. + */ +public interface CustomerRepository { + + List getCustomers(); + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java b/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java new file mode 100644 index 000000000..43ef69bdc --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java @@ -0,0 +1,15 @@ +package com.structurizr.component.example.repository; + +import com.structurizr.component.example.domain.Customer; + +import java.util.ArrayList; +import java.util.List; + +public class CustomerRepositoryImpl implements CustomerRepository { + + @Override + public List getCustomers() { + return new ArrayList<>(); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java new file mode 100644 index 000000000..265a888d6 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java @@ -0,0 +1,60 @@ +package com.structurizr.component.provider; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class JavadocCommentFilterTests { + + @Test + public void test_construction_ThrowsAnIllegalArgumentException_WhenZeroIsSpecified() { + assertThrowsExactly(IllegalArgumentException.class, () -> new JavadocCommentFilter(0)); + } + + @Test + public void test_construction_ThrowsAnIllegalArgumentException_WhenANegativeNumberIsSpecified() { + assertThrowsExactly(IllegalArgumentException.class, () -> new JavadocCommentFilter(-1)); + } + + @Test + public void test_filterAndTruncate_ReturnsNull_WhenGivenNull() { + assertNull(new JavadocCommentFilter(null).filterAndTruncate(null)); + } + + @Test + public void test_filterAndTruncate_ReturnsTheOriginalText_WhenNoMaxLengthHasBeenSpecified() + { + assertEquals("Here is some text.", new JavadocCommentFilter(null).filterAndTruncate("Here is some text.")); + } + + @Test + public void test_filterAndTruncate_TruncatesTheTextWhenAMaxLengthHasBeenSpecified() + { + assertEquals("Here...", new JavadocCommentFilter(7).filterAndTruncate("Here is some text.")); + } + + @Test + public void test_filterAndTruncate_FiltersJavadocLinkTags() + { + assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses {@link SomeClass} and {@link AnotherClass} to do some work.")); + } + + @Test + public void test_filterAndTruncate_FiltersJavadocLinkTagsWithLabels() + { + assertEquals("Uses some class and another class to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses {@link SomeClass some class} and {@link AnotherClass another class} to do some work.")); + } + + @Test + public void test_filterAndTruncate_FiltersHtml() + { + assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses SomeClass and AnotherClass to do some work.")); + } + + @Test + public void test_filterAndTruncate_FiltersLineBreaks() + { + assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses SomeClass and AnotherClass\nto do some work.")); + } + +} \ No newline at end of file From 002e1ac3510c6f7fe41b7487af4922852e1fdfb1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 11:26:53 +0100 Subject: [PATCH 221/418] . --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4cb2d0dc1..d486bd395 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ This repository contains the source code for the following libraries: -- [structurizr-client](structurizr-client) -- [structurizr-core](structurizr-core) -- [structurizr-dsl](structurizr-dsl) -- [structurizr-export](structurizr-export) -- [structurizr-import](structurizr-import) -- [structurizr-autolayout](structurizr-autolayout) -- [structurizr-inspection](structurizr-inspection) +- [structurizr-client](structurizr-client): JSON serialisation/deserialisation utilities, and clients for the cloud service/on-premises workspace/admin APIs. +- [structurizr-core](structurizr-core): The core library for creating a workspace with Java code. +- [structurizr-component](structurizr-component): A library to discover components from Java code. +- [structurizr-dsl](structurizr-dsl): A text-based DSL wrapper around Structurizr for Java. +- [structurizr-export](structurizr-export): Export models and views to external formats (e.g. PlantUML, Mermaid, etc). +- [structurizr-import](structurizr-import): Utilities to import diagrams and documentation into a Structurizr workspace. +- [structurizr-autolayout](structurizr-autolayout): Apply Graphviz automatic layout to views. +- [structurizr-inspection](structurizr-inspection): A Checkstyle inspired approach to verifying workspace content. - [Documentation](https://docs.structurizr.com) - [Changelog](changelog.md) \ No newline at end of file From b590494ce35453b3004606fcecdc64bdb34d06dd Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 11:36:10 +0100 Subject: [PATCH 222/418] Renaming of type providers. --- .../component/ComponentFinderBuilder.java | 42 ++++++++++--------- ...r.java => ClassDirectoryTypeProvider.java} | 6 +-- ...der.java => ClassJarFileTypeProvider.java} | 6 +-- ....java => SourceDirectoryTypeProvider.java} | 8 ++-- .../component/example/Example.java | 4 +- 5 files changed, 34 insertions(+), 32 deletions(-) rename structurizr-component/src/main/java/com/structurizr/component/provider/{DirectoryTypeProvider.java => ClassDirectoryTypeProvider.java} (88%) rename structurizr-component/src/main/java/com/structurizr/component/provider/{JarFileTypeProvider.java => ClassJarFileTypeProvider.java} (89%) rename structurizr-component/src/main/java/com/structurizr/component/provider/{SourceCodeTypeProvider.java => SourceDirectoryTypeProvider.java} (91%) diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java index f685451b5..a4f8f9535 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java @@ -1,8 +1,8 @@ package com.structurizr.component; -import com.structurizr.component.provider.DirectoryTypeProvider; -import com.structurizr.component.provider.JarFileTypeProvider; -import com.structurizr.component.provider.SourceCodeTypeProvider; +import com.structurizr.component.provider.ClassDirectoryTypeProvider; +import com.structurizr.component.provider.ClassJarFileTypeProvider; +import com.structurizr.component.provider.SourceDirectoryTypeProvider; import com.structurizr.component.provider.TypeProvider; import com.structurizr.model.Container; @@ -15,6 +15,8 @@ */ public class ComponentFinderBuilder { + private static final String JAR_FILE_EXTENSION = ".jar"; + private Container container; private final List typeProviders = new ArrayList<>(); private final List componentFinderStrategies = new ArrayList<>(); @@ -25,32 +27,32 @@ public ComponentFinderBuilder forContainer(Container container) { return this; } - public ComponentFinderBuilder fromJarFile(String filename) { - return fromJarFile(new File(filename)); - } - - public ComponentFinderBuilder fromJarFile(File file) { - this.typeProviders.add(new JarFileTypeProvider(file)); - - return this; + public ComponentFinderBuilder fromClasses(String path) { + return fromClasses(new File(path)); } - public ComponentFinderBuilder fromDirectory(String path) { - return fromDirectory(new File(path)); - } + public ComponentFinderBuilder fromClasses(File path) { + if (!path.exists()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " does not exist"); + } - public ComponentFinderBuilder fromDirectory(File path) { - this.typeProviders.add(new DirectoryTypeProvider(path)); + if (path.isDirectory()) { + this.typeProviders.add(new ClassDirectoryTypeProvider(path)); + } else if (path.getName().endsWith(JAR_FILE_EXTENSION)) { + this.typeProviders.add(new ClassJarFileTypeProvider(path)); + } else { + throw new IllegalArgumentException("Expected a directory of classes or a .jar file: " + path.getAbsolutePath()); + } return this; } - public ComponentFinderBuilder fromSourceCode(String path) { - return fromSourceCode(new File(path)); + public ComponentFinderBuilder fromSource(String path) { + return fromSource(new File(path)); } - public ComponentFinderBuilder fromSourceCode(File path) { - this.typeProviders.add(new SourceCodeTypeProvider(path)); + public ComponentFinderBuilder fromSource(File path) { + this.typeProviders.add(new SourceDirectoryTypeProvider(path)); return this; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/DirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java similarity index 88% rename from structurizr-component/src/main/java/com/structurizr/component/provider/DirectoryTypeProvider.java rename to structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java index 05c55f596..a4c5b8339 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/DirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java @@ -14,14 +14,14 @@ /** * A type repository that uses Apache Commons BCEL to load Java classes from a local directory. */ -public final class DirectoryTypeProvider implements TypeProvider { +public final class ClassDirectoryTypeProvider implements TypeProvider { - private static final Log log = LogFactory.getLog(DirectoryTypeProvider.class); + private static final Log log = LogFactory.getLog(ClassDirectoryTypeProvider.class); private static final String CLASS_FILE_EXTENSION = ".class"; private final File directory; - public DirectoryTypeProvider(File directory) { + public ClassDirectoryTypeProvider(File directory) { this.directory = directory; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/JarFileTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java similarity index 89% rename from structurizr-component/src/main/java/com/structurizr/component/provider/JarFileTypeProvider.java rename to structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java index 5904bf9a6..71951c04a 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/JarFileTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java @@ -16,14 +16,14 @@ /** * A type repository that uses Apache Commons BCEL to load Java classes from a local JAR file. */ -public final class JarFileTypeProvider implements TypeProvider { +public final class ClassJarFileTypeProvider implements TypeProvider { - private static final Log log = LogFactory.getLog(JarFileTypeProvider.class); + private static final Log log = LogFactory.getLog(ClassJarFileTypeProvider.class); private static final String CLASS_FILE_EXTENSION = ".class"; private final File jarFile; - public JarFileTypeProvider(File file) { + public ClassJarFileTypeProvider(File file) { this.jarFile = file; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceCodeTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java similarity index 91% rename from structurizr-component/src/main/java/com/structurizr/component/provider/SourceCodeTypeProvider.java rename to structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java index a512a8bbe..f68630a6c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceCodeTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -17,19 +17,19 @@ /** * A type provider that uses JavaParser to read Javadoc comments from source code. */ -public final class SourceCodeTypeProvider implements TypeProvider { +public final class SourceDirectoryTypeProvider implements TypeProvider { - private static final Log log = LogFactory.getLog(SourceCodeTypeProvider.class); + private static final Log log = LogFactory.getLog(SourceDirectoryTypeProvider.class); private static final String JAVA_FILE_EXTENSION = ".java"; private static final int DEFAULT_DESCRIPTION_LENGTH = 60; private final Set types = new HashSet<>(); - public SourceCodeTypeProvider(File path) { + public SourceDirectoryTypeProvider(File path) { this(path, DEFAULT_DESCRIPTION_LENGTH); } - public SourceCodeTypeProvider(File path, int maximumDescriptionLength) { + public SourceDirectoryTypeProvider(File path, int maximumDescriptionLength) { StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); parse(path, maximumDescriptionLength); diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java index 99af51453..f50b90369 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java @@ -19,8 +19,8 @@ public static void main(String[] args) { new ComponentFinderBuilder() .forContainer(container) - .fromDirectory("structurizr-component/build/classes/java/test") - .fromSourceCode("structurizr-component/src/test/java") + .fromClasses("structurizr-component/build/classes/java/test") + .fromSource("structurizr-component/src/test/java") .withStrategy( new ComponentFinderStrategyBuilder() .matchedBy(new NameSuffixTypeMatcher("Controller", "Web MVC Controller")) From bdff6a19e27e4c4b323bdb5c41fc32faf9ca4d96 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 12:16:33 +0100 Subject: [PATCH 223/418] Adds a way to visit discovered components ... this can be used for post-processing after creation. --- .../component/ComponentFinder.java | 6 +++ .../component/ComponentFinderStrategy.java | 11 +++++- .../ComponentFinderStrategyBuilder.java | 11 +++++- .../component/DiscoveredComponent.java | 10 +++++ .../component/visitor/ComponentVisitor.java | 12 ++++++ .../visitor/DefaultComponentVisitor.java | 14 +++++++ .../component/example/Example.java | 39 ++++++++++++------- 7 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/visitor/ComponentVisitor.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index 4abd0dc99..c8d47ba00 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -128,6 +128,12 @@ public void findComponents() { } } } + + // now visit all components + for (DiscoveredComponent discoveredComponent : componentMap.keySet()) { + Component component = componentMap.get(discoveredComponent); + discoveredComponent.getComponentFinderStrategy().visit(component); + } } } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index c6273bc91..80d7094af 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -4,6 +4,8 @@ import com.structurizr.component.matcher.TypeMatcher; import com.structurizr.component.naming.NamingStrategy; import com.structurizr.component.supporting.SupportingTypesStrategy; +import com.structurizr.component.visitor.ComponentVisitor; +import com.structurizr.model.Component; import java.util.HashSet; import java.util.Set; @@ -23,12 +25,14 @@ class ComponentFinderStrategy { private final TypeFilter typeFilter; private final SupportingTypesStrategy supportingTypesStrategy; private final NamingStrategy namingStrategy; + private final ComponentVisitor componentVisitor; - ComponentFinderStrategy(TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy) { + ComponentFinderStrategy(TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy, ComponentVisitor componentVisitor) { this.typeMatcher = typeMatcher; this.typeFilter = typeFilter; this.supportingTypesStrategy = supportingTypesStrategy; this.namingStrategy = namingStrategy; + this.componentVisitor = componentVisitor; } Set findComponents(TypeRepository typeRepository) { @@ -40,6 +44,7 @@ Set findComponents(TypeRepository typeRepository) { DiscoveredComponent component = new DiscoveredComponent(namingStrategy.nameOf(type), type); component.setDescription(type.getDescription()); component.setTechnology(typeMatcher.getTechnology()); + component.setComponentFinderStrategy(this); components.add(component); // now find supporting types @@ -51,4 +56,8 @@ Set findComponents(TypeRepository typeRepository) { return components; } + void visit(Component component) { + this.componentVisitor.visit(component); + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java index 87719be1a..97a919843 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -7,6 +7,8 @@ import com.structurizr.component.naming.NamingStrategy; import com.structurizr.component.supporting.DefaultSupportingTypesStrategy; import com.structurizr.component.supporting.SupportingTypesStrategy; +import com.structurizr.component.visitor.ComponentVisitor; +import com.structurizr.component.visitor.DefaultComponentVisitor; /** * Provides a way to create a {@link ComponentFinderStrategy} instance. @@ -17,6 +19,7 @@ public final class ComponentFinderStrategyBuilder { private TypeFilter typeFilter = new DefaultTypeFilter(); private SupportingTypesStrategy supportingTypesStrategy = new DefaultSupportingTypesStrategy(); private NamingStrategy namingStrategy = new DefaultNamingStrategy(); + private ComponentVisitor componentVisitor = new DefaultComponentVisitor(); public ComponentFinderStrategyBuilder() { } @@ -45,12 +48,18 @@ public ComponentFinderStrategyBuilder namedBy(NamingStrategy namingStrategy) { return this; } + public ComponentFinderStrategyBuilder forEach(ComponentVisitor componentVisitor) { + this.componentVisitor = componentVisitor; + + return this; + } + public ComponentFinderStrategy build() { if (typeMatcher == null) { throw new RuntimeException("A type matcher must be specified"); } - return new ComponentFinderStrategy(typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy); + return new ComponentFinderStrategy(typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, componentVisitor); } } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java index 19091412b..8eddb2e08 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java +++ b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java @@ -11,6 +11,8 @@ final class DiscoveredComponent { private String technology; private final Set supportingTypes = new HashSet<>(); + private ComponentFinderStrategy componentFinderStrategy; + DiscoveredComponent(String name, Type primaryType) { this.name = name; this.primaryType = primaryType; @@ -67,4 +69,12 @@ Set getAllDependencies() { return dependencies; } + ComponentFinderStrategy getComponentFinderStrategy() { + return componentFinderStrategy; + } + + void setComponentFinderStrategy(ComponentFinderStrategy componentFinderStrategy) { + this.componentFinderStrategy = componentFinderStrategy; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/visitor/ComponentVisitor.java b/structurizr-component/src/main/java/com/structurizr/component/visitor/ComponentVisitor.java new file mode 100644 index 000000000..9c6a98646 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/visitor/ComponentVisitor.java @@ -0,0 +1,12 @@ +package com.structurizr.component.visitor; + +import com.structurizr.model.Component; + +/** + * Provides a way to visit each discovered component. + */ +public interface ComponentVisitor { + + void visit(Component component); + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java b/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java new file mode 100644 index 000000000..49b687cf6 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java @@ -0,0 +1,14 @@ +package com.structurizr.component.visitor; + +import com.structurizr.model.Component; + +/** + * No-op implementation of a component visitor. + */ +public class DefaultComponentVisitor implements ComponentVisitor { + + @Override + public void visit(Component component) { + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java index f50b90369..fe13a91a8 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java @@ -5,45 +5,56 @@ import com.structurizr.component.ComponentFinderStrategyBuilder; import com.structurizr.component.matcher.NameSuffixTypeMatcher; import com.structurizr.component.naming.CommonsLangCamelCaseNamingStrategy; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Relationship; -import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.*; public class Example { public static void main(String[] args) { Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + Person user = workspace.getModel().addPerson("User"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); - Container container = softwareSystem.addContainer("Container"); + Container webApplication = softwareSystem.addContainer("Web Application"); + Container databaseSchema = softwareSystem.addContainer("Database Schema"); new ComponentFinderBuilder() - .forContainer(container) + .forContainer(webApplication) .fromClasses("structurizr-component/build/classes/java/test") .fromSource("structurizr-component/src/test/java") .withStrategy( new ComponentFinderStrategyBuilder() .matchedBy(new NameSuffixTypeMatcher("Controller", "Web MVC Controller")) .namedBy(new CommonsLangCamelCaseNamingStrategy()) + .forEach(component -> user.uses(component, "Uses")) .build() ) .withStrategy( new ComponentFinderStrategyBuilder() .matchedBy(new NameSuffixTypeMatcher("Repository", "Data Repository")) .namedBy(new CommonsLangCamelCaseNamingStrategy()) + .forEach(component -> component.uses(databaseSchema, "Reads from and writes to")) .build() ) .build().findComponents(); - for (Component component : container.getComponents()) { - System.out.println(component.getName()); - System.out.println(" - Description: " + component.getDescription()); + for (Element element : workspace.getModel().getElements()) { + print(element); + } + } + + private static void print(Element element) { + System.out.println("[" + element.getClass().getSimpleName() + "] " + element.getName()); + System.out.println(" - Description: " + element.getDescription()); + + if (element instanceof Component component) { System.out.println(" - Technology: " + component.getTechnology()); - System.out.println(" - Type: " + component.getProperties().get("component.type")); - System.out.println(" - Source: " + component.getProperties().get("component.src")); - for (Relationship relationship : component.getRelationships()) { - System.out.println(" -> " + relationship.getDestination().getName()); - } + System.out.println(" - Type: " + element.getProperties().get("component.type")); + System.out.println(" - Source: " + element.getProperties().get("component.src")); + } + + for (Relationship relationship : element.getRelationships()) { + System.out.println(" -> [" + relationship.getDestination().getClass().getSimpleName() + "] " + relationship.getDestination().getName()); } } From 4730125a597ff4678f6109cf6ca2c328de2a95f9 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 22:35:51 +0100 Subject: [PATCH 224/418] A few enhancements. --- .../component/ComponentFinder.java | 12 ++++++-- .../component/ComponentFinderStrategy.java | 11 +++++++- .../ComponentFinderStrategyBuilder.java | 1 + .../structurizr/component/TypeRepository.java | 6 ++-- .../component/filter/DefaultTypeFilter.java | 5 ++++ .../ExcludeAbstractClassTypeFilter.java | 5 ++++ .../filter/ExcludeTypesByRegexFilter.java | 19 +++++++------ .../filter/IncludeTypesByRegexFilter.java | 28 +++++++++++++++++++ .../matcher/AnnotationTypeMatcher.java | 7 +++++ .../component/matcher/ExtendsTypeMatcher.java | 7 +++++ .../matcher/ImplementsTypeMatcher.java | 7 +++++ .../matcher/NameSuffixTypeMatcher.java | 7 +++++ .../component/matcher/RegexTypeMatcher.java | 7 +++++ .../CommonsLangCamelCaseNamingStrategy.java | 17 ----------- .../naming/DefaultNamingStrategy.java | 9 ++++-- .../naming/SimpleNamingStrategy.java | 14 ++++++++++ .../component/example/Example.java | 4 +-- 17 files changed, 128 insertions(+), 38 deletions(-) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java delete mode 100644 structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/naming/SimpleNamingStrategy.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index c8d47ba00..b12b7f04c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -8,6 +8,8 @@ import org.apache.bcel.classfile.ConstantPool; import org.apache.bcel.classfile.Method; import org.apache.bcel.generic.*; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import java.util.*; @@ -17,6 +19,8 @@ */ public final class ComponentFinder { + private static final Log log = LogFactory.getLog(ComponentFinder.class); + private static final String COMPONENT_TYPE_PROPERTY_NAME = "component.type"; private static final String COMPONENT_SOURCE_PROPERTY_NAME = "component.src"; @@ -96,11 +100,15 @@ private void findDependencies(com.structurizr.component.Type type) { * Find components, using all configured rules, in the order they were added. */ public void findComponents() { - Set discoveredComponents = new HashSet<>(); + Set discoveredComponents = new LinkedHashSet<>(); Map componentMap = new HashMap<>(); for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - discoveredComponents.addAll(componentFinderStrategy.findComponents(typeRepository)); + Set set = componentFinderStrategy.findComponents(typeRepository); + if (set.isEmpty()) { + throw new RuntimeException("No components were found by " + componentFinderStrategy); + } + discoveredComponents.addAll(set); } for (DiscoveredComponent discoveredComponent : discoveredComponents) { diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index 80d7094af..1da4f7de7 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -8,6 +8,7 @@ import com.structurizr.model.Component; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; /** @@ -36,7 +37,7 @@ class ComponentFinderStrategy { } Set findComponents(TypeRepository typeRepository) { - Set components = new HashSet<>(); + Set components = new LinkedHashSet<>(); Set types = typeRepository.getTypes(); for (Type type : types) { @@ -60,4 +61,12 @@ void visit(Component component) { this.componentVisitor.visit(component); } + @Override + public String toString() { + return "ComponentFinderStrategy{" + + "typeMatcher=" + typeMatcher + + ", typeFilter=" + typeFilter + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java index 97a919843..8027be99f 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -4,6 +4,7 @@ import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.matcher.TypeMatcher; import com.structurizr.component.naming.DefaultNamingStrategy; +import com.structurizr.component.naming.SimpleNamingStrategy; import com.structurizr.component.naming.NamingStrategy; import com.structurizr.component.supporting.DefaultSupportingTypesStrategy; import com.structurizr.component.supporting.SupportingTypesStrategy; diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java index 670e1da44..45f80f365 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java @@ -1,18 +1,18 @@ package com.structurizr.component; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; final class TypeRepository { - private final Set types = new HashSet<>(); + private final Set types = new LinkedHashSet<>(); public void add(Type type) { this.types.add(type); } public Set getTypes() { - return new HashSet<>(types); + return new LinkedHashSet<>(types); } Type getType(String fullyQualifiedClassName) { diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java index e7c79d302..64e9370d1 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java @@ -11,4 +11,9 @@ public boolean accept(Type type) { return true; } + @Override + public String toString() { + return "DefaultTypeFilter{}"; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java index 162b99313..dad267ef1 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java @@ -11,4 +11,9 @@ public boolean accept(Type type) { return !type.isAbstract(); } + @Override + public String toString() { + return "ExcludeAbstractClassTypeFilter{}"; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java index 497daefd1..0f697b6e8 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java @@ -7,21 +7,22 @@ */ public class ExcludeTypesByRegexFilter implements TypeFilter { - private final String[] regexes; + private final String regex; - public ExcludeTypesByRegexFilter(String... regexes) { - this.regexes = regexes; + public ExcludeTypesByRegexFilter(String regex) { + this.regex = regex; } @Override public boolean accept(Type type) { - for (String regex : regexes) { - if (type.getFullyQualifiedName().matches(regex)) { - return false; - } - } + return !type.getFullyQualifiedName().matches(regex); + } - return true; + @Override + public String toString() { + return "ExcludeTypesByRegexFilter{" + + "regex='" + regex + '\'' + + '}'; } } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java new file mode 100644 index 000000000..dc76b1f41 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java @@ -0,0 +1,28 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; + +/** + * A type filter that includes by matching a regex against the fully qualified type name. + */ +public class IncludeTypesByRegexFilter implements TypeFilter { + + private final String regex; + + public IncludeTypesByRegexFilter(String regex) { + this.regex = regex; + } + + @Override + public boolean accept(Type type) { + return type.getFullyQualifiedName().matches(regex); + } + + @Override + public String toString() { + return "IncludeTypesByRegexFilter{" + + "regex='" + regex + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java index f3eda8594..2674204d8 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java @@ -45,4 +45,11 @@ public boolean matches(Type type) { return false; } + @Override + public String toString() { + return "AnnotationTypeMatcher{" + + "annotationType='" + annotationType + '\'' + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java index bfff7159a..2ae97afe7 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -37,4 +37,11 @@ public boolean matches(Type type) { return false; } + @Override + public String toString() { + return "ExtendsTypeMatcher{" + + "className='" + className + '\'' + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java index 918bcd344..643a6e361 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -29,4 +29,11 @@ public boolean matches(Type type) { return interfaceNames.contains(interfaceName); } + @Override + public String toString() { + return "ImplementsTypeMatcher{" + + "interfaceName='" + interfaceName + '\'' + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java index 41bdd76be..e1a17b5db 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java @@ -24,4 +24,11 @@ public boolean matches(Type type) { return type.getFullyQualifiedName().endsWith(suffix); } + @Override + public String toString() { + return "NameSuffixTypeMatcher{" + + "suffix='" + suffix + '\'' + + '}'; + } + } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java index e39bbf737..565606102 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java @@ -40,4 +40,11 @@ public boolean matches(Type type) { } } + @Override + public String toString() { + return "RegexTypeMatcher{" + + "regex=" + regex + + '}'; + } + } diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java deleted file mode 100644 index 5ee05339c..000000000 --- a/structurizr-component/src/main/java/com/structurizr/component/naming/CommonsLangCamelCaseNamingStrategy.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.structurizr.component.naming; - -import com.structurizr.component.Type; - -/** - * Uses Apache commons-lang to split a camel/Pascal cased name into separate words - * (e.g. "CustomerRepository" -> "Customer Repository"). - */ -public class CommonsLangCamelCaseNamingStrategy implements NamingStrategy { - - @Override - public String nameOf(Type type) { - String[] parts = org.apache.commons.lang3.StringUtils.splitByCharacterTypeCamelCase(type.getName()); - return String.join(" ", parts); - } - -} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java index 5c3d126b2..622060ca0 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java @@ -3,12 +3,15 @@ import com.structurizr.component.Type; /** - * Uses the simple/short name of the type (i.e. without the package name). + * Uses Apache commons-lang to split a camel/Pascal cased name into separate words + * (e.g. "CustomerRepository" -> "Customer Repository"). */ public class DefaultNamingStrategy implements NamingStrategy { + @Override public String nameOf(Type type) { - return type.getName(); + String[] parts = org.apache.commons.lang3.StringUtils.splitByCharacterTypeCamelCase(type.getName()); + return String.join(" ", parts); } -} +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/SimpleNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/SimpleNamingStrategy.java new file mode 100644 index 000000000..7b60b58de --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/SimpleNamingStrategy.java @@ -0,0 +1,14 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; + +/** + * Uses the simple/short name of the type (i.e. without the package name). + */ +public class SimpleNamingStrategy implements NamingStrategy { + + public String nameOf(Type type) { + return type.getName(); + } + +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java index fe13a91a8..7172f5fdb 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java @@ -4,7 +4,7 @@ import com.structurizr.component.ComponentFinderBuilder; import com.structurizr.component.ComponentFinderStrategyBuilder; import com.structurizr.component.matcher.NameSuffixTypeMatcher; -import com.structurizr.component.naming.CommonsLangCamelCaseNamingStrategy; +import com.structurizr.component.naming.DefaultNamingStrategy; import com.structurizr.model.*; public class Example { @@ -25,14 +25,12 @@ public static void main(String[] args) { .withStrategy( new ComponentFinderStrategyBuilder() .matchedBy(new NameSuffixTypeMatcher("Controller", "Web MVC Controller")) - .namedBy(new CommonsLangCamelCaseNamingStrategy()) .forEach(component -> user.uses(component, "Uses")) .build() ) .withStrategy( new ComponentFinderStrategyBuilder() .matchedBy(new NameSuffixTypeMatcher("Repository", "Data Repository")) - .namedBy(new CommonsLangCamelCaseNamingStrategy()) .forEach(component -> component.uses(databaseSchema, "Reads from and writes to")) .build() ) From 3d7e92b4a9f04a9074efaa57a393dc459aa432ad Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 22:49:23 +0100 Subject: [PATCH 225/418] structurizr-dsl: Adds support for element technology expressions (e.g. "element.technology==Java"). Also some refactoring. --- changelog.md | 1 + .../dsl/CustomViewExpressionParser.java | 2 +- .../dsl/DeploymentViewExpressionParser.java | 2 +- ...ssionParser.java => ExpressionParser.java} | 74 ++++++++++++++++--- .../dsl/ModelViewContentParser.java | 2 +- .../dsl/StaticViewExpressionParser.java | 2 +- .../dsl/StructurizrDslExpressions.java | 2 + ...rTests.java => ExpressionParserTests.java} | 30 +++++++- 8 files changed, 98 insertions(+), 17 deletions(-) rename structurizr-dsl/src/main/java/com/structurizr/dsl/{AbstractExpressionParser.java => ExpressionParser.java} (75%) rename structurizr-dsl/src/test/java/com/structurizr/dsl/{AbstractExpressionParserTests.java => ExpressionParserTests.java} (94%) diff --git a/changelog.md b/changelog.md index fd80cc2b1..b5076e453 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ - structurizr-dsl: Adds name-value properties to dynamic view relationship views. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. +- structurizr-dsl: Adds support for element technology expressions (e.g. "element.technology==Java"). ## 2.2.0 (2nd July 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java index 585a23899..16e5be56c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewExpressionParser.java @@ -8,7 +8,7 @@ import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; -final class CustomViewExpressionParser extends AbstractExpressionParser { +final class CustomViewExpressionParser extends ExpressionParser { @Override protected Set evaluateElementTypeExpression(String expr, DslContext context) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java index 1919888d4..33e9a5b37 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java @@ -7,7 +7,7 @@ import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; -final class DeploymentViewExpressionParser extends AbstractExpressionParser { +final class DeploymentViewExpressionParser extends ExpressionParser { @Override protected Set evaluateElementTypeExpression(String expr, DslContext context) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java similarity index 75% rename from structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java rename to structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java index 181d51db8..9599c79f1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java @@ -1,9 +1,6 @@ package com.structurizr.dsl; -import com.structurizr.model.Element; -import com.structurizr.model.ModelItem; -import com.structurizr.model.Relationship; -import com.structurizr.model.StaticStructureElementInstance; +import com.structurizr.model.*; import com.structurizr.util.StringUtils; import java.util.HashSet; @@ -13,7 +10,7 @@ import static com.structurizr.dsl.StructurizrDslExpressions.*; -abstract class AbstractExpressionParser { +class ExpressionParser { private static final String WILDCARD = "*"; @@ -24,6 +21,8 @@ static boolean isExpression(String token) { token.startsWith(ELEMENT_TYPE_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(ELEMENT_TAG_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(ELEMENT_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_TECHNOLOGY_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_TECHNOLOGY_NOT_EQUALS_EXPRESSION.toLowerCase()) || token.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION) || token.startsWith(ELEMENT_PARENT_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(RELATIONSHIP) || token.endsWith(RELATIONSHIP) || token.contains(RELATIONSHIP) || @@ -52,10 +51,10 @@ final Set parseExpression(String expr, DslContext context) { Set modelItems1 = evaluateExpression(expressions[0], context); Set modelItems2 = evaluateExpression(expressions[1], context); - Set elements = new HashSet<>(modelItems1); - elements.addAll(modelItems2); + Set modelItems = new HashSet<>(modelItems1); + modelItems.addAll(modelItems2); - return elements; + return modelItems; } else { return evaluateExpression(expr, context); } @@ -171,6 +170,20 @@ private Set evaluateExpression(String expr, DslContext context) { modelItems.add(element); } }); + } else if (expr.toLowerCase().startsWith(ELEMENT_TECHNOLOGY_EQUALS_EXPRESSION.toLowerCase())) { + String technology = expr.substring(ELEMENT_TECHNOLOGY_EQUALS_EXPRESSION.length()); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Container).map(e -> (Container)e).filter(c -> technology.equals(c.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Component).map(e -> (Component)e).filter(c -> technology.equals(c.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).map(e -> (DeploymentNode)e).filter(dn -> technology.equals(dn.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).map(e -> (InfrastructureNode)e).filter(in -> technology.equals(in.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(c -> technology.equals(c.getContainer().getTechnology())).collect(Collectors.toSet())); + } else if (expr.toLowerCase().startsWith(ELEMENT_TECHNOLOGY_NOT_EQUALS_EXPRESSION)) { + String technology = expr.substring(ELEMENT_TECHNOLOGY_NOT_EQUALS_EXPRESSION.length()); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Container).map(e -> (Container)e).filter(c -> !technology.equals(c.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Component).map(e -> (Component)e).filter(c -> !technology.equals(c.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).map(e -> (DeploymentNode)e).filter(dn -> !technology.equals(dn.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).map(e -> (InfrastructureNode)e).filter(in -> !technology.equals(in.getTechnology())).collect(Collectors.toSet())); + modelItems.addAll(context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(c -> !technology.equals(c.getContainer().getTechnology())).collect(Collectors.toSet())); } else if (expr.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION)) { String propertyName = expr.substring(expr.indexOf("[")+1, expr.indexOf("]")); String propertyValue = expr.substring(expr.indexOf("==")+2); @@ -266,7 +279,42 @@ private Set evaluateExpression(String expr, DslContext context) { return modelItems; } - protected abstract Set evaluateElementTypeExpression(String expr, DslContext context); + protected Set evaluateElementTypeExpression(String expr, DslContext context) { + Set elements = new LinkedHashSet<>(); + + String type = expr.substring(ELEMENT_TYPE_EQUALS_EXPRESSION.length()); + switch (type.toLowerCase()) { + case "custom": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof CustomElement).forEach(elements::add); + break; + case "person": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Person).forEach(elements::add); + break; + case "softwaresystem": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof SoftwareSystem).forEach(elements::add); + break; + case "container": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Container).forEach(elements::add); + break; + case "component": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof Component).forEach(elements::add); + break; + case "deploymentnode": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof DeploymentNode).forEach(elements::add); + break; + case "infrastructurenode": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).forEach(elements::add); + break; + case "softwaresysteminstance": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).forEach(elements::add); + break; + case "containerinstance": + context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).forEach(elements::add); + break; + } + + return elements; + } private boolean hasAllTags(ModelItem modelItem, String[] tags) { boolean result = true; @@ -318,7 +366,9 @@ private boolean hasProperty(ModelItem modelItem, String name, String value) { return result; } - protected abstract Set findAfferentCouplings(Element element); + protected Set findAfferentCouplings(Element element) { + return new LinkedHashSet<>(findAfferentCouplings(element, Element.class)); + } protected Set findAfferentCouplings(Element element, Class typeOfElement) { Set elements = new LinkedHashSet<>(); @@ -331,7 +381,9 @@ protected Set findAfferentCouplings(Element element return elements; } - protected abstract Set findEfferentCouplings(Element element); + protected Set findEfferentCouplings(Element element) { + return new LinkedHashSet<>(findEfferentCouplings(element, Element.class)); + } protected Set findEfferentCouplings(Element element, Class typeOfElement) { Set elements = new LinkedHashSet<>(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java index 44212f52b..2f89808c0 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java @@ -9,7 +9,7 @@ abstract class ModelViewContentParser extends AbstractParser { protected static final String ELEMENT_WILDCARD = "element==*"; protected boolean isExpression(String token) { - return AbstractExpressionParser.isExpression(token.toLowerCase()); + return ExpressionParser.isExpression(token.toLowerCase()); } protected void removeRelationshipFromView(Relationship relationship, ModelView view) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java index 756dd3799..dfa32f147 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewExpressionParser.java @@ -7,7 +7,7 @@ import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; -final class StaticViewExpressionParser extends AbstractExpressionParser { +final class StaticViewExpressionParser extends ExpressionParser { @Override protected Set evaluateElementTypeExpression(String expr, DslContext context) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java index 4626e5934..a10aa0924 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java @@ -3,6 +3,8 @@ class StructurizrDslExpressions { static final String ELEMENT_TYPE_EQUALS_EXPRESSION = "element.type=="; + static final String ELEMENT_TECHNOLOGY_EQUALS_EXPRESSION = "element.technology=="; + static final String ELEMENT_TECHNOLOGY_NOT_EQUALS_EXPRESSION = "element.technology!="; static final String ELEMENT_TAG_EQUALS_EXPRESSION = "element.tag=="; static final String ELEMENT_TAG_NOT_EQUALS_EXPRESSION = "element.tag!="; static final String ELEMENT_PROPERTY_EQUALS_EXPRESSION = "element\\.properties\\[.*]==.*"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java similarity index 94% rename from structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java rename to structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java index 32bf6fdfb..fcf91a830 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractExpressionParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java @@ -10,9 +10,9 @@ import static org.junit.jupiter.api.Assertions.*; -class AbstractExpressionParserTests extends AbstractTests { +class ExpressionParserTests extends AbstractTests { - private final StaticViewExpressionParser parser = new StaticViewExpressionParser(); + private final ExpressionParser parser = new ExpressionParser(); @Test void test_parseExpression_ThrowsAnException_WhenTheRelationshipSourceIsSpecifiedUsingLongSyntaxButDoesNotExist() { @@ -396,6 +396,32 @@ void test_parseExpression_ReturnsElementsAndElementInstances_WhenUsingAnElementT assertTrue(elements.contains(ssi)); // this is not tagged "Software System", but the element it's based upon is } + @Test + void test_parseExpression_ReturnsElementsAndElementInstances_WhenUsingAnElementTechnologyEqualsExpression() { + model.addPerson("User"); + SoftwareSystem ss = model.addSoftwareSystem("Software System"); + Container c = ss.addContainer("Container"); + c.setTechnology("Java"); + DeploymentNode dn = model.addDeploymentNode("DN"); + dn.setTechnology("EC2"); + InfrastructureNode in = dn.addInfrastructureNode("Infrastructure Node"); + in.setTechnology("ELB"); + ContainerInstance ci = dn.add(c); + + Set elements = parser.parseExpression("element.technology==Java", context()); + assertEquals(2, elements.size()); + assertTrue(elements.contains(c)); // this has a technology property of "Java" + assertTrue(elements.contains(ci)); // this has no technology property, but the element it's based upon is + + elements = parser.parseExpression("element.technology==EC2", context()); + assertEquals(1, elements.size()); + assertTrue(elements.contains(dn)); + + elements = parser.parseExpression("element.technology==ELB", context()); + assertEquals(1, elements.size()); + assertTrue(elements.contains(in)); + } + @Test void test_parseExpression_ReturnsElementsAndElementInstances_WhenUsingAnElementPropertyEqualsExpression() { SoftwareSystem a = model.addSoftwareSystem("A"); From 80cc375893af044215375ab50420bba19a837038 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 22:49:47 +0100 Subject: [PATCH 226/418] Remove import. --- .../src/main/java/com/structurizr/dsl/ModelDslContext.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java index ab9ea3ad2..7903af346 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java @@ -1,7 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Location; - final class ModelDslContext extends GroupableDslContext { ModelDslContext() { From db3da9b269e9e6814449564647f9a5bb64ac3c16 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 18 Aug 2024 22:53:02 +0100 Subject: [PATCH 227/418] Adds a way to get constant values from the DSL parser. --- .../main/java/com/structurizr/dsl/StructurizrDslParser.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 11633d6ec..50a2fc62f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1062,6 +1062,10 @@ private void registerIdentifier(String identifier, Relationship relationship) { relationship.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(relationship)); } + public String getConstant(String name) { + return constantsAndVariables.get(name).getValue(); + } + private boolean inContext(Class clazz) { if (contextStack.empty()) { return false; From f3786375dd783f05135e2a7edb572d39cff49e3e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 19 Aug 2024 08:12:00 +0100 Subject: [PATCH 228/418] Tidy and some tests. --- .../java/com/structurizr/component/Type.java | 40 +++++++++---------- .../com/structurizr/component/TypeTests.java | 23 +++++++++++ .../component/example/Example.java | 1 - ...mePackageSupportingTypesStrategyTests.java | 29 ++++++++++++++ ...ncedTypesSupportingTypesStrategyTests.java | 30 ++++++++++++++ .../DefaultSupportingTypesStrategyTests.java | 21 ++++++++++ 6 files changed, 123 insertions(+), 21 deletions(-) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/TypeTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index 33a355248..56c5cca3f 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -3,6 +3,7 @@ import org.apache.bcel.classfile.JavaClass; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; @@ -11,21 +12,20 @@ */ public final class Type { - private JavaClass javaClass; + private final JavaClass javaClass; + private final String fullyQualifiedName; private String description; private String source; - private final Set dependencies = new HashSet<>(); - - private final String fullyQualifiedName; + private final Set dependencies = new LinkedHashSet<>(); public Type(JavaClass javaClass) { - this(javaClass.getClassName()); - + this.fullyQualifiedName = javaClass.getClassName(); this.javaClass = javaClass; } public Type(String fullyQualifiedName) { this.fullyQualifiedName = fullyQualifiedName; + this.javaClass = null; } public String getFullyQualifiedName() { @@ -56,19 +56,6 @@ public void setSource(String source) { this.source = source; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Type type = (Type) o; - return fullyQualifiedName.equals(type.fullyQualifiedName); - } - - @Override - public int hashCode() { - return Objects.hash(fullyQualifiedName); - } - public JavaClass getJavaClass() { return this.javaClass; } @@ -78,13 +65,26 @@ public void addDependency(Type type) { } public Set getDependencies() { - return new HashSet<>(dependencies); + return new LinkedHashSet<>(dependencies); } public boolean isAbstract() { return javaClass.isAbstract(); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Type type = (Type) o; + return fullyQualifiedName.equals(type.fullyQualifiedName); + } + + @Override + public int hashCode() { + return Objects.hash(fullyQualifiedName); + } + @Override public String toString() { return this.fullyQualifiedName; diff --git a/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java b/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java new file mode 100644 index 000000000..d73f761ba --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java @@ -0,0 +1,23 @@ +package com.structurizr.component; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TypeTests { + + private Type type; + + @Test + void name() { + type = new Type("com.example.ClassName"); + assertEquals("ClassName", type.getName()); + } + + @Test + void packageName() { + type = new Type("com.example.ClassName"); + assertEquals("com.example", type.getPackageName()); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java index 7172f5fdb..292199f39 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java @@ -4,7 +4,6 @@ import com.structurizr.component.ComponentFinderBuilder; import com.structurizr.component.ComponentFinderStrategyBuilder; import com.structurizr.component.matcher.NameSuffixTypeMatcher; -import com.structurizr.component.naming.DefaultNamingStrategy; import com.structurizr.model.*; public class Example { diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java new file mode 100644 index 000000000..39db1619c --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java @@ -0,0 +1,29 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AllReferencedTypesInSamePackageSupportingTypesStrategyTests { + + @Test + void findSupportingTypes() { + Type type = new Type("com.example.a.A"); + + Type dependency1 = new Type("com.example.a.AImpl"); + Type dependency2 = new Type("com.example.util.SomeUtils"); + type.addDependency(dependency1); + type.addDependency(dependency2); + + type.addDependency(new Type("com.example.util.SomeUtils")); + + Set supportingTypes = new AllReferencedTypesInSamePackageSupportingTypesStrategy().findSupportingTypes(type); + assertEquals(1, supportingTypes.size()); + assertTrue(supportingTypes.contains(dependency1)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java new file mode 100644 index 000000000..e0afc769a --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java @@ -0,0 +1,30 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AllReferencedTypesSupportingTypesStrategyTests { + + @Test + void findSupportingTypes() { + Type type = new Type("com.example.a.A"); + + Type dependency1 = new Type("com.example.a.AImpl"); + Type dependency2 = new Type("com.example.util.SomeUtils"); + type.addDependency(dependency1); + type.addDependency(dependency2); + + type.addDependency(new Type("com.example.util.SomeUtils")); + + Set supportingTypes = new AllReferencedTypesSupportingTypesStrategy().findSupportingTypes(type); + assertEquals(2, supportingTypes.size()); + assertTrue(supportingTypes.contains(dependency1)); + assertTrue(supportingTypes.contains(dependency2)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java new file mode 100644 index 000000000..6ff35a779 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java @@ -0,0 +1,21 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultSupportingTypesStrategyTests { + + @Test + void findSupportingTypes() { + Type type = new Type("com.example.a.A"); + type.addDependency(new Type("com.example.a.AImpl")); + + Set supportingTypes = new DefaultSupportingTypesStrategy().findSupportingTypes(type); + assertTrue(supportingTypes.isEmpty()); + } + +} \ No newline at end of file From 4bab88ff95d86049116bdb38f9d1f71336b645a7 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 19 Aug 2024 08:31:11 +0100 Subject: [PATCH 229/418] Tidy and more tests. --- .../matcher/AnnotationTypeMatcher.java | 8 +++++ .../component/matcher/ExtendsTypeMatcher.java | 8 +++++ .../matcher/ImplementsTypeMatcher.java | 8 +++++ .../matcher/NameSuffixTypeMatcher.java | 4 +++ .../component/matcher/RegexTypeMatcher.java | 21 ++++------- .../matcher/NameSuffixTypeMatcherTests.java | 36 +++++++++++++++++++ .../matcher/RegexSuffixTypeMatcherTests.java | 36 +++++++++++++++++++ .../naming/DefaultNamingStrategyTests.java | 15 ++++++++ .../FullyQualifiedNamingStrategyTests.java | 15 ++++++++ .../naming/SimpleNamingStrategyTests.java | 15 ++++++++ 10 files changed, 151 insertions(+), 15 deletions(-) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/naming/DefaultNamingStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/naming/FullyQualifiedNamingStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/naming/SimpleNamingStrategyTests.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java index 2674204d8..671fc2b42 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java @@ -35,6 +35,14 @@ public AnnotationTypeMatcher(Class annotation, String tech @Override public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + if (type.getJavaClass() == null) { + throw new IllegalArgumentException("This type matcher requires a BCEL JavaClass"); + } + AnnotationEntry[] annotationEntries = type.getJavaClass().getAnnotationEntries(); for (AnnotationEntry annotationEntry : annotationEntries) { if (annotationType.equals(annotationEntry.getAnnotationType())) { diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java index 2ae97afe7..497f6cde3 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -26,6 +26,14 @@ public ExtendsTypeMatcher(String className, String technology) { @Override public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + if (type.getJavaClass() == null) { + throw new IllegalArgumentException("This type matcher requires a BCEL JavaClass"); + } + JavaClass javaClass = type.getJavaClass(); try { Set superClasses = Stream.of(javaClass.getSuperClasses()).map(JavaClass::getClassName).collect(Collectors.toSet()); diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java index 643a6e361..91fc9b624 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -24,6 +24,14 @@ public ImplementsTypeMatcher(String interfaceName, String technology) { @Override public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + if (type.getJavaClass() == null) { + throw new IllegalArgumentException("This type matcher requires a BCEL JavaClass"); + } + JavaClass javaClass = type.getJavaClass(); Set interfaceNames = Set.of(javaClass.getInterfaceNames()); return interfaceNames.contains(interfaceName); diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java index e1a17b5db..01f27091a 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java @@ -21,6 +21,10 @@ public NameSuffixTypeMatcher(String suffix, String technology) { @Override public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + return type.getFullyQualifiedName().endsWith(suffix); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java index 565606102..0f9c8a8f9 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java @@ -1,6 +1,7 @@ package com.structurizr.component.matcher; import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; import java.util.regex.Pattern; @@ -14,30 +15,20 @@ public class RegexTypeMatcher extends AbstractTypeMatcher { public RegexTypeMatcher(String regex, String technology) { super(technology); - if (regex == null) { + if (StringUtils.isNullOrEmpty(regex)) { throw new IllegalArgumentException("A regex must be supplied"); } this.regex = Pattern.compile(regex); } - public RegexTypeMatcher(Pattern regex, String technology) { - super(technology); - - if (regex == null) { - throw new IllegalArgumentException("A regex must be supplied"); - } - - this.regex = regex; - } - @Override public boolean matches(Type type) { - if (type != null && type.getFullyQualifiedName() != null) { - return Pattern.matches(regex.pattern(), type.getFullyQualifiedName()); - } else { - return false; + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); } + + return Pattern.matches(regex.pattern(), type.getFullyQualifiedName()); } @Override diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java new file mode 100644 index 000000000..80dff112e --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java @@ -0,0 +1,36 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class NameSuffixTypeMatcherTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullSuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher(null, "Technology")); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher(" ", "Technology")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("Suffix", "Technology").matches(null)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoMatch() { + assertFalse(new NameSuffixTypeMatcher("Component", "Technology").matches(new Type("com.example.SomeClass"))); + } + + @Test + void matches_ReturnsTrue_WhenThereIsAMatch() { + assertTrue(new NameSuffixTypeMatcher("Component", "Technology").matches(new Type("com.example.SomeComponent"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java new file mode 100644 index 000000000..a54da7b7c --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java @@ -0,0 +1,36 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class RegexSuffixTypeMatcherTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullRegex() { + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(null, "Technology")); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyRegex() { + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher("", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(" ", "Technology")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(".*Controller", "Technology").matches(null)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoMatch() { + assertFalse(new RegexTypeMatcher(".*Controller", "Technology").matches(new Type("com.example.SomeClass"))); + } + + @Test + void matches_ReturnsTrue_WhenThereIsAMatch() { + assertTrue(new RegexTypeMatcher(".*Controller", "Technology").matches(new Type("com.example.SomeController"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/naming/DefaultNamingStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/naming/DefaultNamingStrategyTests.java new file mode 100644 index 000000000..f6f8f726d --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/naming/DefaultNamingStrategyTests.java @@ -0,0 +1,15 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DefaultNamingStrategyTests { + + @Test + void nameOf() { + assertEquals("Class Name", new DefaultNamingStrategy().nameOf(new Type("com.example.ClassName"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/naming/FullyQualifiedNamingStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/naming/FullyQualifiedNamingStrategyTests.java new file mode 100644 index 000000000..c3d72fa2e --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/naming/FullyQualifiedNamingStrategyTests.java @@ -0,0 +1,15 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FullyQualifiedNamingStrategyTests { + + @Test + void nameOf() { + assertEquals("com.example.ClassName", new FullyQualifiedNamingStrategy().nameOf(new Type("com.example.ClassName"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/naming/SimpleNamingStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/naming/SimpleNamingStrategyTests.java new file mode 100644 index 000000000..a85aae823 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/naming/SimpleNamingStrategyTests.java @@ -0,0 +1,15 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SimpleNamingStrategyTests { + + @Test + void nameOf() { + assertEquals("ClassName", new SimpleNamingStrategy().nameOf(new Type("com.example.ClassName"))); + } + +} \ No newline at end of file From 3db0c028e4d996dc644cb4d826e52791a65a939a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 19 Aug 2024 11:40:32 +0100 Subject: [PATCH 230/418] Some tweaks to support a Spring Modulith style approach, where packages are considered to be components. --- .../component/ComponentFinder.java | 7 ++++-- .../structurizr/component/TypeRepository.java | 2 +- .../matcher/AnnotationTypeMatcher.java | 2 +- .../component/matcher/ExtendsTypeMatcher.java | 2 +- .../matcher/ImplementsTypeMatcher.java | 2 +- .../naming/DefaultPackageNamingStrategy.java | 25 +++++++++++++++++++ .../provider/SourceDirectoryTypeProvider.java | 24 ++++++++++++++++++ ...ypesInPackageSupportingTypesStrategy.java} | 5 ++-- ...eferencedTypesSupportingTypesStrategy.java | 3 ++- ...TypesInPackageSupportingTypesStrategy.java | 23 +++++++++++++++++ ...esUnderPackageSupportingTypesStrategy.java | 23 +++++++++++++++++ .../DefaultSupportingTypesStrategy.java | 3 ++- .../supporting/SupportingTypesStrategy.java | 3 ++- .../DefaultPackageNamingStrategyTests.java | 15 +++++++++++ ...nPackageSupportingTypesStrategyTests.java} | 4 +-- ...ncedTypesSupportingTypesStrategyTests.java | 2 +- .../DefaultSupportingTypesStrategyTests.java | 2 +- 17 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/naming/DefaultPackageNamingStrategy.java rename structurizr-component/src/main/java/com/structurizr/component/supporting/{AllReferencedTypesInSamePackageSupportingTypesStrategy.java => AllReferencedTypesInPackageSupportingTypesStrategy.java} (68%) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategy.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/naming/DefaultPackageNamingStrategyTests.java rename structurizr-component/src/test/java/com/structurizr/component/supporting/{AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java => AllReferencedTypesInPackageSupportingTypesStrategyTests.java} (79%) diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index b12b7f04c..ef6edac08 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -34,7 +34,12 @@ public final class ComponentFinder { } this.container = container; + this.componentFinderStrategies.addAll(componentFinderStrategies); + + findTypes(typeProviders); + } + private void findTypes(Collection typeProviders) { for (TypeProvider typeProvider : typeProviders) { Set types = typeProvider.getTypes(); for (com.structurizr.component.Type type : types) { @@ -59,8 +64,6 @@ public final class ComponentFinder { findDependencies(type); } } - - this.componentFinderStrategies.addAll(componentFinderStrategies); } private void findDependencies(com.structurizr.component.Type type) { diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java index 45f80f365..faa707b57 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java @@ -3,7 +3,7 @@ import java.util.LinkedHashSet; import java.util.Set; -final class TypeRepository { +public final class TypeRepository { private final Set types = new LinkedHashSet<>(); diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java index 671fc2b42..ceadefbf1 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java @@ -40,7 +40,7 @@ public boolean matches(Type type) { } if (type.getJavaClass() == null) { - throw new IllegalArgumentException("This type matcher requires a BCEL JavaClass"); + return false; } AnnotationEntry[] annotationEntries = type.getJavaClass().getAnnotationEntries(); diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java index 497f6cde3..ce402eb40 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -31,7 +31,7 @@ public boolean matches(Type type) { } if (type.getJavaClass() == null) { - throw new IllegalArgumentException("This type matcher requires a BCEL JavaClass"); + return false; } JavaClass javaClass = type.getJavaClass(); diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java index 91fc9b624..c06f73614 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -29,7 +29,7 @@ public boolean matches(Type type) { } if (type.getJavaClass() == null) { - throw new IllegalArgumentException("This type matcher requires a BCEL JavaClass"); + return false; } JavaClass javaClass = type.getJavaClass(); diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultPackageNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultPackageNamingStrategy.java new file mode 100644 index 000000000..4f93efecd --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultPackageNamingStrategy.java @@ -0,0 +1,25 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; + +/** + * Uses Apache commons-lang to split a camel-cased package name into separate words + * (e.g. "com.example.order.package-info" -> "Order"). + */ +public class DefaultPackageNamingStrategy implements NamingStrategy { + + @Override + public String nameOf(Type type) { + String packageName = type.getPackageName(); + if (packageName.contains(".")) { + packageName = packageName.substring(packageName.lastIndexOf(".") + 1); + } + + String[] parts = org.apache.commons.lang3.StringUtils.splitByCharacterTypeCamelCase(packageName); + String name = String.join(" ", parts); + name = name.substring(0, 1).toUpperCase() + name.substring(1); + + return name; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java index f68630a6c..808006c48 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -2,6 +2,8 @@ import com.github.javaparser.ParserConfiguration; import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.PackageDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.comments.JavadocComment; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; @@ -69,6 +71,28 @@ public void visit(ClassOrInterfaceDeclaration n, Object arg) { type.setDescription(new JavadocCommentFilter(maximumDescriptionLength).filterAndTruncate(description)); } + types.add(type); + } + } + + @Override + public void visit(PackageDeclaration n, Object arg) { + String PACKAGE_INFO_JAVA_SOURCE = "package-info.java"; + String PACKAGE_INFO_SUFFIX = ".package-info"; + + if (path.getName().endsWith(PACKAGE_INFO_JAVA_SOURCE)) { + String fullyQualifiedName = n.getName().asString() + PACKAGE_INFO_SUFFIX; + + Type type = new Type(fullyQualifiedName); + type.setSource(path.getAbsolutePath()); + + Node rootNode = n.findRootNode(); + if (rootNode != null && rootNode.getComment().isPresent() && rootNode.getComment().get() instanceof JavadocComment) { + JavadocComment javadocComment = (JavadocComment)rootNode.getComment().get(); + String description = javadocComment.parse().getDescription().toText(); + + type.setDescription(new JavadocCommentFilter(maximumDescriptionLength).filterAndTruncate(description)); + } types.add(type); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategy.java similarity index 68% rename from structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategy.java rename to structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategy.java index 5791d7943..2e34875e2 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategy.java @@ -1,6 +1,7 @@ package com.structurizr.component.supporting; import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; import java.util.Set; import java.util.stream.Collectors; @@ -8,10 +9,10 @@ /** * A strategy that finds all referenced types in the same package as the component type. */ -public class AllReferencedTypesInSamePackageSupportingTypesStrategy implements SupportingTypesStrategy { +public class AllReferencedTypesInPackageSupportingTypesStrategy implements SupportingTypesStrategy { @Override - public Set findSupportingTypes(Type type) { + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { String packageName = type.getPackageName(); return type.getDependencies().stream() diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java index 28738a7aa..e2b4724ce 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java @@ -1,6 +1,7 @@ package com.structurizr.component.supporting; import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; import java.util.Set; @@ -10,7 +11,7 @@ public class AllReferencedTypesSupportingTypesStrategy implements SupportingTypesStrategy { @Override - public Set findSupportingTypes(Type type) { + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { return type.getDependencies(); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategy.java new file mode 100644 index 000000000..cc998f5ed --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategy.java @@ -0,0 +1,23 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A strategy that finds all referenced types in the same package as the component type. + */ +public class AllTypesInPackageSupportingTypesStrategy implements SupportingTypesStrategy { + + @Override + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { + String packageName = type.getPackageName(); + + return typeRepository.getTypes().stream() + .filter(dependency -> dependency.getPackageName().equals(packageName)) + .collect(Collectors.toSet()); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategy.java new file mode 100644 index 000000000..59f7a2a3a --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategy.java @@ -0,0 +1,23 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A strategy that finds all referenced types in the same package as the component type. + */ +public class AllTypesUnderPackageSupportingTypesStrategy implements SupportingTypesStrategy { + + @Override + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { + String packageName = type.getPackageName(); + + return typeRepository.getTypes().stream() + .filter(dependency -> dependency.getPackageName().startsWith(packageName)) + .collect(Collectors.toSet()); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java index 58f1ec8cd..65639a110 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java @@ -1,6 +1,7 @@ package com.structurizr.component.supporting; import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; import java.util.Collections; import java.util.Set; @@ -11,7 +12,7 @@ public class DefaultSupportingTypesStrategy implements SupportingTypesStrategy { @Override - public Set findSupportingTypes(Type type) { + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { return Collections.emptySet(); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java index 1d226daca..e3d6f21a3 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java @@ -1,6 +1,7 @@ package com.structurizr.component.supporting; import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; import java.util.Set; @@ -9,6 +10,6 @@ */ public interface SupportingTypesStrategy { - Set findSupportingTypes(Type type); + Set findSupportingTypes(Type type, TypeRepository typeRepository); } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/naming/DefaultPackageNamingStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/naming/DefaultPackageNamingStrategyTests.java new file mode 100644 index 000000000..7adf582ae --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/naming/DefaultPackageNamingStrategyTests.java @@ -0,0 +1,15 @@ +package com.structurizr.component.naming; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DefaultPackageNamingStrategyTests { + + @Test + void nameOf() { + assertEquals("Order Management", new DefaultPackageNamingStrategy().nameOf(new Type("com.example.orderManagement.package-info"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java similarity index 79% rename from structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java rename to structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java index 39db1619c..761b08d84 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInSamePackageSupportingTypesStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java @@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class AllReferencedTypesInSamePackageSupportingTypesStrategyTests { +public class AllReferencedTypesInPackageSupportingTypesStrategyTests { @Test void findSupportingTypes() { @@ -21,7 +21,7 @@ void findSupportingTypes() { type.addDependency(new Type("com.example.util.SomeUtils")); - Set supportingTypes = new AllReferencedTypesInSamePackageSupportingTypesStrategy().findSupportingTypes(type); + Set supportingTypes = new AllReferencedTypesInPackageSupportingTypesStrategy().findSupportingTypes(type, null); assertEquals(1, supportingTypes.size()); assertTrue(supportingTypes.contains(dependency1)); } diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java index e0afc769a..80f8a15f9 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java @@ -21,7 +21,7 @@ void findSupportingTypes() { type.addDependency(new Type("com.example.util.SomeUtils")); - Set supportingTypes = new AllReferencedTypesSupportingTypesStrategy().findSupportingTypes(type); + Set supportingTypes = new AllReferencedTypesSupportingTypesStrategy().findSupportingTypes(type, null); assertEquals(2, supportingTypes.size()); assertTrue(supportingTypes.contains(dependency1)); assertTrue(supportingTypes.contains(dependency2)); diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java index 6ff35a779..ff83615a2 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategyTests.java @@ -14,7 +14,7 @@ void findSupportingTypes() { Type type = new Type("com.example.a.A"); type.addDependency(new Type("com.example.a.AImpl")); - Set supportingTypes = new DefaultSupportingTypesStrategy().findSupportingTypes(type); + Set supportingTypes = new DefaultSupportingTypesStrategy().findSupportingTypes(type, null); assertTrue(supportingTypes.isEmpty()); } From ff86a22716ab019443cc0b9b9ccd0779e10dd658 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 19 Aug 2024 11:43:01 +0100 Subject: [PATCH 231/418] . --- .../java/com/structurizr/component/ComponentFinderStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index 1da4f7de7..04d499361 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -49,7 +49,7 @@ Set findComponents(TypeRepository typeRepository) { components.add(component); // now find supporting types - Set supportingTypes = supportingTypesStrategy.findSupportingTypes(type); + Set supportingTypes = supportingTypesStrategy.findSupportingTypes(type, typeRepository); component.addSupportingTypes(supportingTypes); } } From 21f42f8ee6c6c73d3999693a1eaf7bb18959b2a4 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 19 Aug 2024 13:13:58 +0100 Subject: [PATCH 232/418] Turned the example into a test. --- .../component/example/Example.java | 58 --------------- .../component/example/ExampleTests.java | 72 +++++++++++++++++++ .../example/repository/IgnoredRepository.java | 5 ++ 3 files changed, 77 insertions(+), 58 deletions(-) delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/Example.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java b/structurizr-component/src/test/java/com/structurizr/component/example/Example.java deleted file mode 100644 index 292199f39..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/example/Example.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.structurizr.component.example; - -import com.structurizr.Workspace; -import com.structurizr.component.ComponentFinderBuilder; -import com.structurizr.component.ComponentFinderStrategyBuilder; -import com.structurizr.component.matcher.NameSuffixTypeMatcher; -import com.structurizr.model.*; - -public class Example { - - public static void main(String[] args) { - Workspace workspace = new Workspace("Name", "Description"); - workspace.getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); - - Person user = workspace.getModel().addPerson("User"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); - Container webApplication = softwareSystem.addContainer("Web Application"); - Container databaseSchema = softwareSystem.addContainer("Database Schema"); - - new ComponentFinderBuilder() - .forContainer(webApplication) - .fromClasses("structurizr-component/build/classes/java/test") - .fromSource("structurizr-component/src/test/java") - .withStrategy( - new ComponentFinderStrategyBuilder() - .matchedBy(new NameSuffixTypeMatcher("Controller", "Web MVC Controller")) - .forEach(component -> user.uses(component, "Uses")) - .build() - ) - .withStrategy( - new ComponentFinderStrategyBuilder() - .matchedBy(new NameSuffixTypeMatcher("Repository", "Data Repository")) - .forEach(component -> component.uses(databaseSchema, "Reads from and writes to")) - .build() - ) - .build().findComponents(); - - for (Element element : workspace.getModel().getElements()) { - print(element); - } - } - - private static void print(Element element) { - System.out.println("[" + element.getClass().getSimpleName() + "] " + element.getName()); - System.out.println(" - Description: " + element.getDescription()); - - if (element instanceof Component component) { - System.out.println(" - Technology: " + component.getTechnology()); - System.out.println(" - Type: " + element.getProperties().get("component.type")); - System.out.println(" - Source: " + element.getProperties().get("component.src")); - } - - for (Relationship relationship : element.getRelationships()) { - System.out.println(" -> [" + relationship.getDestination().getClass().getSimpleName() + "] " + relationship.getDestination().getName()); - } - } - -} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java b/structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java new file mode 100644 index 000000000..4a72fd1be --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java @@ -0,0 +1,72 @@ +package com.structurizr.component.example; + +import com.structurizr.Workspace; +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.component.ComponentFinderStrategyBuilder; +import com.structurizr.component.filter.ExcludeTypesByRegexFilter; +import com.structurizr.component.matcher.NameSuffixTypeMatcher; +import com.structurizr.component.matcher.RegexTypeMatcher; +import com.structurizr.component.naming.SimpleNamingStrategy; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExampleTests { + + @Test + void run() { + Workspace workspace = new Workspace("Name", "Description"); + + Person user = workspace.getModel().addPerson("User"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container webApplication = softwareSystem.addContainer("Web Application"); + Container databaseSchema = softwareSystem.addContainer("Database Schema"); + + new ComponentFinderBuilder() + .forContainer(webApplication) + .fromClasses("build/classes/java/test") + .fromSource("src/test/java") + .withStrategy( + new ComponentFinderStrategyBuilder() + .matchedBy(new NameSuffixTypeMatcher("Controller", "Web MVC Controller")) + .forEach(component -> user.uses(component, "Uses")) + .build() + ) + .withStrategy( + new ComponentFinderStrategyBuilder() + .matchedBy(new RegexTypeMatcher(".*\\..*Repository", "Data Repository")) + .filteredBy(new ExcludeTypesByRegexFilter(".*Ignored.*")) + .forEach(component -> component.uses(databaseSchema, "Reads from and writes to")) + .namedBy(new SimpleNamingStrategy()) + .build() + ) + .build().findComponents(); + + assertEquals(2, webApplication.getComponents().size()); + + Component controller = webApplication.getComponentWithName("Customer Controller"); + assertNotNull(controller); + assertEquals("Allows users to view a list of customers.", controller.getDescription()); + assertEquals("Web MVC Controller", controller.getTechnology()); + assertEquals("com.structurizr.component.example.controller.CustomerController", controller.getProperties().get("component.type")); + assertTrue(controller.getProperties().get("component.src").endsWith("/src/test/java/com/structurizr/component/example/controller/CustomerController.java")); + + Component repository = webApplication.getComponentWithName("CustomerRepository"); + assertNotNull(repository); + assertEquals("Provides a way to access customer data.", repository.getDescription()); + assertEquals("Data Repository", repository.getTechnology()); + assertEquals("com.structurizr.component.example.repository.CustomerRepository", repository.getProperties().get("component.type")); + assertTrue(repository.getProperties().get("component.src").endsWith("/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java")); + + assertEquals(1, controller.getRelationships().size()); + assertNotNull(controller.getEfferentRelationshipWith(repository)); + + assertNotNull(user.getEfferentRelationshipWith(controller)); + assertNotNull(repository.getEfferentRelationshipWith(databaseSchema)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java b/structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java new file mode 100644 index 000000000..b4365d13f --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java @@ -0,0 +1,5 @@ +package com.structurizr.component.example.repository; + +public interface IgnoredRepository { + +} \ No newline at end of file From 76ebaf49cd8caac3470d210fe396a60e0e3bcc89 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 08:57:41 +0100 Subject: [PATCH 233/418] Tidy and more tests. --- .../java/com/structurizr/component/Type.java | 9 +++ .../component/matcher/ExtendsTypeMatcher.java | 4 ++ .../matcher/ImplementsTypeMatcher.java | 4 ++ .../component/example/ExampleTests.java | 72 ------------------- .../controller/CustomerController.java | 19 ----- .../component/example/domain/Customer.java | 4 -- .../repository/CustomerRepository.java | 14 ---- .../repository/CustomerRepositoryImpl.java | 15 ---- .../example/repository/IgnoredRepository.java | 5 -- .../matcher/AnnotationTypeMatcherTests.java | 60 ++++++++++++++++ .../matcher/ExtendsTypeMatcherTests.java | 55 ++++++++++++++ .../matcher/ImplementsTypeMatcherTests.java | 54 ++++++++++++++ .../annotationTypeMatcher/Controller.java | 4 ++ .../CustomerController.java | 5 ++ .../annotationTypeMatcher/Repository.java | 4 ++ .../AbstractController.java | 4 ++ .../AbstractRepository.java | 4 ++ .../CustomerController.java | 4 ++ .../implementsTypeMatcher/Controller.java | 4 ++ .../CustomerController.java | 4 ++ .../implementsTypeMatcher/Repository.java | 4 ++ 21 files changed, 223 insertions(+), 129 deletions(-) delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Repository.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractController.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractRepository.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Controller.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Repository.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index 56c5cca3f..0f0eed870 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -1,5 +1,6 @@ package com.structurizr.component; +import com.structurizr.util.StringUtils; import org.apache.bcel.classfile.JavaClass; import java.util.HashSet; @@ -19,11 +20,19 @@ public final class Type { private final Set dependencies = new LinkedHashSet<>(); public Type(JavaClass javaClass) { + if (javaClass == null) { + throw new IllegalArgumentException("A BCEL JavaClass must be supplied"); + } + this.fullyQualifiedName = javaClass.getClassName(); this.javaClass = javaClass; } public Type(String fullyQualifiedName) { + if (StringUtils.isNullOrEmpty(fullyQualifiedName)) { + throw new IllegalArgumentException("A fully qualified name must be supplied"); + } + this.fullyQualifiedName = fullyQualifiedName; this.javaClass = null; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java index ce402eb40..103d18b54 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -21,6 +21,10 @@ public class ExtendsTypeMatcher extends AbstractTypeMatcher { public ExtendsTypeMatcher(String className, String technology) { super(technology); + if (className == null || className.trim().length() == 0) { + throw new IllegalArgumentException("A fully qualified class name must be supplied"); + } + this.className = className; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java index c06f73614..54aecdcb7 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -19,6 +19,10 @@ public class ImplementsTypeMatcher extends AbstractTypeMatcher { public ImplementsTypeMatcher(String interfaceName, String technology) { super(technology); + if (interfaceName == null || interfaceName.trim().length() == 0) { + throw new IllegalArgumentException("A fully qualified interface name must be supplied"); + } + this.interfaceName = interfaceName; } diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java b/structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java deleted file mode 100644 index 4a72fd1be..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/example/ExampleTests.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.structurizr.component.example; - -import com.structurizr.Workspace; -import com.structurizr.component.ComponentFinderBuilder; -import com.structurizr.component.ComponentFinderStrategyBuilder; -import com.structurizr.component.filter.ExcludeTypesByRegexFilter; -import com.structurizr.component.matcher.NameSuffixTypeMatcher; -import com.structurizr.component.matcher.RegexTypeMatcher; -import com.structurizr.component.naming.SimpleNamingStrategy; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -public class ExampleTests { - - @Test - void run() { - Workspace workspace = new Workspace("Name", "Description"); - - Person user = workspace.getModel().addPerson("User"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); - Container webApplication = softwareSystem.addContainer("Web Application"); - Container databaseSchema = softwareSystem.addContainer("Database Schema"); - - new ComponentFinderBuilder() - .forContainer(webApplication) - .fromClasses("build/classes/java/test") - .fromSource("src/test/java") - .withStrategy( - new ComponentFinderStrategyBuilder() - .matchedBy(new NameSuffixTypeMatcher("Controller", "Web MVC Controller")) - .forEach(component -> user.uses(component, "Uses")) - .build() - ) - .withStrategy( - new ComponentFinderStrategyBuilder() - .matchedBy(new RegexTypeMatcher(".*\\..*Repository", "Data Repository")) - .filteredBy(new ExcludeTypesByRegexFilter(".*Ignored.*")) - .forEach(component -> component.uses(databaseSchema, "Reads from and writes to")) - .namedBy(new SimpleNamingStrategy()) - .build() - ) - .build().findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - - Component controller = webApplication.getComponentWithName("Customer Controller"); - assertNotNull(controller); - assertEquals("Allows users to view a list of customers.", controller.getDescription()); - assertEquals("Web MVC Controller", controller.getTechnology()); - assertEquals("com.structurizr.component.example.controller.CustomerController", controller.getProperties().get("component.type")); - assertTrue(controller.getProperties().get("component.src").endsWith("/src/test/java/com/structurizr/component/example/controller/CustomerController.java")); - - Component repository = webApplication.getComponentWithName("CustomerRepository"); - assertNotNull(repository); - assertEquals("Provides a way to access customer data.", repository.getDescription()); - assertEquals("Data Repository", repository.getTechnology()); - assertEquals("com.structurizr.component.example.repository.CustomerRepository", repository.getProperties().get("component.type")); - assertTrue(repository.getProperties().get("component.src").endsWith("/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java")); - - assertEquals(1, controller.getRelationships().size()); - assertNotNull(controller.getEfferentRelationshipWith(repository)); - - assertNotNull(user.getEfferentRelationshipWith(controller)); - assertNotNull(repository.getEfferentRelationshipWith(databaseSchema)); - } - -} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java b/structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java deleted file mode 100644 index 409e4dd8c..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/example/controller/CustomerController.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.structurizr.component.example.controller; - -import com.structurizr.component.example.repository.CustomerRepository; - -/** - * Allows users to view a list of customers. - */ -public class CustomerController { - - private CustomerRepository customerRepository; - - public CustomerController(CustomerRepository customerRepository) { - this.customerRepository = customerRepository; - } - - void showCustomersPage() { - customerRepository.getCustomers(); - } -} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java b/structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java deleted file mode 100644 index ca9637df3..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/example/domain/Customer.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.component.example.domain; - -public class Customer { -} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java b/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java deleted file mode 100644 index d7040cd2e..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.structurizr.component.example.repository; - -import com.structurizr.component.example.domain.Customer; - -import java.util.List; - -/** - * Provides a way to access customer data. - */ -public interface CustomerRepository { - - List getCustomers(); - -} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java b/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java deleted file mode 100644 index 43ef69bdc..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/example/repository/CustomerRepositoryImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.component.example.repository; - -import com.structurizr.component.example.domain.Customer; - -import java.util.ArrayList; -import java.util.List; - -public class CustomerRepositoryImpl implements CustomerRepository { - - @Override - public List getCustomers() { - return new ArrayList<>(); - } - -} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java b/structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java deleted file mode 100644 index b4365d13f..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/example/repository/IgnoredRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.structurizr.component.example.repository; - -public interface IgnoredRepository { - -} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java new file mode 100644 index 000000000..fc124dfec --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java @@ -0,0 +1,60 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import org.apache.bcel.classfile.ClassParser; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.lang.annotation.Annotation; + +import static org.junit.jupiter.api.Assertions.*; + +public class AnnotationTypeMatcherTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher((String)null, "Technology")); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher("", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher(" ", "Technology")); + } + + @Test + void construction_ThrowsAnException_WhenPassedANullClass() { + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher((Class) null, "Technology")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher("com.example.AnnotationName", "Technology").matches(null)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoUnderlyingJavaClass() { + Type type = new Type("com.structurizr.component.matcher.annotationTypeMatcher.CustomerController"); + + assertFalse(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Controller", "Technology").matches(type)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoMatch() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + assertFalse(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Repository", "Technology").matches(type)); + } + + @Test + void matches_ReturnsTrue_WhenThereIsAMatch() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + assertTrue(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Controller", "Technology").matches(type)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java new file mode 100644 index 000000000..40b71bcb5 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java @@ -0,0 +1,55 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExtendsTypeMatcherTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher(null, "Technology")); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher("", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher(" ", "Technology")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher("com.example.ClassName", "Technology").matches(null)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoUnderlyingJavaClass() { + Type type = new Type("com.structurizr.component.matcher.extendsTypeMatcher.CustomerController"); + + assertFalse(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractController", "Technology").matches(type)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoMatch() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + assertFalse(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractRepository", "Technology").matches(type)); + } + + @Test + void matches_ReturnsTrue_WhenThereIsAMatch() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + assertTrue(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractController", "Technology").matches(type)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java new file mode 100644 index 000000000..4ece7e585 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java @@ -0,0 +1,54 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import org.apache.bcel.classfile.ClassParser; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class ImplementsTypeMatcherTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher(null, "Technology")); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher("", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher(" ", "Technology")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher("com.example.InterfaceName", "Technology").matches(null)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoUnderlyingJavaClass() { + Type type = new Type("com.structurizr.component.matcher.implementsTypeMatcher.CustomerController"); + + assertFalse(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Controller", "Technology").matches(type)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoMatch() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + assertFalse(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Repository", "Technology").matches(type)); + } + + @Test + void matches_ReturnsTrue_WhenThereIsAMatch() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + assertTrue(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Controller", "Technology").matches(type)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java new file mode 100644 index 000000000..967ee7eee --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.annotationTypeMatcher; + +public @interface Controller { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java new file mode 100644 index 000000000..a9a6f3d1c --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java @@ -0,0 +1,5 @@ +package com.structurizr.component.matcher.annotationTypeMatcher; + +@Controller +public class CustomerController { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Repository.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Repository.java new file mode 100644 index 000000000..09a13b504 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Repository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.annotationTypeMatcher; + +public @interface Repository { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractController.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractController.java new file mode 100644 index 000000000..b70b98848 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractController.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.extendsTypeMatcher; + +abstract class AbstractController { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractRepository.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractRepository.java new file mode 100644 index 000000000..467507cfc --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/AbstractRepository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.extendsTypeMatcher; + +abstract class AbstractRepository { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.java new file mode 100644 index 000000000..6edec8bd5 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.extendsTypeMatcher; + +class CustomerController extends AbstractController { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Controller.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Controller.java new file mode 100644 index 000000000..a616ef84c --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Controller.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.implementsTypeMatcher; + +interface Controller { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.java new file mode 100644 index 000000000..d6c47e64f --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.implementsTypeMatcher; + +class CustomerController implements Controller { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Repository.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Repository.java new file mode 100644 index 000000000..ba17b8d88 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/implementsTypeMatcher/Repository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.matcher.implementsTypeMatcher; + +interface Repository { +} From e8ba1f3a85831193f3db99cb97951ac793d04246 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 09:43:59 +0100 Subject: [PATCH 234/418] More tests. --- .../provider/ClassDirectoryTypeProvider.java | 18 +++++++-- .../provider/ClassJarFileTypeProvider.java | 4 +- .../provider/SourceDirectoryTypeProvider.java | 32 +++++++++++---- .../ClassDirectoryTypeProviderTests.java | 39 +++++++++++++++++++ .../SourceDirectoryTypeProviderTests.java | 39 +++++++++++++++++++ 5 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java index a4c5b8339..1c18fb9d5 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java @@ -8,7 +8,7 @@ import java.io.File; import java.io.IOException; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; /** @@ -22,11 +22,23 @@ public final class ClassDirectoryTypeProvider implements TypeProvider { private final File directory; public ClassDirectoryTypeProvider(File directory) { + if (directory == null) { + throw new IllegalArgumentException("A directory must be supplied"); + } + + if (!directory.exists()) { + throw new IllegalArgumentException(directory.getAbsolutePath() + " does not exist"); + } + + if (!directory.isDirectory()) { + throw new IllegalArgumentException(directory.getAbsolutePath() + " is not a directory"); + } + this.directory = directory; } public Set getTypes() { - Set types = new HashSet<>(); + Set types = new LinkedHashSet<>(); Set files = findClassFiles(directory); for (File file : files) { @@ -43,7 +55,7 @@ public Set getTypes() { } private Set findClassFiles(File path) { - Set classFiles = new HashSet<>(); + Set classFiles = new LinkedHashSet<>(); if (path.isDirectory()) { File[] files = path.listFiles(); if (files != null) { diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java index 71951c04a..10f00de7e 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java @@ -9,7 +9,7 @@ import java.io.File; import java.io.IOException; import java.util.Enumeration; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import java.util.jar.JarEntry; @@ -28,7 +28,7 @@ public ClassJarFileTypeProvider(File file) { } public Set getTypes() { - Set types = new HashSet<>(); + Set types = new LinkedHashSet<>(); java.util.jar.JarFile jar = null; try { jar = new java.util.jar.JarFile(jarFile); diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java index 808006c48..f8d8db07f 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -13,7 +13,7 @@ import java.io.File; import java.io.IOException; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; /** @@ -25,21 +25,37 @@ public final class SourceDirectoryTypeProvider implements TypeProvider { private static final String JAVA_FILE_EXTENSION = ".java"; private static final int DEFAULT_DESCRIPTION_LENGTH = 60; - private final Set types = new HashSet<>(); + private final File directory; + private final int maximumDescriptionLength; + private final Set types = new LinkedHashSet<>(); - public SourceDirectoryTypeProvider(File path) { - this(path, DEFAULT_DESCRIPTION_LENGTH); + public SourceDirectoryTypeProvider(File directory) { + this(directory, DEFAULT_DESCRIPTION_LENGTH); } - public SourceDirectoryTypeProvider(File path, int maximumDescriptionLength) { - StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); + public SourceDirectoryTypeProvider(File directory, int maximumDescriptionLength) { + if (directory == null) { + throw new IllegalArgumentException("A directory must be supplied"); + } + + if (!directory.exists()) { + throw new IllegalArgumentException(directory.getAbsolutePath() + " does not exist"); + } - parse(path, maximumDescriptionLength); + if (!directory.isDirectory()) { + throw new IllegalArgumentException(directory.getAbsolutePath() + " is not a directory"); + } + + this.directory = directory; + this.maximumDescriptionLength = maximumDescriptionLength; + StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); } @Override public Set getTypes() { - return new HashSet<>(types); + parse(directory, maximumDescriptionLength); + + return new LinkedHashSet<>(types); } private void parse(File path, int maximumDescriptionLength) { diff --git a/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java new file mode 100644 index 000000000..60d90a8cd --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java @@ -0,0 +1,39 @@ +package com.structurizr.component.provider; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class ClassDirectoryTypeProviderTests { + + private static final File classes = new File("build/classes/java/test"); + + @Test + void construction_ThrowsAnException_WhenPassedANullDirectory() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ClassDirectoryTypeProvider(null)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAPathThatDoesNotExist() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ClassDirectoryTypeProvider(new File(classes, "com/example"))); + } + + @Test + void construction_ThrowsAnException_WhenPassedAFile() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ClassDirectoryTypeProvider(new File(classes, "com/structurizr/component/provider/ClassDirectoryTypeProviderTests.class"))); + } + + @Test + void getTypes() { + TypeProvider typeProvider = new ClassDirectoryTypeProvider(classes); + Set types = typeProvider.getTypes(); + + assertTrue(types.size() > 0); + assertNotNull(types.stream().filter(t -> t.getFullyQualifiedName().equals("com.structurizr.component.provider.ClassDirectoryTypeProviderTests"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java new file mode 100644 index 000000000..6230ec6c6 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java @@ -0,0 +1,39 @@ +package com.structurizr.component.provider; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class SourceDirectoryTypeProviderTests { + + private static final File classes = new File("src/main/java"); + + @Test + void construction_ThrowsAnException_WhenPassedANullDirectory() { + assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(null)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAPathThatDoesNotExist() { + assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(new File(classes, "com/example"))); + } + + @Test + void construction_ThrowsAnException_WhenPassedAFile() { + assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(new File(classes, "com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java"))); + } + + @Test + void getTypes() { + TypeProvider typeProvider = new SourceDirectoryTypeProvider(classes); + Set types = typeProvider.getTypes(); + + assertTrue(types.size() > 0); + assertNotNull(types.stream().filter(t -> t.getFullyQualifiedName().equals("com.structurizr.component.provider.SourceDirectoryTypeProviderTests"))); + } + +} \ No newline at end of file From 1a003d85f02f614222f3119f4e64fe9df954656d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 09:59:59 +0100 Subject: [PATCH 235/418] More tests. --- .../java/com/structurizr/component/Type.java | 7 ++-- ... => ExcludeAbstractClassesTypeFilter.java} | 4 +-- .../filter/ExcludeTypesByRegexFilter.java | 5 +++ .../filter/IncludeTypesByRegexFilter.java | 5 +++ .../component/matcher/ExtendsTypeMatcher.java | 3 +- .../matcher/ImplementsTypeMatcher.java | 3 +- .../matcher/NameSuffixTypeMatcher.java | 3 +- .../filter/DefaultTypeFilterTests.java | 15 +++++++++ .../ExcludeAbstractClassesFilterTests.java | 31 ++++++++++++++++++ .../ExcludeTypesByRegexFilterTests.java | 32 +++++++++++++++++++ .../IncludeTypesByRegexFilterTests.java | 32 +++++++++++++++++++ 11 files changed, 131 insertions(+), 9 deletions(-) rename structurizr-component/src/main/java/com/structurizr/component/filter/{ExcludeAbstractClassTypeFilter.java => ExcludeAbstractClassesTypeFilter.java} (72%) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/filter/DefaultTypeFilterTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeAbstractClassesFilterTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index 0f0eed870..3c9fafe01 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -3,7 +3,6 @@ import com.structurizr.util.StringUtils; import org.apache.bcel.classfile.JavaClass; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; @@ -11,7 +10,7 @@ /** * Represents a Java type (e.g. class or interface) - it's a wrapper around a BCEL JavaClass. */ -public final class Type { +public class Type { private final JavaClass javaClass; private final String fullyQualifiedName; @@ -77,8 +76,8 @@ public Set getDependencies() { return new LinkedHashSet<>(dependencies); } - public boolean isAbstract() { - return javaClass.isAbstract(); + public boolean isAbstractClass() { + return javaClass.isAbstract() && javaClass.isClass(); } @Override diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassesTypeFilter.java similarity index 72% rename from structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java rename to structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassesTypeFilter.java index dad267ef1..2a2d979c9 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassTypeFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassesTypeFilter.java @@ -5,10 +5,10 @@ /** * A type filter that excludes abstract types. */ -public class ExcludeAbstractClassTypeFilter implements TypeFilter { +public class ExcludeAbstractClassesTypeFilter implements TypeFilter { public boolean accept(Type type) { - return !type.isAbstract(); + return !type.isAbstractClass(); } @Override diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java index 0f697b6e8..52e7d2843 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java @@ -1,6 +1,7 @@ package com.structurizr.component.filter; import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; /** * A type filter that excludes by matching a regex against the fully qualified type name. @@ -10,6 +11,10 @@ public class ExcludeTypesByRegexFilter implements TypeFilter { private final String regex; public ExcludeTypesByRegexFilter(String regex) { + if (StringUtils.isNullOrEmpty(regex)) { + throw new IllegalArgumentException("A regex must be supplied"); + } + this.regex = regex; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java index dc76b1f41..3de92d287 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java @@ -1,6 +1,7 @@ package com.structurizr.component.filter; import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; /** * A type filter that includes by matching a regex against the fully qualified type name. @@ -10,6 +11,10 @@ public class IncludeTypesByRegexFilter implements TypeFilter { private final String regex; public IncludeTypesByRegexFilter(String regex) { + if (StringUtils.isNullOrEmpty(regex)) { + throw new IllegalArgumentException("A regex must be supplied"); + } + this.regex = regex; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java index 103d18b54..043ca3cec 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -1,6 +1,7 @@ package com.structurizr.component.matcher; import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; import org.apache.bcel.classfile.JavaClass; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -21,7 +22,7 @@ public class ExtendsTypeMatcher extends AbstractTypeMatcher { public ExtendsTypeMatcher(String className, String technology) { super(technology); - if (className == null || className.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(className)) { throw new IllegalArgumentException("A fully qualified class name must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java index 54aecdcb7..1deb3b6ff 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -1,6 +1,7 @@ package com.structurizr.component.matcher; import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; import org.apache.bcel.classfile.JavaClass; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -19,7 +20,7 @@ public class ImplementsTypeMatcher extends AbstractTypeMatcher { public ImplementsTypeMatcher(String interfaceName, String technology) { super(technology); - if (interfaceName == null || interfaceName.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(interfaceName)) { throw new IllegalArgumentException("A fully qualified interface name must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java index 01f27091a..078a1a511 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java @@ -1,6 +1,7 @@ package com.structurizr.component.matcher; import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; /** * Matches types where the name of the type ends with the specified suffix. @@ -12,7 +13,7 @@ public class NameSuffixTypeMatcher extends AbstractTypeMatcher { public NameSuffixTypeMatcher(String suffix, String technology) { super(technology); - if (suffix == null || suffix.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(suffix)) { throw new IllegalArgumentException("A suffix must be supplied"); } diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/DefaultTypeFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/DefaultTypeFilterTests.java new file mode 100644 index 000000000..6352fb213 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/DefaultTypeFilterTests.java @@ -0,0 +1,15 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultTypeFilterTests { + + @Test + void filter_ReturnsTrue() { + assertTrue(new DefaultTypeFilter().accept(new Type("com.example.Class"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeAbstractClassesFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeAbstractClassesFilterTests.java new file mode 100644 index 000000000..1b3b8c22f --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeAbstractClassesFilterTests.java @@ -0,0 +1,31 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ExcludeAbstractClassesFilterTests { + + @Test + void filter_ReturnsTrue_WhenTheTypeIsNotAbstract() { + assertTrue(new ExcludeAbstractClassesTypeFilter().accept(new Type("com.example.Class") { + @Override + public boolean isAbstractClass() { + return false; + } + })); + } + + @Test + void filter_ReturnsFalse_WhenTheTypeIsAbstract() { + assertFalse(new ExcludeAbstractClassesTypeFilter().accept(new Type("com.example.Class") { + @Override + public boolean isAbstractClass() { + return true; + } + })); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java new file mode 100644 index 000000000..4a0b459fa --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java @@ -0,0 +1,32 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExcludeTypesByRegexFilterTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullSuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter(null)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter(" ")); + } + + + @Test + void filter_ReturnsTrue_WhenTheTypeDoesNotMatchRegex() { + assertTrue(new ExcludeTypesByRegexFilter(".*Utils").accept(new Type("com.example.CustomerComponent"))); + } + + @Test + void filter_ReturnsFalse_WhenTheTypeMatchesRegex() { + assertFalse(new ExcludeTypesByRegexFilter(".*Utils").accept(new Type("com.example.DateUtils"))); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java new file mode 100644 index 000000000..25b223c54 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java @@ -0,0 +1,32 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class IncludeTypesByRegexFilterTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullSuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter(null)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter(" ")); + } + + + @Test + void filter_ReturnsFalse_WhenTheTypeDoesNotMatchRegex() { + assertFalse(new IncludeTypesByRegexFilter(".*Component").accept(new Type("com.example.DateUtils"))); + } + + @Test + void filter_ReturnsTrue_WhenTheTypeMatchesRegex() { + assertTrue(new IncludeTypesByRegexFilter(".*Component").accept(new Type("com.example.CustomerComponent"))); + } + +} \ No newline at end of file From 01d169e09207ad8cabf2a21ca2d59cb77021079b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 10:06:26 +0100 Subject: [PATCH 236/418] More tests. --- ...InPackageSupportingTypesStrategyTests.java | 2 -- ...ncedTypesSupportingTypesStrategyTests.java | 2 -- ...InPackageSupportingTypesStrategyTests.java | 35 ++++++++++++++++++ ...erPackageSupportingTypesStrategyTests.java | 36 +++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategyTests.java diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java index 761b08d84..5562ed190 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java @@ -19,8 +19,6 @@ void findSupportingTypes() { type.addDependency(dependency1); type.addDependency(dependency2); - type.addDependency(new Type("com.example.util.SomeUtils")); - Set supportingTypes = new AllReferencedTypesInPackageSupportingTypesStrategy().findSupportingTypes(type, null); assertEquals(1, supportingTypes.size()); assertTrue(supportingTypes.contains(dependency1)); diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java index 80f8a15f9..6f3122bcc 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java @@ -19,8 +19,6 @@ void findSupportingTypes() { type.addDependency(dependency1); type.addDependency(dependency2); - type.addDependency(new Type("com.example.util.SomeUtils")); - Set supportingTypes = new AllReferencedTypesSupportingTypesStrategy().findSupportingTypes(type, null); assertEquals(2, supportingTypes.size()); assertTrue(supportingTypes.contains(dependency1)); diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategyTests.java new file mode 100644 index 000000000..cf36e4c00 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesInPackageSupportingTypesStrategyTests.java @@ -0,0 +1,35 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AllTypesInPackageSupportingTypesStrategyTests { + + @Test + void findSupportingTypes() { + Type type = new Type("com.example.a.package-info"); + + Type dependency1 = new Type("com.example.a.AImpl"); + Type dependency2 = new Type("com.example.a.internal.AInternal"); + Type dependency3 = new Type("com.example.util.SomeUtils"); + type.addDependency(dependency1); + type.addDependency(dependency2); + type.addDependency(dependency3); + + TypeRepository typeRepository = new TypeRepository(); + typeRepository.add(dependency1); + typeRepository.add(dependency2); + typeRepository.add(dependency3); + + Set supportingTypes = new AllTypesInPackageSupportingTypesStrategy().findSupportingTypes(type, typeRepository); + assertEquals(1, supportingTypes.size()); + assertTrue(supportingTypes.contains(dependency1)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategyTests.java new file mode 100644 index 000000000..7cf97d4c3 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllTypesUnderPackageSupportingTypesStrategyTests.java @@ -0,0 +1,36 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AllTypesUnderPackageSupportingTypesStrategyTests { + + @Test + void findSupportingTypes() { + Type type = new Type("com.example.a.package-info"); + + Type dependency1 = new Type("com.example.a.AImpl"); + Type dependency2 = new Type("com.example.a.internal.AInternal"); + Type dependency3 = new Type("com.example.util.SomeUtils"); + type.addDependency(dependency1); + type.addDependency(dependency2); + type.addDependency(dependency3); + + TypeRepository typeRepository = new TypeRepository(); + typeRepository.add(dependency1); + typeRepository.add(dependency2); + typeRepository.add(dependency3); + + Set supportingTypes = new AllTypesUnderPackageSupportingTypesStrategy().findSupportingTypes(type, typeRepository); + assertEquals(2, supportingTypes.size()); + assertTrue(supportingTypes.contains(dependency1)); + assertTrue(supportingTypes.contains(dependency2)); + } + +} \ No newline at end of file From 6fbaa6395ca02756b374df089b03dfdf27f4241c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 10:17:47 +0100 Subject: [PATCH 237/418] More tests. --- .../component/ComponentFinder.java | 4 -- .../component/ComponentFinderBuilder.java | 12 +++++ .../ComponentFinderBuilderTests.java | 47 +++++++++++++++++++ .../ComponentFinderStrategyBuilderTests.java | 14 ++++++ .../SourceDirectoryTypeProviderTests.java | 8 ++-- 5 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/ComponentFinderBuilderTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index ef6edac08..5234800fc 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -29,10 +29,6 @@ public final class ComponentFinder { private final List componentFinderStrategies = new ArrayList<>(); ComponentFinder(Container container, Collection typeProviders, List componentFinderStrategies) { - if (container == null) { - throw new IllegalArgumentException("A container must be specified."); - } - this.container = container; this.componentFinderStrategies.addAll(componentFinderStrategies); diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java index a4f8f9535..86cd0a843 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java @@ -64,6 +64,18 @@ public ComponentFinderBuilder withStrategy(ComponentFinderStrategy componentFind } public ComponentFinder build() { + if (container == null) { + throw new RuntimeException("A container must be specified"); + } + + if (typeProviders.isEmpty()) { + throw new RuntimeException("One or more type providers must be configured"); + } + + if (componentFinderStrategies.isEmpty()) { + throw new RuntimeException("One or more component finder strategies must be configured"); + } + return new ComponentFinder(container, typeProviders, componentFinderStrategies); } diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderBuilderTests.java new file mode 100644 index 000000000..13206859a --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderBuilderTests.java @@ -0,0 +1,47 @@ +package com.structurizr.component; + +import com.structurizr.Workspace; +import com.structurizr.model.Container; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class ComponentFinderBuilderTests { + + @Test + void build_ThrowsAnException_WhenAContainerHasNotBeenSpecified() { + try { + new ComponentFinderBuilder().build(); + fail(); + } catch (RuntimeException e) { + assertEquals("A container must be specified", e.getMessage()); + } + } + + @Test + void build_ThrowsAnException_WhenATypeProviderHasNotBeenConfigured() { + Container container = new Workspace("Name", "Description").getModel().addSoftwareSystem("Software System").addContainer("Container"); + try { + new ComponentFinderBuilder().forContainer(container).build(); + fail(); + } catch (RuntimeException e) { + assertEquals("One or more type providers must be configured", e.getMessage()); + } + } + + @Test + void build_ThrowsAnException_WhenAComponentFinderStrategyHasNotBeenConfigured() { + Container container = new Workspace("Name", "Description").getModel().addSoftwareSystem("Software System").addContainer("Container"); + File sources = new File("src/main/java"); + try { + new ComponentFinderBuilder().forContainer(container).fromSource(sources).build(); + fail(); + } catch (RuntimeException e) { + assertEquals("One or more component finder strategies must be configured", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java new file mode 100644 index 000000000..b08a60b2a --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -0,0 +1,14 @@ +package com.structurizr.component; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +public class ComponentFinderStrategyBuilderTests { + + @Test + void build_ThrowsAnException_WhenATypeMatcherHasNotBeenConfgured() { + assertThrowsExactly(RuntimeException.class, () -> new ComponentFinderStrategyBuilder().build()); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java index 6230ec6c6..564258a54 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java @@ -10,7 +10,7 @@ public class SourceDirectoryTypeProviderTests { - private static final File classes = new File("src/main/java"); + private static final File sources = new File("src/main/java"); @Test void construction_ThrowsAnException_WhenPassedANullDirectory() { @@ -19,17 +19,17 @@ void construction_ThrowsAnException_WhenPassedANullDirectory() { @Test void construction_ThrowsAnException_WhenPassedAPathThatDoesNotExist() { - assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(new File(classes, "com/example"))); + assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(new File(sources, "com/example"))); } @Test void construction_ThrowsAnException_WhenPassedAFile() { - assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(new File(classes, "com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java"))); + assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(new File(sources, "com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java"))); } @Test void getTypes() { - TypeProvider typeProvider = new SourceDirectoryTypeProvider(classes); + TypeProvider typeProvider = new SourceDirectoryTypeProvider(sources); Set types = typeProvider.getTypes(); assertTrue(types.size() > 0); From 33c13a9a2b3f6177f67e0414f00269acf56ae7ad Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 10:19:31 +0100 Subject: [PATCH 238/418] Typo. --- .../component/ComponentFinderStrategyBuilderTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java index b08a60b2a..6b578022f 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -7,7 +7,7 @@ public class ComponentFinderStrategyBuilderTests { @Test - void build_ThrowsAnException_WhenATypeMatcherHasNotBeenConfgured() { + void build_ThrowsAnException_WhenATypeMatcherHasNotBeenConfigured() { assertThrowsExactly(RuntimeException.class, () -> new ComponentFinderStrategyBuilder().build()); } From ed4ee43cba5fbf4101670b0f06d980d2a6d231ce Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 11:08:22 +0100 Subject: [PATCH 239/418] Adds "tag" as an alternative to "tags". --- .../src/main/java/com/structurizr/dsl/StructurizrDslParser.java | 2 +- .../src/main/java/com/structurizr/dsl/StructurizrDslTokens.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 50a2fc62f..cffcebef4 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -427,7 +427,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr DeploymentNode deploymentNode = getContext(DeploymentNodeDslContext.class).getDeploymentNode(); startContext(new DeploymentNodeDslContext(deploymentNode, group)); registerIdentifier(identifier, group); - } else if (TAGS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + } else if ((TAGS_TOKEN.equalsIgnoreCase(firstToken) || TAG_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { new ModelItemParser().parseTags(getContext(ModelItemDslContext.class), tokens); } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && getContext(ModelItemDslContext.class).getModelItem() instanceof Element && !getContext(ModelItemDslContext.class).hasGroup()) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 35ace2259..c1301def4 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -19,6 +19,7 @@ class StructurizrDslTokens { static final String TECHNOLOGY_TOKEN = "technology"; static final String INSTANCES_TOKEN = "instances"; static final String TAGS_TOKEN = "tags"; + static final String TAG_TOKEN = "tag"; static final String URL_TOKEN = "url"; static final String PROPERTIES_TOKEN = "properties"; static final String PERSPECTIVES_TOKEN = "perspectives"; From 52ec9be442477af82d83281184aa54e88669b261 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 11:09:14 +0100 Subject: [PATCH 240/418] Adds a way to add constants to the parser programmatically. --- .../structurizr/dsl/StructurizrDslParser.java | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index cffcebef4..cca16737a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -889,11 +889,11 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (CONST_TOKEN.equalsIgnoreCase(firstToken)) { NameValuePair nameValuePair = new NameValueParser().parseConstant(tokens); - - if (constantsAndVariables.containsKey(nameValuePair.getName())) { - throw new StructurizrDslParserException("A constant/variable \"" + nameValuePair.getName() + "\" already exists"); + try { + addConstant(nameValuePair); + } catch (IllegalArgumentException e) { + throw new StructurizrDslParserException(e.getMessage()); } - constantsAndVariables.put(nameValuePair.getName(), nameValuePair); } else if (VAR_TOKEN.equalsIgnoreCase(firstToken)) { NameValuePair nameValuePair = new NameValueParser().parseVariable(tokens); @@ -1062,8 +1062,39 @@ private void registerIdentifier(String identifier, Relationship relationship) { relationship.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(relationship)); } + /** + * Gets the named constant. + * + * @param name the name of the constant + * @return the value, or an empty string if the named constant doesn't exist + */ public String getConstant(String name) { - return constantsAndVariables.get(name).getValue(); + NameValuePair nameValuePair = constantsAndVariables.get(name); + if (nameValuePair != null) { + return nameValuePair.getValue(); + } else { + return ""; + } + } + + /** + * Adds a constant to the parser. + * @param name the name of the constant + * @param value the value of the constant + */ + public void addConstant(String name, String value) { + if (StringUtils.isNullOrEmpty(name)) { + throw new IllegalArgumentException("A constant name must be specified"); + } + + addConstant(new NameValuePair(name, value)); + } + + private void addConstant(NameValuePair nameValuePair) { + if (constantsAndVariables.containsKey(nameValuePair.getName())) { + throw new IllegalArgumentException("A constant/variable \"" + nameValuePair.getName() + "\" already exists"); + } + constantsAndVariables.put(nameValuePair.getName(), nameValuePair); } private boolean inContext(Class clazz) { From e30f5e84636ed0e3bc187e8f173588cb709bd276 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 11:41:13 +0100 Subject: [PATCH 241/418] Adds an `!elements` keyword that can be used to find a set of elements via an expression. --- changelog.md | 1 + .../structurizr/dsl/ElementsDslContext.java | 33 +++++++++++++ .../com/structurizr/dsl/ElementsParser.java | 28 +++++++++++ .../structurizr/dsl/ModelItemsDslContext.java | 26 +++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 7 +++ .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../structurizr/dsl/ElementsParserTests.java | 46 +++++++++++++++++++ .../src/test/resources/dsl/test.dsl | 3 ++ 8 files changed, 145 insertions(+) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java diff --git a/changelog.md b/changelog.md index b5076e453..0d0498992 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. - structurizr-dsl: Adds support for element technology expressions (e.g. "element.technology==Java"). +- structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression. ## 2.2.0 (2nd July 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java new file mode 100644 index 000000000..d0aec535b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java @@ -0,0 +1,33 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.ModelItem; + +import java.util.Set; +import java.util.stream.Collectors; + +class ElementsDslContext extends ModelItemsDslContext { + + private final Set elements; + + ElementsDslContext(DslContext parentDslContext, Set elements) { + super(parentDslContext); + + this.elements = elements; + } + + Set getElements() { + return elements; + } + + @Override + Set getModelItems() { + return elements.stream().map(e -> (ModelItem)e).collect(Collectors.toSet()); + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java new file mode 100644 index 000000000..2bfccf11b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java @@ -0,0 +1,28 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.ModelItem; + +import java.util.Set; +import java.util.stream.Collectors; + +final class ElementsParser extends AbstractParser { + + private static final String GRAMMAR = "!elements "; + + private final static int EXPRESSION_INDEX = 1; + + Set parse(DslContext context, Tokens tokens) { + // !elements + + if (tokens.hasMoreThan(EXPRESSION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + String expression = tokens.get(1); + Set modelItems = new ExpressionParser().parseExpression(expression, context); + + return modelItems.stream().filter(mi -> mi instanceof Element).map(mi -> (Element)mi).collect(Collectors.toSet()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java new file mode 100644 index 000000000..1caf3055a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java @@ -0,0 +1,26 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +import java.util.Set; + +abstract class ModelItemsDslContext extends DslContext { + + private final DslContext parentDslContext; + + ModelItemsDslContext(DslContext parentDslContext) { + this.parentDslContext = parentDslContext; + } + + DslContext getParentDslContext() { + return parentDslContext; + } + + abstract Set getModelItems(); + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index cca16737a..d34d77bae 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -348,6 +348,13 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } } + } else if (ELEMENTS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ModelItemDslContext.class))) { + Set elements = new ElementsParser().parse(getContext(), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new ElementsDslContext(getContext(), elements)); + } + } else if (CUSTOM_ELEMENT_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { CustomElement customElement = new CustomElementParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index c1301def4..ab7aff0b1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -106,6 +106,7 @@ class StructurizrDslTokens { static final String IDENTIFIERS_TOKEN = "!identifiers"; static final String IMPLIED_RELATIONSHIPS_TOKEN = "!impliedRelationships"; static final String REF_TOKEN = "!ref"; + static final String ELEMENTS_TOKEN = "!elements"; static final String EXTEND_TOKEN = "!extend"; static final String PLUGIN_TOKEN = "!plugin"; static final String SCRIPT_TOKEN = "!script"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java new file mode 100644 index 000000000..3af081588 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java @@ -0,0 +1,46 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class ElementsParserTests extends AbstractTests { + + private final ElementsParser parser = new ElementsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!elements", "expression", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !elements ", e.getMessage()); + } + } + + @Test + void test_parse_FindsElementsByExpression() { + Container application = model.addSoftwareSystem("Software System").addContainer("Application"); + Component componentA = application.addComponent("A"); + Component componentB = application.addComponent("B"); + Component componentC = application.addComponent("C"); + + ModelItemDslContext context = new ContainerDslContext(application); + context.setWorkspace(workspace); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("application", application); + context.setIdentifierRegister(register); + + Set elements = parser.parse(context, tokens("!elements", "element.parent==application")); + assertTrue(elements.contains(componentA)); + assertTrue(elements.contains(componentB)); + assertTrue(elements.contains(componentC)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index b33be93e0..d71238d07 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -66,6 +66,9 @@ workspace "Name" "Description" { perspectives { "Security" "A description..." } + + !elements "element.parent==webApplication" { + } } url "https://structurizr.com" From 987e0f26985447c488b368542580a20a0b7def3a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 11:53:40 +0100 Subject: [PATCH 242/418] Adds a way to set tags for a set of elements found via `!elements`. --- .../com/structurizr/dsl/ModelItemsParser.java | 24 ++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 3 ++ .../dsl/ModelItemsParserTests.java | 48 +++++++++++++++++++ .../src/test/resources/dsl/test.dsl | 3 +- 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemsParserTests.java diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java new file mode 100644 index 000000000..c34ceb33d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java @@ -0,0 +1,24 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +final class ModelItemsParser extends AbstractParser { + + private final static int TAGS_INDEX = 1; + + void parseTags(ModelItemsDslContext context, Tokens tokens) { + // tags [tags] + if (!tokens.includes(TAGS_INDEX)) { + throw new RuntimeException("Expected: tags [tags]"); + } + + for (int i = TAGS_INDEX; i < tokens.size(); i++) { + String tags = tokens.get(i); + + for (ModelItem modelItem : context.getModelItems()) { + modelItem.addTags(tags.split(",")); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index d34d77bae..a3bbac50c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -437,6 +437,9 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if ((TAGS_TOKEN.equalsIgnoreCase(firstToken) || TAG_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { new ModelItemParser().parseTags(getContext(ModelItemDslContext.class), tokens); + } else if ((TAGS_TOKEN.equalsIgnoreCase(firstToken) || TAG_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ModelItemsDslContext.class)) { + new ModelItemsParser().parseTags(getContext(ModelItemsDslContext.class), tokens); + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && getContext(ModelItemDslContext.class).getModelItem() instanceof Element && !getContext(ModelItemDslContext.class).hasGroup()) { new ModelItemParser().parseDescription(getContext(ModelItemDslContext.class), tokens); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemsParserTests.java new file mode 100644 index 000000000..004d345cf --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemsParserTests.java @@ -0,0 +1,48 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class ModelItemsParserTests extends AbstractTests { + + private final ModelItemsParser parser = new ModelItemsParser(); + + @Test + void test_parseTags_ThrowsAnException_WhenNoTagsAreSpecified() { + try { + parser.parseTags(null, tokens("tags")); + fail(); + } catch (Exception e) { + assertEquals("Expected: tags [tags]", e.getMessage()); + } + } + + @Test + void test_parseTags_AddsTheTags_WhenTagsAreSpecified() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + ElementsDslContext context = new ElementsDslContext(null, Set.of(a, b, c)); + + parser.parseTags(context, tokens("tags", "Tag 1")); + for (Element element : context.getElements()) { + assertEquals(3, element.getTagsAsSet().size()); + assertTrue(element.getTagsAsSet().contains("Tag 1")); + } + + parser.parseTags(context, tokens("tags", "Tag 1, Tag 2, Tag 3")); + for (Element element : context.getElements()) { + assertEquals(5, element.getTagsAsSet().size()); + assertTrue(element.getTagsAsSet().contains("Tag 1")); + assertTrue(element.getTagsAsSet().contains("Tag 2")); + assertTrue(element.getTagsAsSet().contains("Tag 3")); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index d71238d07..994ea2d39 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -67,7 +67,8 @@ workspace "Name" "Description" { "Security" "A description..." } - !elements "element.parent==webApplication" { + !elements "element.parent==webApplication && element.technology==Spring MVC Controller" { + tags "Spring MVC Controller" } } From d2ccb98ff656c943307330592f7eb7dae970d20c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 13:34:34 +0100 Subject: [PATCH 243/418] Tidy up of how groups are handled. --- .../dsl/ContainerInstanceDslContext.java | 8 +++++- .../structurizr/dsl/CustomElementParser.java | 2 +- .../dsl/DeploymentEnvironmentDslContext.java | 17 +++++++++-- .../structurizr/dsl/ElementDslContext.java | 9 ++++++ .../com/structurizr/dsl/ElementGroup.java | 14 ++-------- .../java/com/structurizr/dsl/GroupParser.java | 14 ++++++++-- .../structurizr/dsl/GroupableDslContext.java | 20 ++----------- .../dsl/GroupableElementDslContext.java | 18 ++++++++++-- .../dsl/InfrastructureNodeDslContext.java | 10 +++++-- .../com/structurizr/dsl/ModelDslContext.java | 17 +++++++++-- .../structurizr/dsl/ModelItemDslContext.java | 6 +--- .../com/structurizr/dsl/ModelItemParser.java | 6 ++-- .../com/structurizr/dsl/PersonParser.java | 2 +- .../dsl/SoftwareSystemInstanceDslContext.java | 8 +++++- .../structurizr/dsl/SoftwareSystemParser.java | 2 +- ...ticStructureElementInstanceDslContext.java | 2 +- .../structurizr/dsl/StructurizrDslParser.java | 28 ++++++++++++------- .../com/structurizr/dsl/GroupParserTests.java | 4 +-- .../structurizr/dsl/ModelItemParserTests.java | 6 ++-- 19 files changed, 121 insertions(+), 72 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementDslContext.java diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java index 4268f656b..a8f1bc8bd 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java @@ -1,12 +1,13 @@ package com.structurizr.dsl; import com.structurizr.model.ContainerInstance; +import com.structurizr.model.Element; import com.structurizr.model.ModelItem; import com.structurizr.model.StaticStructureElementInstance; final class ContainerInstanceDslContext extends StaticStructureElementInstanceDslContext { - private ContainerInstance containerInstance; + private final ContainerInstance containerInstance; ContainerInstanceDslContext(ContainerInstance containerInstance) { this.containerInstance = containerInstance; @@ -21,6 +22,11 @@ ModelItem getModelItem() { return getContainerInstance(); } + @Override + Element getElement() { + return getContainerInstance(); + } + @Override StaticStructureElementInstance getElementInstance() { return getContainerInstance(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java index bef0a7707..493b573b9 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java @@ -13,7 +13,7 @@ final class CustomElementParser extends AbstractParser { private final static int DESCRIPTION_INDEX = 3; private final static int TAGS_INDEX = 4; - CustomElement parse(GroupableDslContext context, Tokens tokens) { + CustomElement parse(ModelDslContext context, Tokens tokens) { // element [metadata] [description] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java index c35027242..ceb1c30b2 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java @@ -1,23 +1,34 @@ package com.structurizr.dsl; -final class DeploymentEnvironmentDslContext extends GroupableDslContext { +final class DeploymentEnvironmentDslContext extends DslContext implements GroupableDslContext { private final String environment; + private final ElementGroup group; DeploymentEnvironmentDslContext(String environment) { - super(null); this.environment = environment; + this.group = null; } DeploymentEnvironmentDslContext(String environment, ElementGroup group) { - super(group); this.environment = environment; + this.group = group; } String getEnvironment() { return environment; } + @Override + public boolean hasGroup() { + return group != null; + } + + @Override + public ElementGroup getGroup() { + return group; + } + @Override protected String[] getPermittedTokens() { return new String[] { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementDslContext.java new file mode 100644 index 000000000..11a0594f9 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementDslContext.java @@ -0,0 +1,9 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; + +abstract class ElementDslContext extends ModelItemDslContext { + + abstract Element getElement(); + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java index 751dc1b0f..7c5895cef 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java @@ -9,28 +9,18 @@ class ElementGroup extends Element { - private static final String STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator"; - private Element parent; private final ElementGroup parentGroup; private final String name; private final Set elements = new HashSet<>(); - ElementGroup(Model model, String name) { - setModel(model); + ElementGroup(String name) { this.name = name; this.parentGroup = null; } - ElementGroup(Model model, String name, ElementGroup parentGroup) { - setModel(model); - String groupSeparator = getModel().getProperties().getOrDefault(STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME, ""); - - if (StringUtils.isNullOrEmpty(groupSeparator)) { - throw new RuntimeException("To use nested groups, please define a model property named " + STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME); - } - + ElementGroup(String name, String groupSeparator, ElementGroup parentGroup) { this.name = parentGroup.getName() + groupSeparator + name; this.parentGroup = parentGroup; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java index 8ff6e0fa9..255f3db9d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java @@ -1,7 +1,11 @@ package com.structurizr.dsl; +import com.structurizr.util.StringUtils; + class GroupParser { + private static final String STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator"; + private static final String GRAMMAR = "group {"; private final static int NAME_INDEX = 1; @@ -24,9 +28,15 @@ ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) { ElementGroup group; if (dslContext.hasGroup()) { - group = new ElementGroup(dslContext.getWorkspace().getModel(), tokens.get(NAME_INDEX), dslContext.getGroup()); + String groupSeparator = ((DslContext)dslContext).getWorkspace().getModel().getProperties().getOrDefault(STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME, ""); + + if (StringUtils.isNullOrEmpty(groupSeparator)) { + throw new RuntimeException("To use nested groups, please define a model property named " + STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME); + } + + group = new ElementGroup(tokens.get(NAME_INDEX), groupSeparator, dslContext.getGroup()); } else { - group = new ElementGroup(dslContext.getWorkspace().getModel(), tokens.get(NAME_INDEX)); + group = new ElementGroup(tokens.get(NAME_INDEX)); } return group; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java index 5ac594434..594ac14b6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java @@ -1,23 +1,9 @@ package com.structurizr.dsl; -abstract class GroupableDslContext extends DslContext { +interface GroupableDslContext { - private ElementGroup group; + boolean hasGroup(); - GroupableDslContext() { - this(null); - } - - GroupableDslContext(ElementGroup group) { - this.group = group; - } - - boolean hasGroup() { - return group != null; - } - - ElementGroup getGroup() { - return group; - } + ElementGroup getGroup(); } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java index a26b58cd6..bc6b337d1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java @@ -2,14 +2,26 @@ import com.structurizr.model.GroupableElement; -abstract class GroupableElementDslContext extends ModelItemDslContext { +abstract class GroupableElementDslContext extends ElementDslContext implements GroupableDslContext { + + private final ElementGroup group; GroupableElementDslContext() { - super(); + this.group = null; } GroupableElementDslContext(ElementGroup group) { - super(group); + this.group = group; + } + + @Override + public boolean hasGroup() { + return group != null; + } + + @Override + public ElementGroup getGroup() { + return group; } abstract GroupableElement getElement(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java index 97487a2a5..bda1154ee 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java @@ -1,11 +1,12 @@ package com.structurizr.dsl; +import com.structurizr.model.GroupableElement; import com.structurizr.model.InfrastructureNode; import com.structurizr.model.ModelItem; -final class InfrastructureNodeDslContext extends ModelItemDslContext { +final class InfrastructureNodeDslContext extends GroupableElementDslContext { - private InfrastructureNode infrastructureNode; + private final InfrastructureNode infrastructureNode; InfrastructureNodeDslContext(InfrastructureNode infrastructureNode) { this.infrastructureNode = infrastructureNode; @@ -20,6 +21,11 @@ ModelItem getModelItem() { return getInfrastructureNode(); } + @Override + GroupableElement getElement() { + return infrastructureNode; + } + @Override protected String[] getPermittedTokens() { return new String[] { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java index 7903af346..795354332 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java @@ -1,13 +1,24 @@ package com.structurizr.dsl; -final class ModelDslContext extends GroupableDslContext { +final class ModelDslContext extends DslContext implements GroupableDslContext { + + private ElementGroup group; ModelDslContext() { - super(null); } ModelDslContext(ElementGroup group) { - super(group); + this.group = group; + } + + @Override + public boolean hasGroup() { + return group != null; + } + + @Override + public ElementGroup getGroup() { + return group; } @Override diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java index 72150de5a..823702ffc 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java @@ -2,16 +2,12 @@ import com.structurizr.model.ModelItem; -abstract class ModelItemDslContext extends GroupableDslContext { +abstract class ModelItemDslContext extends DslContext { ModelItemDslContext() { super(); } - ModelItemDslContext(ElementGroup group) { - super(group); - } - abstract ModelItem getModelItem(); } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java index 823728bf0..f98cb241d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java @@ -1,7 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Element; - final class ModelItemParser extends AbstractParser { private final static int DESCRIPTION_INDEX = 1; @@ -26,7 +24,7 @@ void parseTags(ModelItemDslContext context, Tokens tokens) { } } - void parseDescription(ModelItemDslContext context, Tokens tokens) { + void parseDescription(ElementDslContext context, Tokens tokens) { // description if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { throw new RuntimeException("Too many tokens, expected: description "); @@ -37,7 +35,7 @@ void parseDescription(ModelItemDslContext context, Tokens tokens) { } String description = tokens.get(DESCRIPTION_INDEX); - ((Element)context.getModelItem()).setDescription(description); + context.getElement().setDescription(description); } void parseUrl(ModelItemDslContext context, Tokens tokens) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java index c2103af51..aecc819eb 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -10,7 +10,7 @@ final class PersonParser extends AbstractParser { private final static int DESCRIPTION_INDEX = 2; private final static int TAGS_INDEX = 3; - Person parse(GroupableDslContext context, Tokens tokens) { + Person parse(ModelDslContext context, Tokens tokens) { // person [description] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java index 9da1e6760..607db5485 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java @@ -1,12 +1,13 @@ package com.structurizr.dsl; +import com.structurizr.model.Element; import com.structurizr.model.SoftwareSystemInstance; import com.structurizr.model.ModelItem; import com.structurizr.model.StaticStructureElementInstance; final class SoftwareSystemInstanceDslContext extends StaticStructureElementInstanceDslContext { - private SoftwareSystemInstance softwareSystemInstance; + private final SoftwareSystemInstance softwareSystemInstance; SoftwareSystemInstanceDslContext(SoftwareSystemInstance softwareSystemInstance) { this.softwareSystemInstance = softwareSystemInstance; @@ -21,6 +22,11 @@ ModelItem getModelItem() { return getSoftwareSystemInstance(); } + @Override + Element getElement() { + return getSoftwareSystemInstance(); + } + @Override StaticStructureElementInstance getElementInstance() { return getSoftwareSystemInstance(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java index 46cc0c9c4..243cd6d6c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -10,7 +10,7 @@ final class SoftwareSystemParser extends AbstractParser { private final static int DESCRIPTION_INDEX = 2; private final static int TAGS_INDEX = 3; - SoftwareSystem parse(GroupableDslContext context, Tokens tokens) { + SoftwareSystem parse(ModelDslContext context, Tokens tokens) { // softwareSystem [description] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java index 1a01b973d..7066961c3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureElementInstanceDslContext.java @@ -2,7 +2,7 @@ import com.structurizr.model.StaticStructureElementInstance; -abstract class StaticStructureElementInstanceDslContext extends ModelItemDslContext { +abstract class StaticStructureElementInstanceDslContext extends ElementDslContext { abstract StaticStructureElementInstance getElementInstance(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index a3bbac50c..d1e2cf10c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -356,7 +356,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } } else if (CUSTOM_ELEMENT_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { - CustomElement customElement = new CustomElementParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); + CustomElement customElement = new CustomElementParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { startContext(new CustomElementDslContext(customElement)); @@ -365,7 +365,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr registerIdentifier(identifier, customElement); } else if (PERSON_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { - Person person = new PersonParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); + Person person = new PersonParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { startContext(new PersonDslContext(person)); @@ -374,7 +374,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr registerIdentifier(identifier, person); } else if (SOFTWARE_SYSTEM_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { - SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(GroupableDslContext.class), tokens.withoutContextStartToken()); + SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { startContext(new SoftwareSystemDslContext(softwareSystem)); @@ -434,14 +434,14 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr DeploymentNode deploymentNode = getContext(DeploymentNodeDslContext.class).getDeploymentNode(); startContext(new DeploymentNodeDslContext(deploymentNode, group)); registerIdentifier(identifier, group); - } else if ((TAGS_TOKEN.equalsIgnoreCase(firstToken) || TAG_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + } else if ((TAGS_TOKEN.equalsIgnoreCase(firstToken) || TAG_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { new ModelItemParser().parseTags(getContext(ModelItemDslContext.class), tokens); } else if ((TAGS_TOKEN.equalsIgnoreCase(firstToken) || TAG_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ModelItemsDslContext.class)) { new ModelItemsParser().parseTags(getContext(ModelItemsDslContext.class), tokens); - } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && getContext(ModelItemDslContext.class).getModelItem() instanceof Element && !getContext(ModelItemDslContext.class).hasGroup()) { - new ModelItemParser().parseDescription(getContext(ModelItemDslContext.class), tokens); + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementDslContext.class) && !isGroup(getContext())) { + new ModelItemParser().parseDescription(getContext(ElementDslContext.class), tokens); } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class) && !getContext(ContainerDslContext.class).hasGroup()) { new ContainerParser().parseTechnology(getContext(ContainerDslContext.class), tokens); @@ -458,7 +458,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (INSTANCES_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { new DeploymentNodeParser().parseInstances(getContext(DeploymentNodeDslContext.class), tokens); - } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { new ModelItemParser().parseUrl(getContext(ModelItemDslContext.class), tokens); } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { @@ -467,10 +467,10 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { startContext(new PropertiesDslContext(workspace.getModel())); - } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ConfigurationDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ConfigurationDslContext.class)) { startContext(new PropertiesDslContext(getContext(ConfigurationDslContext.class).getWorkspace())); - } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { startContext(new PropertiesDslContext(getContext(ModelItemDslContext.class).getModelItem())); } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { @@ -491,7 +491,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (inContext(PropertiesDslContext.class)) { new PropertyParser().parse(getContext(PropertiesDslContext.class), tokens); - } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !getContext(ModelItemDslContext.class).hasGroup()) { + } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { startContext(new ModelItemPerspectivesDslContext(getContext(ModelItemDslContext.class).getModelItem())); } else if (inContext(ModelItemPerspectivesDslContext.class)) { @@ -1053,6 +1053,14 @@ private void endContext() throws StructurizrDslParserException { } } + private boolean isGroup(DslContext context) { + if (context instanceof GroupableDslContext) { + return ((GroupableDslContext)context).hasGroup(); + } + + return false; + } + /** * Gets the identifier register in use (this is the mapping of DSL identifiers to elements/relationships). * diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java index 0186144de..ef1af486a 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java @@ -47,7 +47,7 @@ void parse() { @Test void parse_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { - ModelDslContext context = new ModelDslContext(new ElementGroup(workspace.getModel(), "Group 1")); + ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1")); context.setWorkspace(workspace); try { @@ -61,7 +61,7 @@ void parse_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { @Test void parse_NestedGroup() { workspace.getModel().addProperty("structurizr.groupSeparator", "/"); - ModelDslContext context = new ModelDslContext(new ElementGroup(workspace.getModel(), "Group 1")); + ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1")); context.setWorkspace(workspace); ElementGroup group = parser.parse(context, tokens("group", "Group 2", "{")); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java index 379ce8e29..011826924 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java @@ -44,7 +44,7 @@ void test_parseTags_AddsTheTags_WhenTagsAreSpecified() { @Test void test_parseDescription_ThrowsAnException_WhenThereAreTooManyTokens() { try { - ModelItemDslContext context = new SoftwareSystemDslContext(null); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(null); parser.parseDescription(context, tokens("description", "description", "extra")); fail(); } catch (Exception e) { @@ -56,7 +56,7 @@ void test_parseDescription_ThrowsAnException_WhenThereAreTooManyTokens() { void test_parseDescription_ThrowsAnException_WhenNoDescriptionIsSpecified() { try { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); parser.parseDescription(context, tokens("description")); fail(); } catch (Exception e) { @@ -67,7 +67,7 @@ void test_parseDescription_ThrowsAnException_WhenNoDescriptionIsSpecified() { @Test void test_parseDescription_SetsTheDescription_WhenADescriptionIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", ""); - ModelItemDslContext context = new SoftwareSystemDslContext(softwareSystem); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); parser.parseDescription(context, tokens("description", "Description")); assertEquals("Description", softwareSystem.getDescription()); From aeb71a6210d5534f27aac4b41374e67c978c69f5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 14:20:01 +0100 Subject: [PATCH 244/418] Adds the ability to bulk create relationships via the `!elements` keyword. --- .../java/com/structurizr/dsl/DslContext.java | 27 ++++-- .../dsl/ExplicitRelationshipParser.java | 97 +++++++++++++++---- .../dsl/ImplicitRelationshipParser.java | 50 +++++++++- .../structurizr/dsl/StructurizrDslParser.java | 14 ++- .../java/com/structurizr/dsl/DslTests.java | 9 ++ .../dsl/ImplicitRelationshipParserTests.java | 20 ++-- .../test/resources/dsl/bulk-operations.dsl | 59 +++++++++++ 7 files changed, 229 insertions(+), 47 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java index ef529853c..a5d6f02a2 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java @@ -49,18 +49,25 @@ Element getElement(String identifier, Class type) { identifier = identifier.toLowerCase(); if (identifiersRegister.getIdentifierScope() == IdentifierScope.Hierarchical) { - if (this instanceof ModelItemDslContext) { - ModelItemDslContext modelItemDslContext = (ModelItemDslContext)this; - if (modelItemDslContext.getModelItem() instanceof Element) { - Element parent = (Element)modelItemDslContext.getModelItem(); - while (parent != null && element == null) { - String parentIdentifier = identifiersRegister.findIdentifier(parent); + ElementDslContext elementDslContext = null; + if (this instanceof ElementDslContext) { + elementDslContext = (ElementDslContext)this; + } else if (this instanceof ElementsDslContext) { + ElementsDslContext elementsDslContext = (ElementsDslContext)this; + if (elementsDslContext.getParentDslContext() instanceof ElementDslContext) { + elementDslContext = (ElementDslContext)elementsDslContext.getParentDslContext(); + } + } - element = identifiersRegister.getElement(parentIdentifier + "." + identifier); - parent = parent.getParent(); + if (elementDslContext != null) { + Element parent = elementDslContext.getElement(); + while (parent != null && element == null) { + String parentIdentifier = identifiersRegister.findIdentifier(parent); - element = checkElementType(element, type); - } + element = identifiersRegister.getElement(parentIdentifier + "." + identifier); + parent = parent.getParent(); + + element = checkElementType(element, type); } } else if (this instanceof DeploymentEnvironmentDslContext) { DeploymentEnvironmentDslContext deploymentEnvironmentDslContext = (DeploymentEnvironmentDslContext)this; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index 055167d93..4771af040 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -1,6 +1,11 @@ package com.structurizr.dsl; -import com.structurizr.model.*; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +import javax.lang.model.util.Elements; +import java.util.LinkedHashSet; +import java.util.Set; final class ExplicitRelationshipParser extends AbstractRelationshipParser { @@ -24,30 +29,55 @@ Relationship parse(DslContext context, Tokens tokens) { } String sourceId = tokens.get(SOURCE_IDENTIFIER_INDEX); + Element sourceElement = findElement(sourceId, context); + if (sourceElement == null) { + throw new RuntimeException("The source element \"" + sourceId + "\" does not exist"); + } + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + Element destinationElement = findElement(destinationId, context); + if (destinationElement == null) { + throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); + } - Element sourceElement = context.getElement(sourceId); - Element destinationElement = context.getElement(destinationId); + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } - if (sourceElement == null) { - if (StructurizrDslTokens.THIS_TOKEN.equalsIgnoreCase(sourceId) && context instanceof GroupableElementDslContext) { - GroupableElementDslContext groupableElementDslContext = (GroupableElementDslContext)context; - sourceElement = groupableElementDslContext.getElement(); - } else { - throw new RuntimeException("The source element \"" + sourceId + "\" does not exist"); - } + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); } - if (destinationElement == null) { - if (StructurizrDslTokens.THIS_TOKEN.equalsIgnoreCase(destinationId) && context instanceof ModelItemDslContext) { - ModelItemDslContext modelItemDslContext = (ModelItemDslContext) context; - if (modelItemDslContext.getModelItem() instanceof Element) { - destinationElement = (Element)modelItemDslContext.getModelItem(); - } - } + String[] tags = new String[0]; + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX).split(","); } - if (destinationElement == null) { + return createRelationship(sourceElement, description, technology, tags, destinationElement); + } + + void parse(ElementsDslContext context, Tokens tokens) { + // -> [description] [technology] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String sourceId = tokens.get(SOURCE_IDENTIFIER_INDEX); + Set sourceElements = findElements(sourceId, context); + if (sourceElements.isEmpty()) { + throw new RuntimeException("The source element \"" + sourceId + "\" does not exist"); + } + + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + Set destinationElements = findElements(destinationId, context); + if (destinationElements.isEmpty()) { throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); } @@ -66,7 +96,36 @@ Relationship parse(DslContext context, Tokens tokens) { tags = tokens.get(TAGS_INDEX).split(","); } - return createRelationship(sourceElement, description, technology, tags, destinationElement); + for (Element sourceElement : sourceElements) { + for (Element destinationElement : destinationElements) { + createRelationship(sourceElement, description, technology, tags, destinationElement); + } + } + } + + private Element findElement(String identifier, DslContext context) { + Element element = context.getElement(identifier); + + if (element == null && StructurizrDslTokens.THIS_TOKEN.equalsIgnoreCase(identifier) && context instanceof ElementDslContext) { + element = ((ElementDslContext)context).getElement(); + } + + return element; + } + + private Set findElements(String identifier, ElementsDslContext context) { + Element element = context.getElement(identifier); + Set elements = new LinkedHashSet<>(); + + if (element == null) { + if (StructurizrDslTokens.THIS_TOKEN.equalsIgnoreCase(identifier)) { + elements.addAll(context.getElements()); + } + } else { + elements.add(element); + } + + return elements; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index d076dffa2..448ee67d1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -1,6 +1,9 @@ package com.structurizr.dsl; -import com.structurizr.model.*; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +import java.util.Set; final class ImplicitRelationshipParser extends AbstractRelationshipParser { @@ -11,7 +14,7 @@ final class ImplicitRelationshipParser extends AbstractRelationshipParser { private final static int TECHNOLOGY_INDEX = 3; private final static int TAGS_INDEX = 4; - Relationship parse(ModelItemDslContext context, Tokens tokens) { + Relationship parse(ElementDslContext context, Tokens tokens) { // -> [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -24,9 +27,9 @@ Relationship parse(ModelItemDslContext context, Tokens tokens) { String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); - Element sourceElement = (Element)context.getModelItem(); - Element destinationElement = context.getElement(destinationId); + Element sourceElement = context.getElement(); + Element destinationElement = context.getElement(destinationId); if (destinationElement == null) { throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); } @@ -49,4 +52,43 @@ Relationship parse(ModelItemDslContext context, Tokens tokens) { return createRelationship(sourceElement, description, technology, tags, destinationElement); } + void parse(ElementsDslContext context, Tokens tokens) { + // -> [description] [technology] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + Set sourceElements = context.getElements(); + + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + Element destinationElement = context.getElement(destinationId); + if (destinationElement == null) { + throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); + } + + String description = ""; + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + String technology = ""; + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + + String[] tags = new String[0]; + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX).split(","); + } + + for (Element sourceElement : sourceElements) { + createRelationship(sourceElement, description, technology, tags, destinationElement); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index d1e2cf10c..41812077c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -295,7 +295,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (inContext(ExternalScriptDslContext.class)) { new ScriptParser().parseParameter(getContext(ExternalScriptDslContext.class), tokens); - } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(CustomElementDslContext.class) || inContext(PersonDslContext.class) || inContext(SoftwareSystemDslContext.class) || inContext(ContainerDslContext.class) || inContext(ComponentDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(DeploymentNodeDslContext.class) || inContext(InfrastructureNodeDslContext.class) || inContext(SoftwareSystemInstanceDslContext.class) || inContext(ContainerInstanceDslContext.class))) { + } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) { Relationship relationship = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { @@ -304,8 +304,8 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr registerIdentifier(identifier, relationship); - } else if (tokens.size() >= 2 && RELATIONSHIP_TOKEN.equals(tokens.get(0)) && (inContext(CustomElementDslContext.class) || inContext(PersonDslContext.class) || inContext(SoftwareSystemDslContext.class) || inContext(ContainerDslContext.class) || inContext(ComponentDslContext.class) || inContext(DeploymentNodeDslContext.class) || inContext(InfrastructureNodeDslContext.class) || inContext(SoftwareSystemInstanceDslContext.class) || inContext(ContainerInstanceDslContext.class))) { - Relationship relationship = new ImplicitRelationshipParser().parse(getContext(ModelItemDslContext.class), tokens.withoutContextStartToken()); + } else if (tokens.size() >= 2 && RELATIONSHIP_TOKEN.equals(tokens.get(0)) && inContext(ElementDslContext.class)) { + Relationship relationship = new ImplicitRelationshipParser().parse(getContext(ElementDslContext.class), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { startContext(new RelationshipDslContext(relationship)); @@ -313,6 +313,12 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr registerIdentifier(identifier, relationship); + } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && inContext(ElementsDslContext.class)) { + new ExplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + + } else if (tokens.size() >= 2 && RELATIONSHIP_TOKEN.equals(tokens.get(0)) && inContext(ElementsDslContext.class)) { + new ImplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + } else if ((REF_TOKEN.equalsIgnoreCase(firstToken) || EXTEND_TOKEN.equalsIgnoreCase(firstToken)) && (inContext(ModelDslContext.class))) { ModelItem modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); @@ -348,7 +354,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } } - } else if (ELEMENTS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ModelItemDslContext.class))) { + } else if (ELEMENTS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { Set elements = new ElementsParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index e34e718c1..680f6d972 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1143,4 +1143,13 @@ void test_Var_CannotOverrideConst() { } } + + @Test + void test_bulkOperations() throws Exception { + File dslFile = new File("src/test/resources/dsl/bulk-operations.dsl"); + + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java index 25f7d194b..c0786cf19 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java @@ -9,8 +9,8 @@ class ImplicitRelationshipParserTests extends AbstractTests { private ImplicitRelationshipParser parser = new ImplicitRelationshipParser(); - private ModelItemDslContext context(Person person) { - ModelItemDslContext context = new PersonDslContext(person); + private ElementDslContext context(Person person) { + PersonDslContext context = new PersonDslContext(person); context.setWorkspace(workspace); model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); @@ -20,7 +20,7 @@ private ModelItemDslContext context(Person person) { @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(null), tokens("->", "destination", "description", "technology", "tags", "extra")); + parser.parse((ElementDslContext)null, tokens("->", "destination", "description", "technology", "tags", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: -> [description] [technology] [tags]", e.getMessage()); @@ -30,7 +30,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { try { - parser.parse(context(null), tokens("->")); + parser.parse((ElementDslContext)null, tokens("->")); fail(); } catch (Exception e) { assertEquals("Expected: -> [description] [technology] [tags]", e.getMessage()); @@ -40,7 +40,7 @@ void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { @Test void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { Person user = model.addPerson("User", "Description"); - ModelItemDslContext context = context(user); + ElementDslContext context = context(user); IdentifiersRegister elements = new IdentifiersRegister(); context.setIdentifierRegister(elements); @@ -56,7 +56,7 @@ void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { void test_parse_AddsTheRelationship() { Person user = model.addPerson("User", "Description"); SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - ModelItemDslContext context = context(user); + ElementDslContext context = context(user); IdentifiersRegister elements = new IdentifiersRegister(); elements.register("destination", softwareSystem); @@ -79,7 +79,7 @@ void test_parse_AddsTheRelationship() { void test_parse_AddsTheRelationshipWithADescription() { Person user = model.addPerson("User", "Description"); SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - ModelItemDslContext context = context(user); + ElementDslContext context = context(user); IdentifiersRegister elements = new IdentifiersRegister(); elements.register("destination", softwareSystem); @@ -102,7 +102,7 @@ void test_parse_AddsTheRelationshipWithADescription() { void test_parse_AddsTheRelationshipWithADescriptionAndTechnology() { Person user = model.addPerson("User", "Description"); SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - ModelItemDslContext context = context(user); + ElementDslContext context = context(user); IdentifiersRegister elements = new IdentifiersRegister(); elements.register("destination", softwareSystem); @@ -124,7 +124,7 @@ void test_parse_AddsTheRelationshipWithADescriptionAndTechnology() { void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTags() { Person user = model.addPerson("User", "Description"); SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - ModelItemDslContext context = context(user); + ElementDslContext context = context(user); IdentifiersRegister elements = new IdentifiersRegister(); elements.register("destination", softwareSystem); @@ -148,7 +148,7 @@ void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTe Person user = model.addPerson("User", "Description"); SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); - ModelItemDslContext context = context(user); + ElementDslContext context = context(user); IdentifiersRegister elements = new IdentifiersRegister(); elements.register("destination", container); diff --git a/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl b/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl new file mode 100644 index 000000000..95fdcaa27 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl @@ -0,0 +1,59 @@ +workspace { + + model { + user = person "User" + + !identifiers flat + + softwareSystem1 = softwareSystem "Software System 1" { + application1 = container "Application" { + component "ComponentA" + component "ComponentB" + component "ComponentC" + } + + databaseSchema1 = container "Database Schema" + + !elements "element.parent==application1" { + tags "Tag 1" + user -> this "Uses 1" + this -> databaseSchema1 "Uses 1" + } + + !elements "element.parent==application1" { + -> databaseSchema1 "Uses 2" + } + } + + !identifiers hierarchical + + softwareSystem2 = softwareSystem "Software System 2" { + application2 = container "Application" { + component "ComponentA" + component "ComponentB" + component "ComponentC" + } + + databaseSchema2 = container "Database Schema" + + !elements "element.parent==application2" { + tags "Tag 1" + user -> this "Uses" + this -> softwareSystem2.databaseSchema2 "Uses 1" + } + + !elements "element.parent==application2" { + this -> databaseSchema2 "Uses 2" + } + + !elements "element.parent==application2" { + -> softwareSystem2.databaseSchema2 "Uses 3" + } + + !elements "element.parent==application2" { + -> databaseSchema2 "Uses 4" + } + } + } + +} \ No newline at end of file From 477bac967b7046abdea50470eb4b1376587605dc Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 16:45:23 +0100 Subject: [PATCH 245/418] Adds an `!relationships` keyword that can be used to find a set of relationships via an expression. --- changelog.md | 1 + .../dsl/ExplicitRelationshipParser.java | 7 ++- .../dsl/ImplicitRelationshipParser.java | 8 +++- .../structurizr/dsl/ModelItemsDslContext.java | 4 ++ .../dsl/RelationshipsDslContext.java | 32 +++++++++++++ .../structurizr/dsl/RelationshipsParser.java | 29 ++++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 19 +++++++- .../structurizr/dsl/StructurizrDslTokens.java | 2 + .../structurizr/dsl/ElementsParserTests.java | 2 +- .../dsl/RelationshipsParserTests.java | 45 +++++++++++++++++++ .../test/resources/dsl/bulk-operations.dsl | 24 +++++++--- 11 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java diff --git a/changelog.md b/changelog.md index 0d0498992..903f6b2fb 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. - structurizr-dsl: Adds support for element technology expressions (e.g. "element.technology==Java"). - structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression. +- structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. ## 2.2.0 (2nd July 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index 4771af040..489958f19 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -58,7 +58,7 @@ Relationship parse(DslContext context, Tokens tokens) { return createRelationship(sourceElement, description, technology, tags, destinationElement); } - void parse(ElementsDslContext context, Tokens tokens) { + Set parse(ElementsDslContext context, Tokens tokens) { // -> [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -96,11 +96,14 @@ void parse(ElementsDslContext context, Tokens tokens) { tags = tokens.get(TAGS_INDEX).split(","); } + Set relationships = new LinkedHashSet<>(); for (Element sourceElement : sourceElements) { for (Element destinationElement : destinationElements) { - createRelationship(sourceElement, description, technology, tags, destinationElement); + relationships.add(createRelationship(sourceElement, description, technology, tags, destinationElement)); } } + + return relationships; } private Element findElement(String identifier, DslContext context) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index 448ee67d1..19448e25d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -3,6 +3,7 @@ import com.structurizr.model.Element; import com.structurizr.model.Relationship; +import java.util.LinkedHashSet; import java.util.Set; final class ImplicitRelationshipParser extends AbstractRelationshipParser { @@ -52,7 +53,7 @@ Relationship parse(ElementDslContext context, Tokens tokens) { return createRelationship(sourceElement, description, technology, tags, destinationElement); } - void parse(ElementsDslContext context, Tokens tokens) { + Set parse(ElementsDslContext context, Tokens tokens) { // -> [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -86,9 +87,12 @@ void parse(ElementsDslContext context, Tokens tokens) { tags = tokens.get(TAGS_INDEX).split(","); } + Set relationships = new LinkedHashSet<>(); for (Element sourceElement : sourceElements) { - createRelationship(sourceElement, description, technology, tags, destinationElement); + relationships.add(createRelationship(sourceElement, description, technology, tags, destinationElement)); } + + return relationships; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java index 1caf3055a..b4bedc9bc 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java @@ -8,6 +8,10 @@ abstract class ModelItemsDslContext extends DslContext { private final DslContext parentDslContext; + public ModelItemsDslContext() { + this.parentDslContext = null; + } + ModelItemsDslContext(DslContext parentDslContext) { this.parentDslContext = parentDslContext; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java new file mode 100644 index 000000000..3797be712 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java @@ -0,0 +1,32 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Relationship; + +import java.util.Set; +import java.util.stream.Collectors; + +class RelationshipsDslContext extends ModelItemsDslContext { + + private final Set relationships; + + RelationshipsDslContext(DslContext parentDslContext, Set relationships) { + super(parentDslContext); + this.relationships = relationships; + } + + Set getRelationships() { + return relationships; + } + + @Override + Set getModelItems() { + return relationships.stream().map(e -> (ModelItem)e).collect(Collectors.toSet()); + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java new file mode 100644 index 000000000..eb0e0cf7b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.ModelItem; +import com.structurizr.model.Relationship; + +import java.util.Set; +import java.util.stream.Collectors; + +final class RelationshipsParser extends AbstractParser { + + private static final String GRAMMAR = "!relationships "; + + private final static int EXPRESSION_INDEX = 1; + + Set parse(DslContext context, Tokens tokens) { + // !relationships + + if (tokens.hasMoreThan(EXPRESSION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + String expression = tokens.get(1); + Set modelItems = new ExpressionParser().parseExpression(expression, context); + + return modelItems.stream().filter(mi -> mi instanceof Relationship).map(mi -> (Relationship)mi).collect(Collectors.toSet()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 41812077c..d1d423ed0 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -314,10 +314,18 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr registerIdentifier(identifier, relationship); } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && inContext(ElementsDslContext.class)) { - new ExplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + Set relationships = new ExplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipsDslContext(getContext(), relationships)); + } } else if (tokens.size() >= 2 && RELATIONSHIP_TOKEN.equals(tokens.get(0)) && inContext(ElementsDslContext.class)) { - new ImplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + Set relationships = new ImplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipsDslContext(getContext(), relationships)); + } } else if ((REF_TOKEN.equalsIgnoreCase(firstToken) || EXTEND_TOKEN.equalsIgnoreCase(firstToken)) && (inContext(ModelDslContext.class))) { ModelItem modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); @@ -361,6 +369,13 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr startContext(new ElementsDslContext(getContext(), elements)); } + } else if (RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { + Set relationships = new RelationshipsParser().parse(getContext(), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipsDslContext(getContext(), relationships)); + } + } else if (CUSTOM_ELEMENT_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { CustomElement customElement = new CustomElementParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index ab7aff0b1..4ef709312 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -107,6 +107,8 @@ class StructurizrDslTokens { static final String IMPLIED_RELATIONSHIPS_TOKEN = "!impliedRelationships"; static final String REF_TOKEN = "!ref"; static final String ELEMENTS_TOKEN = "!elements"; + static final String RELATIONSHIPS_TOKEN = "!relationships"; + static final String EXTEND_TOKEN = "!extend"; static final String PLUGIN_TOKEN = "!plugin"; static final String SCRIPT_TOKEN = "!script"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java index 3af081588..6e437b972 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java @@ -31,7 +31,7 @@ void test_parse_FindsElementsByExpression() { Component componentB = application.addComponent("B"); Component componentC = application.addComponent("C"); - ModelItemDslContext context = new ContainerDslContext(application); + ContainerDslContext context = new ContainerDslContext(application); context.setWorkspace(workspace); IdentifiersRegister register = new IdentifiersRegister(); register.register("application", application); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java new file mode 100644 index 000000000..c3e685ccc --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java @@ -0,0 +1,45 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class RelationshipsParserTests extends AbstractTests { + + private final RelationshipsParser parser = new RelationshipsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!relationships", "expression", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !relationships ", e.getMessage()); + } + } + + @Test + void test_parse_FindsRelationshipsByExpression() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + Relationship relationship1 = a.uses(b, "Uses"); + Relationship relationship2 = b.uses(c, "Uses"); + Relationship relationship3 = a.uses(c, "Uses"); + + ModelDslContext context = context(); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("c", c); + context.setIdentifierRegister(register); + + Set relationships = parser.parse(context, tokens("!relationships", "*->c")); + assertTrue(relationships.contains(relationship2)); + assertTrue(relationships.contains(relationship3)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl b/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl index 95fdcaa27..d1833badc 100644 --- a/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl +++ b/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl @@ -17,11 +17,15 @@ workspace { !elements "element.parent==application1" { tags "Tag 1" user -> this "Uses 1" - this -> databaseSchema1 "Uses 1" + this -> databaseSchema1 "Uses 1" { + tags "Tag" + } } !elements "element.parent==application1" { - -> databaseSchema1 "Uses 2" + -> databaseSchema1 "Uses 2" { + tags "Tag" + } } } @@ -39,19 +43,27 @@ workspace { !elements "element.parent==application2" { tags "Tag 1" user -> this "Uses" - this -> softwareSystem2.databaseSchema2 "Uses 1" + this -> softwareSystem2.databaseSchema2 "Uses 1" { + tags "Tag" + } } !elements "element.parent==application2" { - this -> databaseSchema2 "Uses 2" + this -> databaseSchema2 "Uses 2" { + tags "Tag" + } } !elements "element.parent==application2" { - -> softwareSystem2.databaseSchema2 "Uses 3" + -> softwareSystem2.databaseSchema2 "Uses 3" { + tags "Tag" + } } !elements "element.parent==application2" { - -> databaseSchema2 "Uses 4" + -> databaseSchema2 "Uses 4" { + tags "Tag" + } } } } From 0f2b05fb75a2a9f84131ac5ee44914857b48c1b6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 20 Aug 2024 17:03:18 +0100 Subject: [PATCH 246/418] Initial version of DSL wrapper for the component finder. --- changelog.md | 2 + structurizr-dsl/build.gradle | 1 + .../dsl/ComponentFinderDslContext.java | 32 ++++ .../dsl/ComponentFinderParser.java | 28 ++++ .../ComponentFinderStrategyDslContext.java | 34 ++++ .../dsl/ComponentFinderStrategyParser.java | 158 ++++++++++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 30 ++++ .../structurizr/dsl/StructurizrDslTokens.java | 9 + .../java/com/structurizr/dsl/DslTests.java | 56 +++++++ .../test/resources/dsl/spring-petclinic.dsl | 96 +++++++++++ 10 files changed, 446 insertions(+) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java create mode 100644 structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl diff --git a/changelog.md b/changelog.md index 903f6b2fb..16c2ac577 100644 --- a/changelog.md +++ b/changelog.md @@ -3,12 +3,14 @@ ## unreleased - structurizr-core: Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316). +- structurizr-component: Initial rewrite of the original `structurizr-analysis` library - provides a way to automatically find components in a Java codebase. - structurizr-dsl: Adds name-value properties to dynamic view relationship views. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. - structurizr-dsl: Adds support for element technology expressions (e.g. "element.technology==Java"). - structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression. - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. +- structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. ## 2.2.0 (2nd July 2024) diff --git a/structurizr-dsl/build.gradle b/structurizr-dsl/build.gradle index 3416670df..2b6eaabbb 100644 --- a/structurizr-dsl/build.gradle +++ b/structurizr-dsl/build.gradle @@ -2,6 +2,7 @@ dependencies { api project(':structurizr-client') api project(':structurizr-import') + api project(':structurizr-component') testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.19' testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.8.10' diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java new file mode 100644 index 000000000..c63b2fc3c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java @@ -0,0 +1,32 @@ +package com.structurizr.dsl; + +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.model.Container; + +final class ComponentFinderDslContext extends DslContext { + + private final ComponentFinderBuilder componentFinderBuilder = new ComponentFinderBuilder(); + + ComponentFinderDslContext(Container container) { + componentFinderBuilder.forContainer(container); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.COMPONENT_FINDER_CLASSES_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_SOURCE_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_TOKEN + }; + } + + ComponentFinderBuilder getComponentFinderBuilder() { + return this.componentFinderBuilder; + } + + @Override + void end() { + componentFinderBuilder.build().findComponents(); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java new file mode 100644 index 000000000..61d0d49d6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java @@ -0,0 +1,28 @@ +package com.structurizr.dsl; + +final class ComponentFinderParser extends AbstractParser { + + private static final String CLASSES_GRAMMAR = "classes "; + private static final String SOURCE_GRAMMAR = "source "; + + void parseClasses(ComponentFinderDslContext context, Tokens tokens) { + // classes + + if (tokens.hasMoreThan(1)) { + throw new RuntimeException("Too many tokens, expected: " + CLASSES_GRAMMAR); + } + + context.getComponentFinderBuilder().fromClasses(tokens.get(1)); + } + + void parseSource(ComponentFinderDslContext context, Tokens tokens) { + // source + + if (tokens.hasMoreThan(1)) { + throw new RuntimeException("Too many tokens, expected: " + SOURCE_GRAMMAR); + } + + context.getComponentFinderBuilder().fromSource(tokens.get(1)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java new file mode 100644 index 000000000..fede404b4 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java @@ -0,0 +1,34 @@ +package com.structurizr.dsl; + +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.component.ComponentFinderStrategyBuilder; + +final class ComponentFinderStrategyDslContext extends DslContext { + + private final ComponentFinderBuilder componentFinderBuilder; + private final ComponentFinderStrategyBuilder componentFinderStrategyBuilder = new ComponentFinderStrategyBuilder(); + + ComponentFinderStrategyDslContext(ComponentFinderBuilder componentFinderBuilder) { + this.componentFinderBuilder = componentFinderBuilder; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FILTER_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_NAMING_TOKEN + }; + } + + ComponentFinderStrategyBuilder getComponentFinderStrategyBuilder() { + return this.componentFinderStrategyBuilder; + } + + @Override + void end() { + componentFinderBuilder.withStrategy(componentFinderStrategyBuilder.build()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java new file mode 100644 index 000000000..32bd19a64 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -0,0 +1,158 @@ +package com.structurizr.dsl; + +import com.structurizr.component.filter.ExcludeTypesByRegexFilter; +import com.structurizr.component.filter.IncludeTypesByRegexFilter; +import com.structurizr.component.matcher.*; +import com.structurizr.component.naming.DefaultPackageNamingStrategy; +import com.structurizr.component.naming.SimpleNamingStrategy; +import com.structurizr.component.naming.FullyQualifiedNamingStrategy; +import com.structurizr.component.supporting.AllReferencedTypesInPackageSupportingTypesStrategy; +import com.structurizr.component.supporting.AllReferencedTypesSupportingTypesStrategy; +import com.structurizr.component.supporting.AllTypesInPackageSupportingTypesStrategy; +import com.structurizr.component.supporting.AllTypesUnderPackageSupportingTypesStrategy; + +final class ComponentFinderStrategyParser extends AbstractParser { + + private static final String MATCHER_GRAMMAR = "matcher [parameters]"; + private static final String FILTER_GRAMMAR = "filter [parameters]"; + private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes [parameters]"; + private static final String NAMING_GRAMMAR = "naming "; + + void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens) { + if (tokens.size() < 2) { + throw new RuntimeException("Too few tokens, expected: " + MATCHER_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case "annotation": + if (tokens.size() == 4) { + String name = tokens.get(2); + String technology = tokens.get(3); + + context.getComponentFinderStrategyBuilder().matchedBy(new AnnotationTypeMatcher(name, technology)); + } else { + throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + } + break; + case "extends": + if (tokens.size() == 4) { + String name = tokens.get(2); + String technology = tokens.get(3); + + context.getComponentFinderStrategyBuilder().matchedBy(new ExtendsTypeMatcher(name, technology)); + } else { + throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + } + break; + case "implements": + if (tokens.size() == 4) { + String name = tokens.get(2); + String technology = tokens.get(3); + + context.getComponentFinderStrategyBuilder().matchedBy(new ImplementsTypeMatcher(name, technology)); + } else { + throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + } + break; + case "namesuffix": + if (tokens.size() == 4) { + String suffix = tokens.get(2); + String technology = tokens.get(3); + + context.getComponentFinderStrategyBuilder().matchedBy(new NameSuffixTypeMatcher(suffix, technology)); + } else { + throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + } + break; + case "regex": + if (tokens.size() == 4) { + String regex = tokens.get(2); + String technology = tokens.get(3); + + context.getComponentFinderStrategyBuilder().matchedBy(new RegexTypeMatcher(regex, technology)); + } else { + throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + } + break; + default: + throw new IllegalArgumentException("Unknown matcher: " + type); + } + } + + void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens) { + if (tokens.size() < 2) { + throw new RuntimeException("Too few tokens, expected: " + FILTER_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case "includeregex": + if (tokens.size() == 3) { + String regex = tokens.get(2); + + context.getComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(regex)); + } else { + throw new RuntimeException("Expected: " + FILTER_GRAMMAR); + } + break; + case "excluderegex": + if (tokens.size() == 3) { + String regex = tokens.get(2); + + context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeTypesByRegexFilter(regex)); + } else { + throw new RuntimeException("Expected: " + FILTER_GRAMMAR); + } + break; + default: + throw new IllegalArgumentException("Unknown filter: " + type); + } + } + + void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens tokens) { + if (tokens.size() < 2) { + throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case "referenced": + context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesSupportingTypesStrategy()); + break; + case "referencedinpackage": + context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesInPackageSupportingTypesStrategy()); + break; + case "inpackage": + context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesInPackageSupportingTypesStrategy()); + break; + case "underpackage": + context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesUnderPackageSupportingTypesStrategy()); + break; + default: + throw new IllegalArgumentException("Unknown supporting types strategy: " + type); + } + } + + void parseNaming(ComponentFinderStrategyDslContext context, Tokens tokens) { + if (tokens.size() < 1) { + throw new RuntimeException("Too few tokens, expected: " + NAMING_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case "name": + context.getComponentFinderStrategyBuilder().namedBy(new SimpleNamingStrategy()); + break; + case "fqn": + context.getComponentFinderStrategyBuilder().namedBy(new FullyQualifiedNamingStrategy()); + break; + case "package": + context.getComponentFinderStrategyBuilder().namedBy(new DefaultPackageNamingStrategy()); + break; + default: + throw new IllegalArgumentException("Unknown naming strategy: " + type); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index d1d423ed0..f44406559 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -421,6 +421,36 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr registerIdentifier(identifier, component); + } else if (COMPONENT_FINDER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + if (!restricted) { + if (shouldStartContext(tokens)) { + startContext(new ComponentFinderDslContext(getContext(ContainerDslContext.class).getContainer())); + } + } + + } else if (COMPONENT_FINDER_CLASSES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { + new ComponentFinderParser().parseClasses(getContext(ComponentFinderDslContext.class), tokens); + + } else if (COMPONENT_FINDER_SOURCE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { + new ComponentFinderParser().parseSource(getContext(ComponentFinderDslContext.class), tokens); + + } else if (COMPONENT_FINDER_STRATEGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { + if (shouldStartContext(tokens)) { + startContext(new ComponentFinderStrategyDslContext(getContext(ComponentFinderDslContext.class).getComponentFinderBuilder())); + } + + } else if (COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseMatcher(getContext(ComponentFinderStrategyDslContext.class), tokens); + + } else if (COMPONENT_FINDER_STRATEGY_FILTER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseFilter(getContext(ComponentFinderStrategyDslContext.class), tokens); + + } else if (COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseSupportingTypes(getContext(ComponentFinderStrategyDslContext.class), tokens); + + } else if (COMPONENT_FINDER_STRATEGY_NAMING_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseNaming(getContext(ComponentFinderStrategyDslContext.class), tokens); + } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { throw new RuntimeException("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group)"); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 4ef709312..1962ec6b0 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -113,4 +113,13 @@ class StructurizrDslTokens { static final String PLUGIN_TOKEN = "!plugin"; static final String SCRIPT_TOKEN = "!script"; + static final String COMPONENT_FINDER_TOKEN = "!components"; + static final String COMPONENT_FINDER_CLASSES_TOKEN = "classes"; + static final String COMPONENT_FINDER_SOURCE_TOKEN = "source"; + static final String COMPONENT_FINDER_STRATEGY_TOKEN = "strategy"; + static final String COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN = "matcher"; + static final String COMPONENT_FINDER_STRATEGY_FILTER_TOKEN = "filter"; + static final String COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN = "supportingTypes"; + static final String COMPONENT_FINDER_STRATEGY_NAMING_TOKEN = "naming"; + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 680f6d972..15ec0e457 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1143,6 +1143,62 @@ void test_Var_CannotOverrideConst() { } } + @Test + void springPetClinic() throws Exception { + File path = new File("/Users/simon/sandbox/spring-petclinic"); + if (path.exists()) { + System.out.println("Running Spring PetClinic example..."); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.addConstant("SPRING_PETCLINIC_DIR", path.getAbsolutePath()); + parser.parse(new File("src/test/resources/dsl/spring-petclinic.dsl")); + + Container webApplication = (Container)parser.getIdentifiersRegister().getElement("springPetClinic.webApplication"); + assertEquals(7, webApplication.getComponents().size()); + + Component welcomeController = webApplication.getComponentWithName("Welcome Controller"); + assertNotNull(welcomeController); + assertEquals("org.springframework.samples.petclinic.system.WelcomeController", welcomeController.getProperties().get("component.type")); + assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java").getAbsolutePath(), welcomeController.getProperties().get("component.src")); + + Component ownerController = webApplication.getComponentWithName("Owner Controller"); + assertNotNull(ownerController); + assertEquals("org.springframework.samples.petclinic.owner.OwnerController", ownerController.getProperties().get("component.type")); + assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java").getAbsolutePath(), ownerController.getProperties().get("component.src")); + + Component petController = webApplication.getComponentWithName("Pet Controller"); + assertNotNull(petController); + assertEquals("org.springframework.samples.petclinic.owner.PetController", petController.getProperties().get("component.type")); + assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/PetController.java").getAbsolutePath(), petController.getProperties().get("component.src")); + + Component vetController = webApplication.getComponentWithName("Vet Controller"); + assertNotNull(vetController); + assertEquals("org.springframework.samples.petclinic.vet.VetController", vetController.getProperties().get("component.type")); + assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/vet/VetController.java").getAbsolutePath(), vetController.getProperties().get("component.src")); + + Component visitController = webApplication.getComponentWithName("Visit Controller"); + assertNotNull(visitController); + assertEquals("org.springframework.samples.petclinic.owner.VisitController", visitController.getProperties().get("component.type")); + assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/VisitController.java").getAbsolutePath(), visitController.getProperties().get("component.src")); + + Component ownerRepository = webApplication.getComponentWithName("Owner Repository"); + assertNotNull(ownerRepository); + assertEquals("org.springframework.samples.petclinic.owner.OwnerRepository", ownerRepository.getProperties().get("component.type")); + assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); + + Component vetRepository = webApplication.getComponentWithName("Vet Repository"); + assertNotNull(vetRepository); + assertEquals("org.springframework.samples.petclinic.vet.VetRepository", vetRepository.getProperties().get("component.type")); + assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src")); + + assertTrue(welcomeController.getRelationships().isEmpty()); + + assertNotNull(petController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(visitController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(ownerController.getEfferentRelationshipWith(ownerRepository)); + + assertNotNull(vetController.getEfferentRelationshipWith(vetRepository)); + } + } @Test void test_bulkOperations() throws Exception { diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl new file mode 100644 index 000000000..d1bbc3021 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl @@ -0,0 +1,96 @@ + workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (https://github.com/spring-projects/spring-petclinic/)" { + + !identifiers hierarchical + + model { + clinicEmployee = person "Clinic Employee" "An employee of the clinic." + springPetClinic = softwareSystem "Spring PetClinic" "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." { + relationalDatabaseSchema = container "Relational Database Schema" { + description "Stores information regarding the veterinarians, the clients, and their pets." + technology "Relational Database Schema" + tag "Relational Database Schema" + } + + webApplication = container "Web Application" { + description "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." + technology "Java and Spring" + + !components { + classes "${SPRING_PETCLINIC_DIR}/target/spring-petclinic-3.3.0-SNAPSHOT.jar" + source "${SPRING_PETCLINIC_DIR}/src/main/java" + strategy { + matcher annotation "org.springframework.stereotype.Controller" "Spring MVC Controller" + filter excludeRegex ".*.CrashController" + } + strategy { + matcher implements "org.springframework.data.repository.Repository" "Spring Data Repository" + } + } + + !script groovy { + element.components.each { it.url = it.properties["component.src"].replace(context.dslParser.getConstant("SPRING_PETCLINIC_DIR") + "/src/main/java", "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java") } + } + + !elements "element.parent==webApplication && element.technology==Spring MVC Controller" { + clinicEmployee -> this "Uses" + tag "Spring MVC Controller" + } + + !elements "element.parent==webApplication && element.technology==Spring Data Repository" { + -> relationalDatabaseSchema "Reads from and writes to" + tag "Spring Data Repository" + } + } + } + } + + views { + systemContext springPetClinic "SystemContext" { + include * + autolayout + } + + container springPetClinic "Containers" { + include * + autolayout + } + + component springPetClinic.webApplication "Components" { + include * + autolayout + } + + styles { + element "Person" { + shape person + background #519823 + color #FFFFFF + } + + element "Software System" { + background #6CB33E + color #FFFFFF + } + + element "Container" { + background #91D366 + color #FFFFFF + } + + element "Relational Database Schema" { + shape cylinder + } + + element "Spring MVC Controller" { + background #D4F3C0 + color #000000 + } + + element "Spring Data Repository" { + background #95D46C + color #000000 + } + } + } + +} \ No newline at end of file From 69262cc78fafda837f34f71a13811b67529e4475 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 21 Aug 2024 09:21:49 +0100 Subject: [PATCH 247/418] Makes the Spring PetClinic example easier to run. --- .../java/com/structurizr/dsl/DslTests.java | 30 ++++++++++++------- .../test/resources/dsl/spring-petclinic.dsl | 10 +++++-- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 15ec0e457..bb5cc5041 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -3,6 +3,7 @@ import com.structurizr.Workspace; import com.structurizr.documentation.Section; import com.structurizr.model.*; +import com.structurizr.util.StringUtils; import com.structurizr.view.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -1145,11 +1146,11 @@ void test_Var_CannotOverrideConst() { @Test void springPetClinic() throws Exception { - File path = new File("/Users/simon/sandbox/spring-petclinic"); - if (path.exists()) { + String springPetClinicHome = System.getenv().getOrDefault("SPRING_PETCLINIC_HOME", ""); + System.out.println(springPetClinicHome); + if (!StringUtils.isNullOrEmpty(springPetClinicHome)) { System.out.println("Running Spring PetClinic example..."); StructurizrDslParser parser = new StructurizrDslParser(); - parser.addConstant("SPRING_PETCLINIC_DIR", path.getAbsolutePath()); parser.parse(new File("src/test/resources/dsl/spring-petclinic.dsl")); Container webApplication = (Container)parser.getIdentifiersRegister().getElement("springPetClinic.webApplication"); @@ -1158,37 +1159,44 @@ void springPetClinic() throws Exception { Component welcomeController = webApplication.getComponentWithName("Welcome Controller"); assertNotNull(welcomeController); assertEquals("org.springframework.samples.petclinic.system.WelcomeController", welcomeController.getProperties().get("component.type")); - assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java").getAbsolutePath(), welcomeController.getProperties().get("component.src")); + assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java").getAbsolutePath(), welcomeController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getUrl()); Component ownerController = webApplication.getComponentWithName("Owner Controller"); assertNotNull(ownerController); assertEquals("org.springframework.samples.petclinic.owner.OwnerController", ownerController.getProperties().get("component.type")); - assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java").getAbsolutePath(), ownerController.getProperties().get("component.src")); + assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java").getAbsolutePath(), ownerController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getUrl()); Component petController = webApplication.getComponentWithName("Pet Controller"); assertNotNull(petController); assertEquals("org.springframework.samples.petclinic.owner.PetController", petController.getProperties().get("component.type")); - assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/PetController.java").getAbsolutePath(), petController.getProperties().get("component.src")); + assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/PetController.java").getAbsolutePath(), petController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetController.java", petController.getUrl()); Component vetController = webApplication.getComponentWithName("Vet Controller"); assertNotNull(vetController); assertEquals("org.springframework.samples.petclinic.vet.VetController", vetController.getProperties().get("component.type")); - assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/vet/VetController.java").getAbsolutePath(), vetController.getProperties().get("component.src")); + assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetController.java").getAbsolutePath(), vetController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetController.java", vetController.getUrl()); Component visitController = webApplication.getComponentWithName("Visit Controller"); assertNotNull(visitController); assertEquals("org.springframework.samples.petclinic.owner.VisitController", visitController.getProperties().get("component.type")); - assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/VisitController.java").getAbsolutePath(), visitController.getProperties().get("component.src")); + assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/VisitController.java").getAbsolutePath(), visitController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java", visitController.getUrl()); Component ownerRepository = webApplication.getComponentWithName("Owner Repository"); assertNotNull(ownerRepository); assertEquals("org.springframework.samples.petclinic.owner.OwnerRepository", ownerRepository.getProperties().get("component.type")); - assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); + assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); Component vetRepository = webApplication.getComponentWithName("Vet Repository"); assertNotNull(vetRepository); assertEquals("org.springframework.samples.petclinic.vet.VetRepository", vetRepository.getProperties().get("component.type")); - assertEquals(new File(path, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src")); + assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getUrl()); assertTrue(welcomeController.getRelationships().isEmpty()); @@ -1197,6 +1205,8 @@ void springPetClinic() throws Exception { assertNotNull(ownerController.getEfferentRelationshipWith(ownerRepository)); assertNotNull(vetController.getEfferentRelationshipWith(vetRepository)); + } else { + System.out.println("Skipping Spring PetClinic example..."); } } diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl index d1bbc3021..279c77f73 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl @@ -1,5 +1,9 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (https://github.com/spring-projects/spring-petclinic/)" { + // this example requires an environment variable as follows: + // - Name: SPRING_PETCLINIC_HOME + // - Value: the full path to the location of the spring-petclinic example (e.g. /Users/simon/spring-petclinic) + !identifiers hierarchical model { @@ -16,8 +20,8 @@ technology "Java and Spring" !components { - classes "${SPRING_PETCLINIC_DIR}/target/spring-petclinic-3.3.0-SNAPSHOT.jar" - source "${SPRING_PETCLINIC_DIR}/src/main/java" + classes "${SPRING_PETCLINIC_HOME}/target/spring-petclinic-3.3.0-SNAPSHOT.jar" + source "${SPRING_PETCLINIC_HOME}/src/main/java" strategy { matcher annotation "org.springframework.stereotype.Controller" "Spring MVC Controller" filter excludeRegex ".*.CrashController" @@ -28,7 +32,7 @@ } !script groovy { - element.components.each { it.url = it.properties["component.src"].replace(context.dslParser.getConstant("SPRING_PETCLINIC_DIR") + "/src/main/java", "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java") } + element.components.each { it.url = it.properties["component.src"].replace(System.getenv("SPRING_PETCLINIC_HOME") + "/src/main/java", "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java") } } !elements "element.parent==webApplication && element.technology==Spring MVC Controller" { From 726f6ebe0e805cbe5e51e2505e302d3c933c79f1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 21 Aug 2024 09:27:39 +0100 Subject: [PATCH 248/418] Makes the Spring PetClinic example easier to run from Lite. --- .gitignore | 2 ++ structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java | 2 +- .../{spring-petclinic.dsl => spring-petclinic/workspace.dsl} | 0 3 files changed, 3 insertions(+), 1 deletion(-) rename structurizr-dsl/src/test/resources/dsl/{spring-petclinic.dsl => spring-petclinic/workspace.dsl} (100%) diff --git a/.gitignore b/.gitignore index 7068eac3c..30458f2b7 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ bin # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +structurizr-dsl/src/test/resources/dsl/spring-petclinic/.structurizr +structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.json \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index bb5cc5041..3518da1f8 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1151,7 +1151,7 @@ void springPetClinic() throws Exception { if (!StringUtils.isNullOrEmpty(springPetClinicHome)) { System.out.println("Running Spring PetClinic example..."); StructurizrDslParser parser = new StructurizrDslParser(); - parser.parse(new File("src/test/resources/dsl/spring-petclinic.dsl")); + parser.parse(new File("src/test/resources/dsl/spring-petclinic/workspace.dsl")); Container webApplication = (Container)parser.getIdentifiersRegister().getElement("springPetClinic.webApplication"); assertEquals(7, webApplication.getComponents().size()); diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/spring-petclinic.dsl rename to structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl From 6c8689a044f1a736fef971a1a2fa63eac2587d2a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 22 Aug 2024 21:04:05 +0100 Subject: [PATCH 249/418] Adds discovered components into the DSL parser identifiers register. --- .../com/structurizr/component/ComponentFinder.java | 6 +++++- .../structurizr/dsl/ComponentFinderDslContext.java | 13 +++++++++++-- .../com/structurizr/dsl/IdentifiersRegister.java | 9 +++++++++ .../com/structurizr/dsl/StructurizrDslParser.java | 6 +++--- .../src/test/java/com/structurizr/dsl/DslTests.java | 7 +++++++ 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index 5234800fc..541d2c082 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -98,9 +98,10 @@ private void findDependencies(com.structurizr.component.Type type) { /** * Find components, using all configured rules, in the order they were added. */ - public void findComponents() { + public Set findComponents() { Set discoveredComponents = new LinkedHashSet<>(); Map componentMap = new HashMap<>(); + Set componentSet = new LinkedHashSet<>(); for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { Set set = componentFinderStrategy.findComponents(typeRepository); @@ -119,6 +120,7 @@ public void findComponents() { component.setDescription(discoveredComponent.getDescription()); component.setTechnology(discoveredComponent.getTechnology()); componentMap.put(discoveredComponent, component); + componentSet.add(component); } // find dependencies between all components @@ -141,6 +143,8 @@ public void findComponents() { Component component = componentMap.get(discoveredComponent); discoveredComponent.getComponentFinderStrategy().visit(component); } + + return componentSet; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java index c63b2fc3c..0172fd657 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java @@ -1,13 +1,19 @@ package com.structurizr.dsl; import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.model.Component; import com.structurizr.model.Container; +import java.util.Set; + final class ComponentFinderDslContext extends DslContext { private final ComponentFinderBuilder componentFinderBuilder = new ComponentFinderBuilder(); - ComponentFinderDslContext(Container container) { + private final StructurizrDslParser dslParser; + + ComponentFinderDslContext(StructurizrDslParser dslParser, Container container) { + this.dslParser = dslParser; componentFinderBuilder.forContainer(container); } @@ -26,7 +32,10 @@ ComponentFinderBuilder getComponentFinderBuilder() { @Override void end() { - componentFinderBuilder.build().findComponents(); + Set components = componentFinderBuilder.build().findComponents(); + for (Component component : components) { + dslParser.registerIdentifier(IdentifiersRegister.toIdentifier(component.getName()), component); + } } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java index 1d5524552..654538752 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java @@ -219,4 +219,13 @@ void validateIdentifierName(String identifier) { } } + static String toIdentifier(String s) { + String identifierName = s.replaceAll("[^a-zA-Z0-9_-]", ""); + if (identifierName.startsWith("-")) { + identifierName = identifierName.substring(1); + } + + return identifierName; + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index f44406559..a162091f8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -424,7 +424,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (COMPONENT_FINDER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { if (!restricted) { if (shouldStartContext(tokens)) { - startContext(new ComponentFinderDslContext(getContext(ContainerDslContext.class).getContainer())); + startContext(new ComponentFinderDslContext(this, getContext(ContainerDslContext.class).getContainer())); } } @@ -1121,12 +1121,12 @@ public IdentifiersRegister getIdentifiersRegister() { return identifiersRegister; } - private void registerIdentifier(String identifier, Element element) { + void registerIdentifier(String identifier, Element element) { identifiersRegister.register(identifier, element); element.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(element)); } - private void registerIdentifier(String identifier, Relationship relationship) { + void registerIdentifier(String identifier, Relationship relationship) { identifiersRegister.register(identifier, relationship); relationship.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(relationship)); } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 3518da1f8..981a5f5ae 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1161,42 +1161,49 @@ void springPetClinic() throws Exception { assertEquals("org.springframework.samples.petclinic.system.WelcomeController", welcomeController.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java").getAbsolutePath(), welcomeController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getUrl()); + assertSame(welcomeController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.welcomecontroller")); Component ownerController = webApplication.getComponentWithName("Owner Controller"); assertNotNull(ownerController); assertEquals("org.springframework.samples.petclinic.owner.OwnerController", ownerController.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java").getAbsolutePath(), ownerController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getUrl()); + assertSame(ownerController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerController")); Component petController = webApplication.getComponentWithName("Pet Controller"); assertNotNull(petController); assertEquals("org.springframework.samples.petclinic.owner.PetController", petController.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/PetController.java").getAbsolutePath(), petController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetController.java", petController.getUrl()); + assertSame(petController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.petcontroller")); Component vetController = webApplication.getComponentWithName("Vet Controller"); assertNotNull(vetController); assertEquals("org.springframework.samples.petclinic.vet.VetController", vetController.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetController.java").getAbsolutePath(), vetController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetController.java", vetController.getUrl()); + assertSame(vetController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetcontroller")); Component visitController = webApplication.getComponentWithName("Visit Controller"); assertNotNull(visitController); assertEquals("org.springframework.samples.petclinic.owner.VisitController", visitController.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/VisitController.java").getAbsolutePath(), visitController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java", visitController.getUrl()); + assertSame(visitController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.visitcontroller")); Component ownerRepository = webApplication.getComponentWithName("Owner Repository"); assertNotNull(ownerRepository); assertEquals("org.springframework.samples.petclinic.owner.OwnerRepository", ownerRepository.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); + assertSame(ownerRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerrepository")); Component vetRepository = webApplication.getComponentWithName("Vet Repository"); assertNotNull(vetRepository); assertEquals("org.springframework.samples.petclinic.vet.VetRepository", vetRepository.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getUrl()); + assertSame(vetRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetrepository")); assertTrue(welcomeController.getRelationships().isEmpty()); From f6b55e51f37202b2e6a637fa3b878ba7d4097f00 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 24 Aug 2024 15:13:40 +0100 Subject: [PATCH 250/418] More tidy up and tests. --- .../component/ComponentFinderStrategy.java | 6 +- .../ComponentFinderStrategyBuilder.java | 21 +- .../matcher/AbstractTypeMatcher.java | 19 -- .../matcher/AnnotationTypeMatcher.java | 10 +- .../component/matcher/ExtendsTypeMatcher.java | 6 +- .../matcher/ImplementsTypeMatcher.java | 6 +- .../matcher/NameSuffixTypeMatcher.java | 6 +- .../component/matcher/RegexTypeMatcher.java | 6 +- .../component/matcher/TypeMatcher.java | 2 - .../naming/DefaultNamingStrategy.java | 5 + .../DefaultSupportingTypesStrategy.java | 5 + .../visitor/DefaultComponentVisitor.java | 5 + .../matcher/AnnotationTypeMatcherTests.java | 16 +- .../matcher/ExtendsTypeMatcherTests.java | 14 +- .../matcher/ImplementsTypeMatcherTests.java | 14 +- .../matcher/NameSuffixTypeMatcherTests.java | 12 +- .../matcher/RegexSuffixTypeMatcherTests.java | 12 +- .../ComponentFinderStrategyDslContext.java | 1 + .../dsl/ComponentFinderStrategyParser.java | 78 +++++--- .../java/com/structurizr/dsl/DslContext.java | 4 +- .../com/structurizr/dsl/ElementsParser.java | 8 +- .../dsl/ImpliedRelationshipsParser.java | 4 +- .../structurizr/dsl/RelationshipsParser.java | 9 +- .../structurizr/dsl/StructurizrDslParser.java | 11 +- .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../ComponentFinderStrategyParserTests.java | 180 ++++++++++++++++++ .../structurizr/dsl/ElementsParserTests.java | 10 + .../dsl/ImpliedRelationshipsParserTests.java | 7 + .../dsl/RelationshipsParserTests.java | 10 + .../CustomImpliedRelationshipsStrategy.java | 13 ++ .../dsl/example/CustomTypeMatcher.java | 18 ++ .../dsl/spring-petclinic/workspace.dsl | 6 +- 32 files changed, 404 insertions(+), 121 deletions(-) delete mode 100644 structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomImpliedRelationshipsStrategy.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomTypeMatcher.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index 04d499361..91ca7d1e4 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -22,13 +22,15 @@ */ class ComponentFinderStrategy { + private final String technology; private final TypeMatcher typeMatcher; private final TypeFilter typeFilter; private final SupportingTypesStrategy supportingTypesStrategy; private final NamingStrategy namingStrategy; private final ComponentVisitor componentVisitor; - ComponentFinderStrategy(TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy, ComponentVisitor componentVisitor) { + ComponentFinderStrategy(String technology, TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy, ComponentVisitor componentVisitor) { + this.technology = technology; this.typeMatcher = typeMatcher; this.typeFilter = typeFilter; this.supportingTypesStrategy = supportingTypesStrategy; @@ -44,7 +46,7 @@ Set findComponents(TypeRepository typeRepository) { if (typeMatcher.matches(type) && typeFilter.accept(type)) { DiscoveredComponent component = new DiscoveredComponent(namingStrategy.nameOf(type), type); component.setDescription(type.getDescription()); - component.setTechnology(typeMatcher.getTechnology()); + component.setTechnology(this.technology); component.setComponentFinderStrategy(this); components.add(component); diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java index 8027be99f..c2a7ee4a0 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -16,6 +16,7 @@ */ public final class ComponentFinderStrategyBuilder { + private String technology; private TypeMatcher typeMatcher; private TypeFilter typeFilter = new DefaultTypeFilter(); private SupportingTypesStrategy supportingTypesStrategy = new DefaultSupportingTypesStrategy(); @@ -25,6 +26,12 @@ public final class ComponentFinderStrategyBuilder { public ComponentFinderStrategyBuilder() { } + public ComponentFinderStrategyBuilder forTechnology(String technology) { + this.technology = technology; + + return this; + } + public ComponentFinderStrategyBuilder matchedBy(TypeMatcher typeMatcher) { this.typeMatcher = typeMatcher; @@ -60,7 +67,19 @@ public ComponentFinderStrategy build() { throw new RuntimeException("A type matcher must be specified"); } - return new ComponentFinderStrategy(typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, componentVisitor); + return new ComponentFinderStrategy(technology, typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, componentVisitor); + } + + @Override + public String toString() { + return "ComponentFinderStrategyBuilder{" + + "technology='" + technology + '\'' + + ", typeMatcher=" + typeMatcher + + ", typeFilter=" + typeFilter + + ", supportingTypesStrategy=" + supportingTypesStrategy + + ", namingStrategy=" + namingStrategy + + ", componentVisitor=" + componentVisitor + + '}'; } } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java deleted file mode 100644 index f2296d5e5..000000000 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/AbstractTypeMatcher.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.structurizr.component.matcher; - -/** - * A superclass for TypeMatcher implementations. - */ -public abstract class AbstractTypeMatcher implements TypeMatcher { - - private final String technology; - - public AbstractTypeMatcher(String technology) { - this.technology = technology; - } - - @Override - public String getTechnology() { - return technology; - } - -} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java index ceadefbf1..e418ce84c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java @@ -9,13 +9,11 @@ /** * Matches types based upon the presence of a type-level annotation. */ -public class AnnotationTypeMatcher extends AbstractTypeMatcher { +public class AnnotationTypeMatcher implements TypeMatcher { private final String annotationType; - public AnnotationTypeMatcher(String annotationType, String technology) { - super(technology); - + public AnnotationTypeMatcher(String annotationType) { if (StringUtils.isNullOrEmpty(annotationType)) { throw new IllegalArgumentException("An annotation type must be supplied"); } @@ -23,9 +21,7 @@ public AnnotationTypeMatcher(String annotationType, String technology) { this.annotationType = "L" + annotationType.replace(".", "/") + ";"; } - public AnnotationTypeMatcher(Class annotation, String technology) { - super(technology); - + public AnnotationTypeMatcher(Class annotation) { if (annotation == null) { throw new IllegalArgumentException("An annotation must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java index 043ca3cec..81bad21fe 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -13,15 +13,13 @@ /** * Matches types where the type extends the specified class. */ -public class ExtendsTypeMatcher extends AbstractTypeMatcher { +public class ExtendsTypeMatcher implements TypeMatcher { private static final Log log = LogFactory.getLog(ExtendsTypeMatcher.class); private final String className; - public ExtendsTypeMatcher(String className, String technology) { - super(technology); - + public ExtendsTypeMatcher(String className) { if (StringUtils.isNullOrEmpty(className)) { throw new IllegalArgumentException("A fully qualified class name must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java index 1deb3b6ff..4c6c24aaa 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -11,15 +11,13 @@ /** * Matches types where the type implements the specified interface. */ -public class ImplementsTypeMatcher extends AbstractTypeMatcher { +public class ImplementsTypeMatcher implements TypeMatcher { private static final Log log = LogFactory.getLog(ImplementsTypeMatcher.class); private final String interfaceName; - public ImplementsTypeMatcher(String interfaceName, String technology) { - super(technology); - + public ImplementsTypeMatcher(String interfaceName) { if (StringUtils.isNullOrEmpty(interfaceName)) { throw new IllegalArgumentException("A fully qualified interface name must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java index 078a1a511..9f89ca07c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java @@ -6,13 +6,11 @@ /** * Matches types where the name of the type ends with the specified suffix. */ -public class NameSuffixTypeMatcher extends AbstractTypeMatcher { +public class NameSuffixTypeMatcher implements TypeMatcher { private final String suffix; - public NameSuffixTypeMatcher(String suffix, String technology) { - super(technology); - + public NameSuffixTypeMatcher(String suffix) { if (StringUtils.isNullOrEmpty(suffix)) { throw new IllegalArgumentException("A suffix must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java index 0f9c8a8f9..d55583a56 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java @@ -8,13 +8,11 @@ /** * Matches types using a regex against the fully qualified type name. */ -public class RegexTypeMatcher extends AbstractTypeMatcher { +public class RegexTypeMatcher implements TypeMatcher { private final Pattern regex; - public RegexTypeMatcher(String regex, String technology) { - super(technology); - + public RegexTypeMatcher(String regex) { if (StringUtils.isNullOrEmpty(regex)) { throw new IllegalArgumentException("A regex must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java index 572d159d0..4293c937a 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java @@ -9,6 +9,4 @@ public interface TypeMatcher { boolean matches(Type type); - String getTechnology(); - } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java index 622060ca0..61cc11b64 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java @@ -14,4 +14,9 @@ public String nameOf(Type type) { return String.join(" ", parts); } + @Override + public String toString() { + return "DefaultNamingStrategy{}"; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java index 65639a110..134d2cbae 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java @@ -16,4 +16,9 @@ public Set findSupportingTypes(Type type, TypeRepository typeRepository) { return Collections.emptySet(); } + @Override + public String toString() { + return "DefaultSupportingTypesStrategy{}"; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java b/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java index 49b687cf6..054d7e9c9 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java +++ b/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java @@ -11,4 +11,9 @@ public class DefaultComponentVisitor implements ComponentVisitor { public void visit(Component component) { } + @Override + public String toString() { + return "DefaultComponentVisitor{}"; + } + } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java index fc124dfec..6e9e0ce11 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java @@ -13,30 +13,30 @@ public class AnnotationTypeMatcherTests { @Test void construction_ThrowsAnException_WhenPassedANullName() { - assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher((String)null, "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher((String)null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptyName() { - assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher("", "Technology")); - assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher(" ", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher(" ")); } @Test void construction_ThrowsAnException_WhenPassedANullClass() { - assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher((Class) null, "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher((Class) null)); } @Test void matches_ThrowsAnException_WhenPassedNull() { - assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher("com.example.AnnotationName", "Technology").matches(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher("com.example.AnnotationName").matches(null)); } @Test void matches_ReturnsFalse_WhenThereIsNoUnderlyingJavaClass() { Type type = new Type("com.structurizr.component.matcher.annotationTypeMatcher.CustomerController"); - assertFalse(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Controller", "Technology").matches(type)); + assertFalse(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Controller").matches(type)); } @Test @@ -45,7 +45,7 @@ void matches_ReturnsFalse_WhenThereIsNoMatch() throws Exception { ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.class").getAbsolutePath()); Type type = new Type(parser.parse()); - assertFalse(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Repository", "Technology").matches(type)); + assertFalse(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Repository").matches(type)); } @Test @@ -54,7 +54,7 @@ void matches_ReturnsTrue_WhenThereIsAMatch() throws Exception { ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.class").getAbsolutePath()); Type type = new Type(parser.parse()); - assertTrue(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Controller", "Technology").matches(type)); + assertTrue(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Controller").matches(type)); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java index 40b71bcb5..66481383d 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/ExtendsTypeMatcherTests.java @@ -13,25 +13,25 @@ public class ExtendsTypeMatcherTests { @Test void construction_ThrowsAnException_WhenPassedANullName() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher(null, "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptyName() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher("", "Technology")); - assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher(" ", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher(" ")); } @Test void matches_ThrowsAnException_WhenPassedNull() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher("com.example.ClassName", "Technology").matches(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher("com.example.ClassName").matches(null)); } @Test void matches_ReturnsFalse_WhenThereIsNoUnderlyingJavaClass() { Type type = new Type("com.structurizr.component.matcher.extendsTypeMatcher.CustomerController"); - assertFalse(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractController", "Technology").matches(type)); + assertFalse(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractController").matches(type)); } @Test @@ -40,7 +40,7 @@ void matches_ReturnsFalse_WhenThereIsNoMatch() throws Exception { ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.class").getAbsolutePath()); Type type = new Type(parser.parse()); - assertFalse(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractRepository", "Technology").matches(type)); + assertFalse(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractRepository").matches(type)); } @Test @@ -49,7 +49,7 @@ void matches_ReturnsTrue_WhenThereIsAMatch() throws Exception { ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/extendsTypeMatcher/CustomerController.class").getAbsolutePath()); Type type = new Type(parser.parse()); - assertTrue(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractController", "Technology").matches(type)); + assertTrue(new ExtendsTypeMatcher("com.structurizr.component.matcher.extendsTypeMatcher.AbstractController").matches(type)); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java index 4ece7e585..af5acd807 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/ImplementsTypeMatcherTests.java @@ -12,25 +12,25 @@ public class ImplementsTypeMatcherTests { @Test void construction_ThrowsAnException_WhenPassedANullName() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher(null, "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptyName() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher("", "Technology")); - assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher(" ", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher(" ")); } @Test void matches_ThrowsAnException_WhenPassedNull() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher("com.example.InterfaceName", "Technology").matches(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher("com.example.InterfaceName").matches(null)); } @Test void matches_ReturnsFalse_WhenThereIsNoUnderlyingJavaClass() { Type type = new Type("com.structurizr.component.matcher.implementsTypeMatcher.CustomerController"); - assertFalse(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Controller", "Technology").matches(type)); + assertFalse(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Controller").matches(type)); } @Test @@ -39,7 +39,7 @@ void matches_ReturnsFalse_WhenThereIsNoMatch() throws Exception { ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.class").getAbsolutePath()); Type type = new Type(parser.parse()); - assertFalse(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Repository", "Technology").matches(type)); + assertFalse(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Repository").matches(type)); } @Test @@ -48,7 +48,7 @@ void matches_ReturnsTrue_WhenThereIsAMatch() throws Exception { ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/implementsTypeMatcher/CustomerController.class").getAbsolutePath()); Type type = new Type(parser.parse()); - assertTrue(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Controller", "Technology").matches(type)); + assertTrue(new ImplementsTypeMatcher("com.structurizr.component.matcher.implementsTypeMatcher.Controller").matches(type)); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java index 80dff112e..b0055a443 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/NameSuffixTypeMatcherTests.java @@ -9,28 +9,28 @@ public class NameSuffixTypeMatcherTests { @Test void construction_ThrowsAnException_WhenPassedANullSuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher(null, "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("", "Technology")); - assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher(" ", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher(" ")); } @Test void matches_ThrowsAnException_WhenPassedNull() { - assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("Suffix", "Technology").matches(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("Suffix").matches(null)); } @Test void matches_ReturnsFalse_WhenThereIsNoMatch() { - assertFalse(new NameSuffixTypeMatcher("Component", "Technology").matches(new Type("com.example.SomeClass"))); + assertFalse(new NameSuffixTypeMatcher("Component").matches(new Type("com.example.SomeClass"))); } @Test void matches_ReturnsTrue_WhenThereIsAMatch() { - assertTrue(new NameSuffixTypeMatcher("Component", "Technology").matches(new Type("com.example.SomeComponent"))); + assertTrue(new NameSuffixTypeMatcher("Component").matches(new Type("com.example.SomeComponent"))); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java index a54da7b7c..2c757c025 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/RegexSuffixTypeMatcherTests.java @@ -9,28 +9,28 @@ public class RegexSuffixTypeMatcherTests { @Test void construction_ThrowsAnException_WhenPassedANullRegex() { - assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(null, "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptyRegex() { - assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher("", "Technology")); - assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(" ", "Technology")); + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(" ")); } @Test void matches_ThrowsAnException_WhenPassedNull() { - assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(".*Controller", "Technology").matches(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(".*Controller").matches(null)); } @Test void matches_ReturnsFalse_WhenThereIsNoMatch() { - assertFalse(new RegexTypeMatcher(".*Controller", "Technology").matches(new Type("com.example.SomeClass"))); + assertFalse(new RegexTypeMatcher(".*Controller").matches(new Type("com.example.SomeClass"))); } @Test void matches_ReturnsTrue_WhenThereIsAMatch() { - assertTrue(new RegexTypeMatcher(".*Controller", "Technology").matches(new Type("com.example.SomeController"))); + assertTrue(new RegexTypeMatcher(".*Controller").matches(new Type("com.example.SomeController"))); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java index fede404b4..2b2fd405f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java @@ -15,6 +15,7 @@ final class ComponentFinderStrategyDslContext extends DslContext { @Override protected String[] getPermittedTokens() { return new String[] { + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_TECHNOLOGY_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FILTER_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index 32bd19a64..61d89c4b8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -11,76 +11,98 @@ import com.structurizr.component.supporting.AllTypesInPackageSupportingTypesStrategy; import com.structurizr.component.supporting.AllTypesUnderPackageSupportingTypesStrategy; +import java.io.File; +import java.lang.reflect.Constructor; + final class ComponentFinderStrategyParser extends AbstractParser { + private static final String TECHNOLOGY_GRAMMAR = "technology "; + private static final String MATCHER_GRAMMAR = "matcher [parameters]"; + private static final String MATCHER_ANNOTATION_GRAMMAR = "matcher annotation "; + private static final String MATCHER_EXTENDS_GRAMMAR = "matcher extends "; + private static final String MATCHER_IMPLEMENTS_GRAMMAR = "matcher implements "; + private static final String MATCHER_NAMESUFFIX_GRAMMAR = "matcher namesuffix "; + private static final String MATCHER_REGEX_GRAMMAR = "matcher regex "; + private static final String FILTER_GRAMMAR = "filter [parameters]"; private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes [parameters]"; private static final String NAMING_GRAMMAR = "naming "; - void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens) { + void parseTechnology(ComponentFinderStrategyDslContext context, Tokens tokens) { + if (tokens.size() != 2) { + throw new RuntimeException("Expected: " + TECHNOLOGY_GRAMMAR); + } + + String name = tokens.get(1); + context.getComponentFinderStrategyBuilder().forTechnology(name); + } + + void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { if (tokens.size() < 2) { throw new RuntimeException("Too few tokens, expected: " + MATCHER_GRAMMAR); } - String type = tokens.get(1).toLowerCase(); - switch (type) { + String type = tokens.get(1); + switch (type.toLowerCase()) { case "annotation": - if (tokens.size() == 4) { + if (tokens.size() == 3) { String name = tokens.get(2); - String technology = tokens.get(3); - context.getComponentFinderStrategyBuilder().matchedBy(new AnnotationTypeMatcher(name, technology)); + context.getComponentFinderStrategyBuilder().matchedBy(new AnnotationTypeMatcher(name)); } else { - throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + throw new RuntimeException("Expected: " + MATCHER_ANNOTATION_GRAMMAR); } break; case "extends": - if (tokens.size() == 4) { + if (tokens.size() == 3) { String name = tokens.get(2); - String technology = tokens.get(3); - context.getComponentFinderStrategyBuilder().matchedBy(new ExtendsTypeMatcher(name, technology)); + context.getComponentFinderStrategyBuilder().matchedBy(new ExtendsTypeMatcher(name)); } else { - throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + throw new RuntimeException("Expected: " + MATCHER_EXTENDS_GRAMMAR); } break; case "implements": - if (tokens.size() == 4) { + if (tokens.size() == 3) { String name = tokens.get(2); - String technology = tokens.get(3); - context.getComponentFinderStrategyBuilder().matchedBy(new ImplementsTypeMatcher(name, technology)); + context.getComponentFinderStrategyBuilder().matchedBy(new ImplementsTypeMatcher(name)); } else { - throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + throw new RuntimeException("Expected: " + MATCHER_IMPLEMENTS_GRAMMAR); } break; case "namesuffix": - if (tokens.size() == 4) { + if (tokens.size() == 3) { String suffix = tokens.get(2); - String technology = tokens.get(3); - context.getComponentFinderStrategyBuilder().matchedBy(new NameSuffixTypeMatcher(suffix, technology)); + context.getComponentFinderStrategyBuilder().matchedBy(new NameSuffixTypeMatcher(suffix)); } else { - throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + throw new RuntimeException("Expected: " + MATCHER_NAMESUFFIX_GRAMMAR); } break; case "regex": - if (tokens.size() == 4) { + if (tokens.size() == 3) { String regex = tokens.get(2); - String technology = tokens.get(3); - context.getComponentFinderStrategyBuilder().matchedBy(new RegexTypeMatcher(regex, technology)); + context.getComponentFinderStrategyBuilder().matchedBy(new RegexTypeMatcher(regex)); } else { - throw new RuntimeException("Expected: " + MATCHER_GRAMMAR); + throw new RuntimeException("Expected: " + MATCHER_REGEX_GRAMMAR); } break; default: - throw new IllegalArgumentException("Unknown matcher: " + type); + try { + Class typeMatcherClass = context.loadClass(type, dslFile); + Constructor constructor = typeMatcherClass.getDeclaredConstructor(); + TypeMatcher typeMatcher = constructor.newInstance(); + context.getComponentFinderStrategyBuilder().matchedBy(typeMatcher); + } catch (Exception e) { + throw new RuntimeException("Type matcher \"" + type + "\" could not be loaded - " + e.getClass() + ": " + e.getMessage()); + } } } - void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens) { + void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { if (tokens.size() < 2) { throw new RuntimeException("Too few tokens, expected: " + FILTER_GRAMMAR); } @@ -110,7 +132,7 @@ void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens) { } } - void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens tokens) { + void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { if (tokens.size() < 2) { throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_GRAMMAR); } @@ -134,8 +156,8 @@ void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens toke } } - void parseNaming(ComponentFinderStrategyDslContext context, Tokens tokens) { - if (tokens.size() < 1) { + void parseNaming(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { + if (tokens.size() < 2) { throw new RuntimeException("Too few tokens, expected: " + NAMING_GRAMMAR); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java index a5d6f02a2..2711c9138 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java @@ -103,7 +103,7 @@ Relationship getRelationship(String identifier) { return identifiersRegister.getRelationship(identifier.toLowerCase()); } - protected Class loadClass(String fqn, File dslFile) throws Exception { + protected Class loadClass(String fqn, File dslFile) throws Exception { File pluginsDirectory = new File(dslFile.getParent(), PLUGINS_DIRECTORY_NAME); URL[] urls = new URL[0]; @@ -122,7 +122,7 @@ protected Class loadClass(String fqn, File dslFile) throws Exception { } URLClassLoader childClassLoader = new URLClassLoader(urls, getClass().getClassLoader()); - return childClassLoader.loadClass(fqn); + return (Class) childClassLoader.loadClass(fqn); } void end() { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java index 2bfccf11b..f205241ce 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java @@ -22,7 +22,13 @@ Set parse(DslContext context, Tokens tokens) { String expression = tokens.get(1); Set modelItems = new ExpressionParser().parseExpression(expression, context); - return modelItems.stream().filter(mi -> mi instanceof Element).map(mi -> (Element)mi).collect(Collectors.toSet()); + Set elements = modelItems.stream().filter(mi -> mi instanceof Element).map(mi -> (Element)mi).collect(Collectors.toSet()); + + if (elements.isEmpty()) { + throw new RuntimeException("No elements found for expression \"" + expression + "\""); + } + + return elements; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java index 14e781495..b647e5b3d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java @@ -47,8 +47,8 @@ void parse(DslContext context, Tokens tokens, File dslFile, boolean restricted) } try { - Class impliedRelationshipsStrategyClass = context.loadClass(option, dslFile); - Constructor constructor = impliedRelationshipsStrategyClass.getDeclaredConstructor(); + Class impliedRelationshipsStrategyClass = context.loadClass(option, dslFile); + Constructor constructor = impliedRelationshipsStrategyClass.getDeclaredConstructor(); ImpliedRelationshipsStrategy impliedRelationshipsStrategy = (ImpliedRelationshipsStrategy)constructor.newInstance(); context.getWorkspace().getModel().setImpliedRelationshipsStrategy(impliedRelationshipsStrategy); } catch (ClassNotFoundException cnfe) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java index eb0e0cf7b..890a295d6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java @@ -23,7 +23,14 @@ Set parse(DslContext context, Tokens tokens) { String expression = tokens.get(1); Set modelItems = new ExpressionParser().parseExpression(expression, context); - return modelItems.stream().filter(mi -> mi instanceof Relationship).map(mi -> (Relationship)mi).collect(Collectors.toSet()); + Set relationships = modelItems.stream().filter(mi -> mi instanceof Relationship).map(mi -> (Relationship)mi).collect(Collectors.toSet()); + + + if (relationships.isEmpty()) { + throw new RuntimeException("No relationships found for expression \"" + expression + "\""); + } + + return relationships; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index a162091f8..472df90e3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -439,17 +439,20 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr startContext(new ComponentFinderStrategyDslContext(getContext(ComponentFinderDslContext.class).getComponentFinderBuilder())); } + } else if (COMPONENT_FINDER_STRATEGY_TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseTechnology(getContext(ComponentFinderStrategyDslContext.class), tokens); + } else if (COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { - new ComponentFinderStrategyParser().parseMatcher(getContext(ComponentFinderStrategyDslContext.class), tokens); + new ComponentFinderStrategyParser().parseMatcher(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); } else if (COMPONENT_FINDER_STRATEGY_FILTER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { - new ComponentFinderStrategyParser().parseFilter(getContext(ComponentFinderStrategyDslContext.class), tokens); + new ComponentFinderStrategyParser().parseFilter(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); } else if (COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { - new ComponentFinderStrategyParser().parseSupportingTypes(getContext(ComponentFinderStrategyDslContext.class), tokens); + new ComponentFinderStrategyParser().parseSupportingTypes(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); } else if (COMPONENT_FINDER_STRATEGY_NAMING_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { - new ComponentFinderStrategyParser().parseNaming(getContext(ComponentFinderStrategyDslContext.class), tokens); + new ComponentFinderStrategyParser().parseNaming(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { throw new RuntimeException("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group)"); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 1962ec6b0..9fd5fb14f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -117,6 +117,7 @@ class StructurizrDslTokens { static final String COMPONENT_FINDER_CLASSES_TOKEN = "classes"; static final String COMPONENT_FINDER_SOURCE_TOKEN = "source"; static final String COMPONENT_FINDER_STRATEGY_TOKEN = "strategy"; + static final String COMPONENT_FINDER_STRATEGY_TECHNOLOGY_TOKEN = "technology"; static final String COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN = "matcher"; static final String COMPONENT_FINDER_STRATEGY_FILTER_TOKEN = "filter"; static final String COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN = "supportingTypes"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java new file mode 100644 index 000000000..ba92a3351 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -0,0 +1,180 @@ +package com.structurizr.dsl; + +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.component.matcher.AnnotationTypeMatcher; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ComponentFinderStrategyParserTests extends AbstractTests { + + private final ComponentFinderStrategyParser parser = new ComponentFinderStrategyParser(); + private final ComponentFinderBuilder componentFinderBuilder = new ComponentFinderBuilder(); + private final ComponentFinderStrategyDslContext context = new ComponentFinderStrategyDslContext(componentFinderBuilder); + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooFewTokens() { + try { + parser.parseTechnology(context, tokens("technology")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology ", e.getMessage()); + } + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseTechnology(context, tokens("technology", "name", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology ", e.getMessage()); + } + } + + @Test + void test_parseTechnology() { + parser.parseTechnology(context, tokens("technology", "name")); + assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseMatcher(context, tokens("matcher"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: matcher [parameters]", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "annotation"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher annotation ", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsed() { + parser.parseMatcher(context, tokens("matcher", "annotation", "com.example.Component"), null); + assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=AnnotationTypeMatcher{annotationType='Lcom/example/Component;'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "extends"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher extends ", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsed() { + parser.parseMatcher(context, tokens("matcher", "extends", "com.example.Component"), null); + assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=ExtendsTypeMatcher{className='com.example.Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "implements"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher implements ", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsed() { + parser.parseMatcher(context, tokens("matcher", "implements", "com.example.Component"), null); + assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=ImplementsTypeMatcher{interfaceName='com.example.Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "namesuffix"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher namesuffix ", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsed() { + parser.parseMatcher(context, tokens("matcher", "namesuffix", "Component"), null); + assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheRegexTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "regex"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher regex ", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenTheRegexTypeMatcherIsUsed() { + parser.parseMatcher(context, tokens("matcher", "regex", ".*Component"), null); + assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=RegexTypeMatcher{regex=.*Component}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenACustomTypeMatcherIsUsedButCannotBeLoaded() { + try { + parser.parseMatcher(context, tokens("matcher", "com.example.CustomTypeMatcher"), new File(".")); + fail(); + } catch (Exception e) { + assertEquals("Type matcher \"com.example.CustomTypeMatcher\" could not be loaded - class java.lang.ClassNotFoundException: com.example.CustomTypeMatcher", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenACustomTypeMatcherIsUsed() { + parser.parseMatcher(context, tokens("matcher", "com.structurizr.dsl.example.CustomTypeMatcher"), new File(".")); + assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=CustomTypeMatcher{}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseFilter_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseFilter(context, tokens("filter"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseSupportingTypes_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseSupportingTypes(context, tokens("supportingTypes"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: supportingTypes [parameters]", e.getMessage()); + } + } + + @Test + void test_parseNaming_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseNaming(context, tokens("naming"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: naming ", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java index 6e437b972..9eb68b7f8 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java @@ -24,6 +24,16 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { } } + @Test + void test_parse_ThrowsAnException_WhenThereAreNoElementsFound() { + try { + parser.parse(context(), tokens("!elements", "expression")); + fail(); + } catch (Exception e) { + assertEquals("No elements found for expression \"expression\"", e.getMessage()); + } + } + @Test void test_parse_FindsElementsByExpression() { Container application = model.addSoftwareSystem("Software System").addContainer("Application"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java index eb7ded0f6..eae9a875c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java @@ -79,4 +79,11 @@ void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedInUnrestrictedModeBut } } + @Test + void test_parse_SetsTheStrategy_WhenACustomStrategyIsUsedInUnrestrictedMode() { + parser.parse(context(), tokens("!impliedRelationships", "com.structurizr.dsl.example.CustomImpliedRelationshipsStrategy"), new File("."), false); + + assertEquals("com.structurizr.dsl.example.CustomImpliedRelationshipsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java index c3e685ccc..b1f42db8a 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java @@ -22,6 +22,16 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { } } + @Test + void test_parse_ThrowsAnException_WhenThereAreNoRelationshipsFound() { + try { + parser.parse(context(), tokens("!relationships", "expression")); + fail(); + } catch (Exception e) { + assertEquals("No relationships found for expression \"expression\"", e.getMessage()); + } + } + @Test void test_parse_FindsRelationshipsByExpression() { SoftwareSystem a = model.addSoftwareSystem("A"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomImpliedRelationshipsStrategy.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomImpliedRelationshipsStrategy.java new file mode 100644 index 000000000..bb45da945 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomImpliedRelationshipsStrategy.java @@ -0,0 +1,13 @@ +package com.structurizr.dsl.example; + +import com.structurizr.model.ImpliedRelationshipsStrategy; +import com.structurizr.model.Relationship; + +public class CustomImpliedRelationshipsStrategy implements ImpliedRelationshipsStrategy { + + @Override + public void createImpliedRelationships(Relationship relationship) { + + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomTypeMatcher.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomTypeMatcher.java new file mode 100644 index 000000000..6530ae000 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/example/CustomTypeMatcher.java @@ -0,0 +1,18 @@ +package com.structurizr.dsl.example; + +import com.structurizr.component.Type; +import com.structurizr.component.matcher.TypeMatcher; + +public class CustomTypeMatcher implements TypeMatcher { + + @Override + public boolean matches(Type type) { + return false; + } + + @Override + public String toString() { + return "CustomTypeMatcher{}"; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index 279c77f73..f672c28e9 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -23,11 +23,13 @@ classes "${SPRING_PETCLINIC_HOME}/target/spring-petclinic-3.3.0-SNAPSHOT.jar" source "${SPRING_PETCLINIC_HOME}/src/main/java" strategy { - matcher annotation "org.springframework.stereotype.Controller" "Spring MVC Controller" + technology "Spring MVC Controller" + matcher annotation "org.springframework.stereotype.Controller" filter excludeRegex ".*.CrashController" } strategy { - matcher implements "org.springframework.data.repository.Repository" "Spring Data Repository" + technology "Spring Data Repository" + matcher implements "org.springframework.data.repository.Repository" } } From bc93a0f9f658d5a037de2b4248e93a68e5b5480e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 24 Aug 2024 15:51:28 +0100 Subject: [PATCH 251/418] Adds support for custom type matchers with a parameter. --- .../dsl/ComponentFinderStrategyParser.java | 11 +++++++++-- .../dsl/ComponentFinderStrategyParserTests.java | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index 61d89c4b8..8f2f7daf9 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -93,8 +93,15 @@ void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File default: try { Class typeMatcherClass = context.loadClass(type, dslFile); - Constructor constructor = typeMatcherClass.getDeclaredConstructor(); - TypeMatcher typeMatcher = constructor.newInstance(); + + TypeMatcher typeMatcher; + if (tokens.size() == 3) { + String parameter = tokens.get(2); + typeMatcher = typeMatcherClass.getDeclaredConstructor(String.class).newInstance(parameter); + } else { + typeMatcher = typeMatcherClass.getDeclaredConstructor().newInstance(); + } + context.getComponentFinderStrategyBuilder().matchedBy(typeMatcher); } catch (Exception e) { throw new RuntimeException("Type matcher \"" + type + "\" could not be loaded - " + e.getClass() + ": " + e.getMessage()); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index ba92a3351..ad95614e9 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -2,6 +2,7 @@ import com.structurizr.component.ComponentFinderBuilder; import com.structurizr.component.matcher.AnnotationTypeMatcher; +import com.structurizr.component.matcher.NameSuffixTypeMatcher; import org.junit.jupiter.api.Test; import java.io.File; @@ -142,11 +143,17 @@ void test_parseMatcher_WhenACustomTypeMatcherIsUsedButCannotBeLoaded() { } @Test - void test_parseMatcher_WhenACustomTypeMatcherIsUsed() { + void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithoutParameters() { parser.parseMatcher(context, tokens("matcher", "com.structurizr.dsl.example.CustomTypeMatcher"), new File(".")); assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=CustomTypeMatcher{}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); } + @Test + void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithAParameter() { + parser.parseMatcher(context, tokens("matcher", NameSuffixTypeMatcher.class.getCanonicalName(), "Component"), new File(".")); + assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + } + @Test void test_parseFilter_ThrowsAnException_WhenNoTypeIsSpecified() { try { From 8d5751150a4af172fe82366eca3a61df5712675b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 25 Aug 2024 11:25:30 +0100 Subject: [PATCH 252/418] More tidy up ... also provides a way to iterate over each discovered component in the DSL. --- .../ComponentFinderStrategyBuilder.java | 13 +++--- .../component/DiscoveredComponent.java | 7 +++ .../ComponentFinderStrategyDslContext.java | 3 +- ...ponentFinderStrategyForEachDslContext.java | 30 +++++++++++++ .../dsl/ComponentFinderStrategyParser.java | 3 +- .../structurizr/dsl/StructurizrDslParser.java | 45 +++++++++++-------- .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../java/com/structurizr/dsl/DslTests.java | 28 +++++++++--- .../dsl/spring-petclinic/workspace.dsl | 20 ++++----- 9 files changed, 104 insertions(+), 46 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java index c2a7ee4a0..88adf2efe 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -4,7 +4,6 @@ import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.matcher.TypeMatcher; import com.structurizr.component.naming.DefaultNamingStrategy; -import com.structurizr.component.naming.SimpleNamingStrategy; import com.structurizr.component.naming.NamingStrategy; import com.structurizr.component.supporting.DefaultSupportingTypesStrategy; import com.structurizr.component.supporting.SupportingTypesStrategy; @@ -26,12 +25,6 @@ public final class ComponentFinderStrategyBuilder { public ComponentFinderStrategyBuilder() { } - public ComponentFinderStrategyBuilder forTechnology(String technology) { - this.technology = technology; - - return this; - } - public ComponentFinderStrategyBuilder matchedBy(TypeMatcher typeMatcher) { this.typeMatcher = typeMatcher; @@ -56,6 +49,12 @@ public ComponentFinderStrategyBuilder namedBy(NamingStrategy namingStrategy) { return this; } + public ComponentFinderStrategyBuilder asTechnology(String technology) { + this.technology = technology; + + return this; + } + public ComponentFinderStrategyBuilder forEach(ComponentVisitor componentVisitor) { this.componentVisitor = componentVisitor; diff --git a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java index 8eddb2e08..79bba4d51 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java +++ b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java @@ -77,4 +77,11 @@ void setComponentFinderStrategy(ComponentFinderStrategy componentFinderStrategy) this.componentFinderStrategy = componentFinderStrategy; } + @Override + public String toString() { + return "DiscoveredComponent{" + + "name='" + name + '\'' + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java index 2b2fd405f..762d4c383 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java @@ -19,7 +19,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FILTER_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN, - StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_NAMING_TOKEN + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_NAMING_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java new file mode 100644 index 000000000..ff2db4158 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java @@ -0,0 +1,30 @@ +package com.structurizr.dsl; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +final class ComponentFinderStrategyForEachDslContext extends DslContext { + + private final List dslLines = new ArrayList<>(); + + ComponentFinderStrategyForEachDslContext(ComponentFinderStrategyDslContext dslContext, StructurizrDslParser dslParser) { + dslContext.getComponentFinderStrategyBuilder().forEach(component -> { + try { + dslParser.parse(dslLines, new ComponentDslContext(component)); + } catch (StructurizrDslParserException e) { + throw new RuntimeException(e); + } + }); + } + + void addLine(String line) { + this.dslLines.add(line); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] {}; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index 8f2f7daf9..030fc3ff3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -12,7 +12,6 @@ import com.structurizr.component.supporting.AllTypesUnderPackageSupportingTypesStrategy; import java.io.File; -import java.lang.reflect.Constructor; final class ComponentFinderStrategyParser extends AbstractParser { @@ -35,7 +34,7 @@ void parseTechnology(ComponentFinderStrategyDslContext context, Tokens tokens) { } String name = tokens.get(1); - context.getComponentFinderStrategyBuilder().forTechnology(name); + context.getComponentFinderStrategyBuilder().asTechnology(name); } void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 472df90e3..6efeb2db6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -108,14 +108,7 @@ public Workspace getWorkspace() { } private String getParsedDsl() { - StringBuilder buf = new StringBuilder(); - - for (String line : dslSourceLines) { - buf.append(line); - buf.append(System.lineSeparator()); - } - - return buf.toString(); + return String.join(System.lineSeparator(), dslSourceLines); } void parse(DslParserContext context, File path) throws StructurizrDslParserException { @@ -140,7 +133,7 @@ public void parse(File dslFile) throws StructurizrDslParserException { } try { - parse(Files.readAllLines(dslFile.toPath(), characterEncoding), dslFile, false); + parse(Files.readAllLines(dslFile.toPath(), characterEncoding), dslFile, false, true); } catch (IOException e) { throw new StructurizrDslParserException(e.getMessage()); } @@ -175,7 +168,13 @@ public void parse(String dsl, File dslFile) throws StructurizrDslParserException } List lines = Arrays.asList(dsl.split("\\r?\\n")); - parse(lines, dslFile, false); + parse(lines, dslFile, false, true); + } + + void parse(List lines, DslContext dslContext) throws StructurizrDslParserException { + startContext(dslContext); + parse(lines, null, true, false); + endContext(); } /** @@ -185,13 +184,12 @@ public void parse(String dsl, File dslFile) throws StructurizrDslParserException * @param dslFile a File representing the DSL file, and therefore where includes/images/etc should be loaded relative to * @throws StructurizrDslParserException when something goes wrong */ - void parse(List lines, File dslFile, boolean include) throws StructurizrDslParserException { + void parse(List lines, File dslFile, boolean fragment, boolean includeInDslSourceLines) throws StructurizrDslParserException { List dslLines = preProcessLines(lines); for (DslLine dslLine : dslLines) { - boolean includeInDslSourceLines = true; - String line = dslLine.getSource(); + String lineForDslSource = line; if (line.startsWith(BOM)) { // this caters for files encoded as "UTF-8 with BOM" @@ -255,12 +253,13 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr paddedLines.add(leadingSpace + unpaddedLine); } - parse(paddedLines, includedFile.getFile(), true); + parse(paddedLines, includedFile.getFile(), true, true); } - - includeInDslSourceLines = false; } + // include the !include in the parser DSL as: # !include ... + lineForDslSource = null; + } else if (PLUGIN_TOKEN.equalsIgnoreCase(firstToken)) { if (!restricted) { String fullyQualifiedClassName = new PluginParser().parse(getContext(), tokens.withoutContextStartToken()); @@ -454,6 +453,14 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } else if (COMPONENT_FINDER_STRATEGY_NAMING_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { new ComponentFinderStrategyParser().parseNaming(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); + } else if (COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + if (shouldStartContext(tokens)) { + startContext(new ComponentFinderStrategyForEachDslContext(getContext(ComponentFinderStrategyDslContext.class), this)); + } + + } else if (inContext(ComponentFinderStrategyForEachDslContext.class)) { + getContext(ComponentFinderStrategyForEachDslContext.class).addLine(line); + } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { throw new RuntimeException("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group)"); @@ -998,8 +1005,8 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } } - if (includeInDslSourceLines) { - dslSourceLines.add(line); + if (includeInDslSourceLines && lineForDslSource != null) { + dslSourceLines.add(lineForDslSource); } } catch (Exception e) { if (e.getMessage() != null) { @@ -1010,7 +1017,7 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } } - if (!include && !contextStack.empty()) { + if (!fragment && !contextStack.empty()) { throw new StructurizrDslParserException("Unexpected end of DSL content - are one or more closing curly braces missing?"); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 9fd5fb14f..5e26d24df 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -122,5 +122,6 @@ class StructurizrDslTokens { static final String COMPONENT_FINDER_STRATEGY_FILTER_TOKEN = "filter"; static final String COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN = "supportingTypes"; static final String COMPONENT_FINDER_STRATEGY_NAMING_TOKEN = "naming"; + static final String COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN = "forEach"; } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 981a5f5ae..f3db3bb16 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -332,7 +332,7 @@ void test_includeLocalFile() throws Exception { " }\n" + " }\n" + "\n" + - "}\n", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + "}", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); } @Test @@ -380,7 +380,7 @@ void test_includeLocalDirectory() throws Exception { " }\n" + " }\n" + "\n" + - "}\n", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + "}", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); } @Test @@ -415,7 +415,7 @@ void test_includeUrl() throws Exception { " }\n" + " }\n" + "\n" + - "}\n", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + "}", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); } @Test @@ -1150,10 +1150,16 @@ void springPetClinic() throws Exception { System.out.println(springPetClinicHome); if (!StringUtils.isNullOrEmpty(springPetClinicHome)) { System.out.println("Running Spring PetClinic example..."); + + File workspaceFile = new File("src/test/resources/dsl/spring-petclinic/workspace.dsl"); StructurizrDslParser parser = new StructurizrDslParser(); - parser.parse(new File("src/test/resources/dsl/spring-petclinic/workspace.dsl")); + parser.parse(workspaceFile); + + Person clinicEmployee = (Person)parser.getIdentifiersRegister().getElement("clinicEmployee"); Container webApplication = (Container)parser.getIdentifiersRegister().getElement("springPetClinic.webApplication"); + Container relationalDatabaseSchema = (Container)parser.getIdentifiersRegister().getElement("springPetClinic.relationalDatabaseSchema"); + assertEquals(7, webApplication.getComponents().size()); Component welcomeController = webApplication.getComponentWithName("Welcome Controller"); @@ -1162,6 +1168,7 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java").getAbsolutePath(), welcomeController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getUrl()); assertSame(welcomeController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.welcomecontroller")); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(welcomeController)); Component ownerController = webApplication.getComponentWithName("Owner Controller"); assertNotNull(ownerController); @@ -1169,6 +1176,7 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java").getAbsolutePath(), ownerController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getUrl()); assertSame(ownerController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerController")); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(ownerController)); Component petController = webApplication.getComponentWithName("Pet Controller"); assertNotNull(petController); @@ -1176,6 +1184,7 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/PetController.java").getAbsolutePath(), petController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetController.java", petController.getUrl()); assertSame(petController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.petcontroller")); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(petController)); Component vetController = webApplication.getComponentWithName("Vet Controller"); assertNotNull(vetController); @@ -1183,6 +1192,7 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetController.java").getAbsolutePath(), vetController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetController.java", vetController.getUrl()); assertSame(vetController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetcontroller")); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(vetController)); Component visitController = webApplication.getComponentWithName("Visit Controller"); assertNotNull(visitController); @@ -1190,6 +1200,7 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/VisitController.java").getAbsolutePath(), visitController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java", visitController.getUrl()); assertSame(visitController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.visitcontroller")); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(visitController)); Component ownerRepository = webApplication.getComponentWithName("Owner Repository"); assertNotNull(ownerRepository); @@ -1197,6 +1208,7 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); assertSame(ownerRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerrepository")); + assertTrue(ownerRepository.hasEfferentRelationshipWith(relationalDatabaseSchema)); Component vetRepository = webApplication.getComponentWithName("Vet Repository"); assertNotNull(vetRepository); @@ -1204,14 +1216,18 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getUrl()); assertSame(vetRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetrepository")); + assertTrue(vetRepository.hasEfferentRelationshipWith(relationalDatabaseSchema)); assertTrue(welcomeController.getRelationships().isEmpty()); - assertNotNull(petController.getEfferentRelationshipWith(ownerRepository)); assertNotNull(visitController.getEfferentRelationshipWith(ownerRepository)); assertNotNull(ownerController.getEfferentRelationshipWith(ownerRepository)); - assertNotNull(vetController.getEfferentRelationshipWith(vetRepository)); + + // this checks that the component forEach { ... } lines don't get repeated in the outputted DSL source + String content = Files.readString(workspaceFile.toPath()); + assertEquals(content, new String(Base64.getDecoder().decode(parser.getWorkspace().getProperties().get("structurizr.dsl")))); + } else { System.out.println("Skipping Spring PetClinic example..."); } diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index f672c28e9..abf7e451a 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -1,4 +1,4 @@ - workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (https://github.com/spring-projects/spring-petclinic/)" { +workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (https://github.com/spring-projects/spring-petclinic/)" { // this example requires an environment variable as follows: // - Name: SPRING_PETCLINIC_HOME @@ -26,26 +26,24 @@ technology "Spring MVC Controller" matcher annotation "org.springframework.stereotype.Controller" filter excludeRegex ".*.CrashController" + forEach { + clinicEmployee -> this "Uses" + tag "Spring MVC Controller" + } } strategy { technology "Spring Data Repository" matcher implements "org.springframework.data.repository.Repository" + forEach { + -> relationalDatabaseSchema "Reads from and writes to" + tag "Spring Data Repository" + } } } !script groovy { element.components.each { it.url = it.properties["component.src"].replace(System.getenv("SPRING_PETCLINIC_HOME") + "/src/main/java", "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java") } } - - !elements "element.parent==webApplication && element.technology==Spring MVC Controller" { - clinicEmployee -> this "Uses" - tag "Spring MVC Controller" - } - - !elements "element.parent==webApplication && element.technology==Spring Data Repository" { - -> relationalDatabaseSchema "Reads from and writes to" - tag "Spring Data Repository" - } } } } From 2ed0ccaa624e25a06475593a285f45e637813530 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 26 Aug 2024 13:00:44 +0100 Subject: [PATCH 253/418] Initial commit of a utility to load a Structurizr workspace into a Neo4j database. --- settings.gradle | 3 +- structurizr-neo4j/README.md | 5 +++ structurizr-neo4j/build.gradle | 9 +++++ .../com/structurizr/neo4j/SimpleLoader.java | 36 +++++++++++++++++++ .../test/java/com/structurizr/Example.java | 28 +++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 structurizr-neo4j/README.md create mode 100644 structurizr-neo4j/build.gradle create mode 100644 structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java create mode 100644 structurizr-neo4j/src/test/java/com/structurizr/Example.java diff --git a/settings.gradle b/settings.gradle index 57df82f6d..8035bdd02 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,5 @@ include 'structurizr-core' include 'structurizr-dsl' include 'structurizr-export' include 'structurizr-import' -include 'structurizr-inspection' \ No newline at end of file +include 'structurizr-inspection' +include 'structurizr-neo4j' \ No newline at end of file diff --git a/structurizr-neo4j/README.md b/structurizr-neo4j/README.md new file mode 100644 index 000000000..c01eb1a5e --- /dev/null +++ b/structurizr-neo4j/README.md @@ -0,0 +1,5 @@ +# structurizr-neo4j + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-neo4j.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-neo4j) + +This library provides utilities to import a Structurizr workspace into Neo4j. \ No newline at end of file diff --git a/structurizr-neo4j/build.gradle b/structurizr-neo4j/build.gradle new file mode 100644 index 000000000..2c06e792e --- /dev/null +++ b/structurizr-neo4j/build.gradle @@ -0,0 +1,9 @@ +dependencies { + + api project(':structurizr-core') + implementation 'org.neo4j.driver:neo4j-java-driver:5.23.0' + + testImplementation project(':structurizr-client') + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + +} \ No newline at end of file diff --git a/structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java b/structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java new file mode 100644 index 000000000..727b06c5e --- /dev/null +++ b/structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java @@ -0,0 +1,36 @@ +package com.structurizr.neo4j; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; +import org.neo4j.driver.Driver; +import org.neo4j.driver.SessionConfig; + +public class SimpleLoader { + + public void load(Workspace workspace, Driver driver, String database) { + try (var session = driver.session(SessionConfig.builder().withDatabase(database).build())) { + for (Element element : workspace.getModel().getElements()) { + session.run(String.format( + "CREATE ( :Element { id: '%s', name: \"%s\", type: \"%s\" })", + element.getId(), element.getName(), element.getClass().getSimpleName().toLowerCase() + )); + } + + session.run("CREATE INDEX element_index FOR (n:Element) ON (n.id)"); + + for (Relationship relationship : workspace.getModel().getRelationships()) { + session.run(String.format( + """ + MATCH ( from:Element { id: '%s' } ), ( to:Element { id: '%s' } ) + CREATE (from)-[:HAS_RELATIONSHIP_WITH {role: '%s'}]->(to)""", + relationship.getSource().getId(), + relationship.getDestination().getId(), + !StringUtils.isNullOrEmpty(relationship.getDescription()) ? relationship.getDescription() : "uses" + )); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-neo4j/src/test/java/com/structurizr/Example.java b/structurizr-neo4j/src/test/java/com/structurizr/Example.java new file mode 100644 index 000000000..4ef9f5290 --- /dev/null +++ b/structurizr-neo4j/src/test/java/com/structurizr/Example.java @@ -0,0 +1,28 @@ +package com.structurizr; + +import com.structurizr.neo4j.SimpleLoader; +import com.structurizr.util.WorkspaceUtils; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Result; + +import java.io.File; + +public class Example { + + public static void main(String[] args) throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("workspace.json")); + + try (Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "password"))) { + try (var session = driver.session()) { + session.run("DROP DATABASE structurizr IF EXISTS"); + Result result = session.run("CREATE DATABASE structurizr"); + System.out.println(result.consume()); + } + + new SimpleLoader().load(workspace, driver, "structurizr"); + } + } + +} \ No newline at end of file From 2a8602dfe0d3b23c2eedd3c4406fa380670c0924 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 30 Aug 2024 10:57:21 +0100 Subject: [PATCH 254/418] Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). --- changelog.md | 1 + .../java/com/structurizr/view/ThemeUtils.java | 102 +++++++++++++----- .../com/structurizr/util/ThemeUtilsTests.java | 35 ++++++ .../src/test/resources/logo.png | Bin 0 -> 9262 bytes .../src/test/resources/theme.json | 12 +++ .../java/com/structurizr/view/Styles.java | 26 +++++ .../com/structurizr/view/StylesTests.java | 17 +++ .../structurizr/dsl/StructurizrDslParser.java | 4 +- .../java/com/structurizr/dsl/ThemeParser.java | 51 ++++++--- .../com/structurizr/dsl/ThemeParserTests.java | 61 ++++++++--- .../src/test/resources/themes/theme.json | 11 ++ 11 files changed, 262 insertions(+), 58 deletions(-) create mode 100644 structurizr-client/src/test/java/com/structurizr/util/ThemeUtilsTests.java create mode 100644 structurizr-client/src/test/resources/logo.png create mode 100644 structurizr-client/src/test/resources/theme.json create mode 100644 structurizr-dsl/src/test/resources/themes/theme.json diff --git a/changelog.md b/changelog.md index 16c2ac577..ed1e98deb 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ - structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression. - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. - structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. +- structurizr-dsl: Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). ## 2.2.0 (2nd July 2024) diff --git a/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java index fff6ef9ed..283ec0f3a 100644 --- a/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java +++ b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java @@ -6,17 +6,22 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.structurizr.Workspace; import com.structurizr.io.WorkspaceWriterException; +import com.structurizr.model.Relationship; +import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; +import com.structurizr.util.Url; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; import org.apache.hc.core5.http.io.entity.EntityUtils; import java.io.*; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.concurrent.TimeUnit; /** @@ -65,7 +70,7 @@ public static String toJson(Workspace workspace) throws Exception { } /** - * Loads (and inlines) the element and relationship styles from the themes defined in the workspace, into the workspace itself. + * Loads the element and relationship styles from the themes defined in the workspace, into the workspace itself. * This implementation simply copies the styles from all themes into the workspace. * This uses a default timeout value of 10000ms. * @@ -77,7 +82,7 @@ public static void loadThemes(Workspace workspace) throws Exception { } /** - * Loads (and inlines) the element and relationship styles from the themes defined in the workspace, into the workspace itself. + * Loads the element and relationship styles from the themes defined in the workspace, into the workspace itself. * This implementation simply copies the styles from all themes into the workspace. * * @param workspace a Workspace object @@ -85,32 +90,11 @@ public static void loadThemes(Workspace workspace) throws Exception { * @throws Exception if something goes wrong */ public static void loadThemes(Workspace workspace, int timeoutInMilliseconds) throws Exception { - for (String url : workspace.getViews().getConfiguration().getThemes()) { - ConnectionConfig connectionConfig = ConnectionConfig.custom() - .setConnectTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) - .setSocketTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) - .build(); - - BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); - cm.setConnectionConfig(connectionConfig); - - CloseableHttpClient httpClient = HttpClientBuilder.create() - .useSystemProperties() - .setConnectionManager(cm) - .build(); - - HttpGet httpGet = new HttpGet(url); - - CloseableHttpResponse response = httpClient.execute(httpGet); - if (response.getCode() == HTTP_OK_STATUS) { - String json = EntityUtils.toString(response.getEntity()); - - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - Theme theme = objectMapper.readValue(json, Theme.class); - String baseUrl = url.substring(0, url.lastIndexOf('/') + 1); + for (String themeLocation : workspace.getViews().getConfiguration().getThemes()) { + if (Url.isUrl(themeLocation)) { + String json = loadFrom(themeLocation, timeoutInMilliseconds); + Theme theme = fromJson(json); + String baseUrl = themeLocation.substring(0, themeLocation.lastIndexOf('/') + 1); for (ElementStyle elementStyle : theme.getElements()) { String icon = elementStyle.getIcon(); @@ -128,9 +112,69 @@ public static void loadThemes(Workspace workspace, int timeoutInMilliseconds) th workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(theme); } + } + } - httpClient.close(); + /** + * Inlines the element and relationship styles from the specified file, adding the styles into the workspace + * and overriding any properties already set. + * + * @param workspace the Workspace to load the theme into + * @param file a File object representing a theme (a JSON file) + * @throws Exception if something goes wrong + */ + public static void inlineTheme(Workspace workspace, File file) throws Exception { + String json = Files.readString(file.toPath()); + Theme theme = fromJson(json); + + for (ElementStyle elementStyle : theme.getElements()) { + String icon = elementStyle.getIcon(); + if (!StringUtils.isNullOrEmpty(icon)) { + if (icon.startsWith("http")) { + // okay, image served over HTTP + } else if (icon.startsWith("data:image")) { + // also okay, data URI + } else { + // convert the relative icon filename into a data URI + elementStyle.setIcon(ImageUtils.getImageAsDataUri(new File(file.getParentFile(), icon))); + } + } } + + workspace.getViews().getConfiguration().getStyles().inlineTheme(theme); + } + + private static String loadFrom(String url, int timeoutInMilliseconds) throws Exception { + ConnectionConfig connectionConfig = ConnectionConfig.custom() + .setConnectTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) + .setSocketTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) + .build(); + + BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); + cm.setConnectionConfig(connectionConfig); + + try (CloseableHttpClient httpClient = HttpClientBuilder.create() + .useSystemProperties() + .setConnectionManager(cm) + .build()) { + + HttpGet httpGet = new HttpGet(url); + + CloseableHttpResponse response = httpClient.execute(httpGet); + if (response.getCode() == HTTP_OK_STATUS) { + return EntityUtils.toString(response.getEntity()); + } + } + + return ""; + } + + private static Theme fromJson(String json) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + return objectMapper.readValue(json, Theme.class); } private static void write(Workspace workspace, Writer writer) throws Exception { diff --git a/structurizr-client/src/test/java/com/structurizr/util/ThemeUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/util/ThemeUtilsTests.java new file mode 100644 index 000000000..03c6cfbcf --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/util/ThemeUtilsTests.java @@ -0,0 +1,35 @@ +package com.structurizr.util; + +import com.structurizr.Workspace; +import com.structurizr.view.ThemeUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ThemeUtilsTests { + + @Test + void inlineTheme() throws Exception { + File themeFile = new File("src/test/resources/theme.json"); + + try { + Workspace theme = new Workspace("Theme", ""); + theme.getViews().getConfiguration().getStyles().addElementStyle("Tag").background("#ff0000").icon("logo.png"); + theme.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag").color("#00ff00"); + ThemeUtils.toJson(theme, themeFile); + } catch (Exception e) { + throw new RuntimeException(e); + } + + Workspace workspace = new Workspace("Name", "Description"); + ThemeUtils.inlineTheme(workspace, themeFile); + + assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); + assertEquals("#ff0000", workspace.getViews().getConfiguration().getStyles().getElementStyle("Tag").getBackground()); + assertEquals("data:image/png;base64,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", workspace.getViews().getConfiguration().getStyles().getElementStyle("Tag").getIcon()); + assertEquals("#00ff00", workspace.getViews().getConfiguration().getStyles().getRelationshipStyle("Tag").getColor()); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/logo.png b/structurizr-client/src/test/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..763d19bf5c7d827bec4f6c31ff8e8660f09aab54 GIT binary patch literal 9262 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 lines, File dslFile, boolean fragment, boolean includeIn new DynamicViewRelationshipParser().parseUrl(getContext(DynamicViewRelationshipContext.class), tokens.withoutContextStartToken()); } else if (THEME_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { - new ThemeParser().parseTheme(getContext(), tokens); + new ThemeParser().parseTheme(getContext(), dslFile, tokens); } else if (THEMES_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { - new ThemeParser().parseThemes(getContext(), tokens); + new ThemeParser().parseThemes(getContext(), dslFile, tokens); } else if (TERMINOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { startContext(new TerminologyDslContext()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java index 9cc5286af..c16c531c5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java @@ -1,41 +1,66 @@ package com.structurizr.dsl; +import com.structurizr.util.Url; +import com.structurizr.view.ThemeUtils; + +import java.io.File; + final class ThemeParser extends AbstractParser { + private static final String DEFAULT_THEME_NAME = "default"; private static final String DEFAULT_THEME_URL = "https://static.structurizr.com/themes/default/theme.json"; private final static int FIRST_THEME_INDEX = 1; - void parseTheme(DslContext context, Tokens tokens) { - // theme + void parseTheme(DslContext context, File dslFile, Tokens tokens) { + // theme if (tokens.hasMoreThan(FIRST_THEME_INDEX)) { - throw new RuntimeException("Too many tokens, expected: theme "); + throw new RuntimeException("Too many tokens, expected: theme "); } if (!tokens.includes(FIRST_THEME_INDEX)) { - throw new RuntimeException("Expected: theme "); + throw new RuntimeException("Expected: theme "); } - addTheme(context, tokens.get(FIRST_THEME_INDEX)); + addTheme(context, dslFile, tokens.get(FIRST_THEME_INDEX)); } - void parseThemes(DslContext context, Tokens tokens) { - // themes [url] ... [url] + void parseThemes(DslContext context, File dslFile, Tokens tokens) { + // themes [url|file] ... [url|file] if (!tokens.includes(FIRST_THEME_INDEX)) { - throw new RuntimeException("Expected: themes [url] ... [url]"); + throw new RuntimeException("Expected: themes [url|file] ... [url|file]"); } for (int i = FIRST_THEME_INDEX; i < tokens.size(); i++) { - addTheme(context, tokens.get(i)); + addTheme(context, dslFile, tokens.get(i)); } } - private void addTheme(DslContext context, String url) { - if ("default".equalsIgnoreCase(url)) { - url = DEFAULT_THEME_URL; + private void addTheme(DslContext context, File dslFile, String theme) { + if (DEFAULT_THEME_NAME.equalsIgnoreCase(theme)) { + theme = DEFAULT_THEME_URL; } - context.getWorkspace().getViews().getConfiguration().addTheme(url); + if (Url.isUrl(theme)) { + // this adds the theme to the list of theme URLs in the workspace + context.getWorkspace().getViews().getConfiguration().addTheme(theme); + } else { + // this inlines the file-based theme into the workspace + File file = new File(dslFile.getParentFile(), theme); + if (file.exists()) { + if (file.isFile()) { + try { + ThemeUtils.inlineTheme(context.getWorkspace(), file); + } catch (Exception e) { + throw new RuntimeException("Error loading theme from " + file.getAbsolutePath() + ": " + e.getMessage()); + } + } else { + throw new RuntimeException(file.getAbsolutePath() + " is not a file"); + } + } else { + throw new RuntimeException(file.getAbsolutePath() + " does not exist"); + } + } } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java index 46463d603..3a3c9312b 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java @@ -2,36 +2,37 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; class ThemeParserTests extends AbstractTests { - private ThemeParser parser = new ThemeParser(); + private final ThemeParser parser = new ThemeParser(); @Test void test_parseTheme_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parseTheme(context(), tokens("theme", "url", "extra")); + parser.parseTheme(context(), null, tokens("theme", "url", "extra")); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: theme ", e.getMessage()); + assertEquals("Too many tokens, expected: theme ", e.getMessage()); } } @Test void test_parseTheme_ThrowsAnException_WhenNoThemeIsSpecified() { try { - parser.parseTheme(context(), tokens("theme")); + parser.parseTheme(context(), null, tokens("theme")); fail(); } catch (Exception e) { - assertEquals("Expected: theme ", e.getMessage()); + assertEquals("Expected: theme ", e.getMessage()); } } @Test void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { - parser.parseTheme(context(), tokens("theme", "http://example.com/1")); + parser.parseTheme(context(), null, tokens("theme", "http://example.com/1")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -39,7 +40,7 @@ void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { @Test void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { - parser.parseTheme(context(), tokens("theme", "default")); + parser.parseTheme(context(), null, tokens("theme", "default")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); @@ -48,16 +49,16 @@ void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { @Test void test_parseThemes_ThrowsAnException_WhenNoThemesAreSpecified() { try { - parser.parseThemes(context(), tokens("themes")); + parser.parseThemes(context(), null, tokens("themes")); fail(); } catch (Exception e) { - assertEquals("Expected: themes [url] ... [url]", e.getMessage()); + assertEquals("Expected: themes [url|file] ... [url|file]", e.getMessage()); } } @Test void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { - parser.parseThemes(context(), tokens("themes", "http://example.com/1")); + parser.parseThemes(context(), null, tokens("themes", "http://example.com/1")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -65,7 +66,7 @@ void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { @Test void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { - parser.parseThemes(context(), tokens("themes", "http://example.com/1", "http://example.com/2", "http://example.com/3")); + parser.parseThemes(context(), null, tokens("themes", "http://example.com/1", "http://example.com/2", "http://example.com/3")); assertEquals(3, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -75,10 +76,42 @@ void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { @Test void test_parseThemes_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { - parser.parseThemes(context(), tokens("themes", "default")); + parser.parseThemes(context(), null, tokens("themes", "default")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); } + @Test + void test_parseTheme_ThrowsAnException_WhenTheThemeFileDoesNotExist() { + File dslFile = new File("src/test/resources/themes/workspace.dsl"); + try { + parser.parseTheme(context(), dslFile, tokens("theme", "my-theme.json")); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().endsWith("/src/test/resources/themes/my-theme.json does not exist")); + } + } + + @Test + void test_parseTheme_ThrowsAnException_WhenTheThemeFileIsADirectory() { + File dslFile = new File("src/test/resources/workspace.dsl"); + try { + parser.parseTheme(context(), dslFile, tokens("theme", "themes")); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().endsWith("/src/test/resources/themes is not a file")); + } + } + + @Test + void test_parseTheme_InlinesTheTheme_WhenAThemeFileIsSpecified() { + File dslFile = new File("src/test/resources/themes/workspace.dsl"); + parser.parseTheme(context(), dslFile, tokens("theme", "theme.json")); + + assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); + assertEquals("#ff0000", workspace.getViews().getConfiguration().getStyles().getElementStyle("Tag").getBackground()); + assertEquals("#00ff00", workspace.getViews().getConfiguration().getStyles().getRelationshipStyle("Tag").getColor()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/themes/theme.json b/structurizr-dsl/src/test/resources/themes/theme.json new file mode 100644 index 000000000..4f9774996 --- /dev/null +++ b/structurizr-dsl/src/test/resources/themes/theme.json @@ -0,0 +1,11 @@ +{ + "name" : "Theme", + "elements" : [ { + "tag" : "Tag", + "background" : "#ff0000" + } ], + "relationships" : [ { + "tag" : "Tag", + "color" : "#00ff00" + } ] +} \ No newline at end of file From a6e531fefe72bc212439a7a5483af48d654af184 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 1 Sep 2024 09:55:36 +0100 Subject: [PATCH 255/418] Adds support for icons to the Ilograph exporter (#332). --- .../structurizr/export/ilograph/IlographExporter.java | 11 +++++++++++ .../com/structurizr/export/ilograph/54915.ilograph | 8 ++++++++ ...aphWriterTests.java => IlographExporterTests.java} | 9 ++++----- 3 files changed, 23 insertions(+), 5 deletions(-) rename structurizr-export/src/test/java/com/structurizr/export/ilograph/{IlographWriterTests.java => IlographExporterTests.java} (90%) diff --git a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java index b091d40dc..38e225582 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java @@ -16,6 +16,8 @@ */ public class IlographExporter extends AbstractWorkspaceExporter { + public static final String ILOGRAPH_ICON = "ilograph.icon"; + public WorkspaceExport export(Workspace workspace) { IndentingWriter writer = new IndentingWriter(); writer.writeLine("resources:"); @@ -216,6 +218,15 @@ private void writeElement(IndentingWriter writer, Workspace workspace, Element e writer.writeLine(String.format("backgroundColor: \"%s\"", elementStyle.getBackground())); } writer.writeLine(String.format("color: \"%s\"", elementStyle.getColor())); + + String icon = elementStyle.getProperties().get(ILOGRAPH_ICON); + if (StringUtils.isNullOrEmpty(icon)) { + icon = elementStyle.getIcon(); + } + if (!StringUtils.isNullOrEmpty(icon)) { + writer.writeLine(String.format("icon: \"%s\"", icon)); + } + writer.writeLine(); writer.outdent(); } diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph index 9689909c5..13ee0a0bb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph @@ -27,6 +27,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#232f3e" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png" children: - id: "6" @@ -34,6 +35,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#147eba" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png" children: - id: "12" @@ -41,6 +43,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#3b48cc" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png" children: - id: "13" @@ -48,6 +51,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#3b48cc" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png" children: - id: "14" @@ -62,6 +66,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#cc2264" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png" children: - id: "10" @@ -69,6 +74,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#d86613" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png" children: - id: "11" @@ -84,6 +90,7 @@ resources: description: "Highly available and scalable cloud DNS service." backgroundColor: "#ffffff" color: "#693cc5" + icon: "AWS/Networking/Route-53.svg" - id: "8" name: "Elastic Load Balancer" @@ -91,6 +98,7 @@ resources: description: "Automatically distributes incoming application traffic." backgroundColor: "#ffffff" color: "#693cc5" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png" perspectives: - name: Static Structure diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographWriterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java similarity index 90% rename from structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographWriterTests.java rename to structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java index 3cca4626d..346b687b2 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographWriterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -2,21 +2,18 @@ import com.structurizr.Workspace; import com.structurizr.export.AbstractExporterTests; -import com.structurizr.export.Diagram; import com.structurizr.export.WorkspaceExport; -import com.structurizr.export.dot.DOTExporter; import com.structurizr.model.CustomElement; import com.structurizr.model.Model; import com.structurizr.util.WorkspaceUtils; -import com.structurizr.view.CustomView; import com.structurizr.view.ThemeUtils; import org.junit.jupiter.api.Test; import java.io.File; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class IlographWriterTests extends AbstractExporterTests { +public class IlographExporterTests extends AbstractExporterTests { @Test public void test_BigBankPlcExample() throws Exception { @@ -31,6 +28,8 @@ public void test_BigBankPlcExample() throws Exception { @Test public void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Amazon Web Services - Route 53").addProperty(IlographExporter.ILOGRAPH_ICON, "AWS/Networking/Route-53.svg"); + ThemeUtils.loadThemes(workspace); IlographExporter ilographExporter = new IlographExporter(); WorkspaceExport export = ilographExporter.export(workspace); From 5de236b248d34211296d26b332235f0aaa362072 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 1 Sep 2024 09:55:46 +0100 Subject: [PATCH 256/418] . --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index ed1e98deb..355ec1b0a 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. - structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. - structurizr-dsl: Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). +- structurizr-export: Adds support for icons to the Ilograph exporter. ## 2.2.0 (2nd July 2024) From 2a4eea01261eb2b360a1e10e899ccc2f6088ea2d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 2 Sep 2024 10:45:28 +0100 Subject: [PATCH 257/418] Removes empty line. --- .../java/com/structurizr/export/ilograph/IlographExporter.java | 1 - .../src/test/java/com/structurizr/export/ilograph/36141.ilograph | 1 - .../src/test/java/com/structurizr/export/ilograph/54915.ilograph | 1 - .../com/structurizr/export/ilograph/IlographExporterTests.java | 1 - 4 files changed, 4 deletions(-) diff --git a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java index 38e225582..420631d8d 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java @@ -21,7 +21,6 @@ public class IlographExporter extends AbstractWorkspaceExporter { public WorkspaceExport export(Workspace workspace) { IndentingWriter writer = new IndentingWriter(); writer.writeLine("resources:"); - writer.writeLine(); writer.indent(); Model model = workspace.getModel(); diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph index 31f0a6f7e..6a416a4d3 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph @@ -1,5 +1,4 @@ resources: - - id: "1" name: "Personal Banking Customer" subtitle: "[Person]" diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph index 13ee0a0bb..411ac3c5e 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph @@ -1,5 +1,4 @@ resources: - - id: "1" name: "Spring PetClinic" subtitle: "[Software System]" diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java index 346b687b2..323d74200 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -49,7 +49,6 @@ public void test_renderCustomElements() throws Exception { WorkspaceExport export = new IlographExporter().export(workspace); assertEquals("resources:\n" + - "\n" + " - id: \"1\"\n" + " name: \"A\"\n" + " subtitle: \"\"\n" + From 8dbdd7897bac3aa77c18f29cb9132fab49f4dcd6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 2 Sep 2024 10:47:31 +0100 Subject: [PATCH 258/418] Adds support for imports to the Ilograph exporter (#332). --- changelog.md | 3 +- .../export/ilograph/IlographExporter.java | 31 +++++++++++++++++++ .../ilograph/IlographExporterTests.java | 23 ++++++++++++-- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 355ec1b0a..695191c84 100644 --- a/changelog.md +++ b/changelog.md @@ -12,7 +12,8 @@ - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. - structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. - structurizr-dsl: Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). -- structurizr-export: Adds support for icons to the Ilograph exporter. +- structurizr-export: Adds support for icons to the Ilograph exporter (https://github.com/structurizr/java/issues/332). +- structurizr-export: Adds support for imports to the Ilograph exporter (https://github.com/structurizr/java/issues/332). ## 2.2.0 (2nd July 2024) diff --git a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java index 420631d8d..06aefe67d 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java @@ -16,10 +16,41 @@ */ public class IlographExporter extends AbstractWorkspaceExporter { + public static final String ILOGRAPH_IMPORTS = "ilograph.imports"; public static final String ILOGRAPH_ICON = "ilograph.icon"; public WorkspaceExport export(Workspace workspace) { IndentingWriter writer = new IndentingWriter(); + + // Ilograph imports can be specified in the form: + // + // AWS:ilograph/aws + // + // Which gets exported as: + // + // imports: + // - from: ilograph/aws + // namespace: AWS + String commaSeparatedListOfImports = workspace.getProperties().get(ILOGRAPH_IMPORTS); + if (!StringUtils.isNullOrEmpty(commaSeparatedListOfImports)) { + writer.writeLine("imports:"); + + String[] ilographImports = commaSeparatedListOfImports.split(","); + for (String ilographImport : ilographImports) { + String[] parts = ilographImport.split(":"); + if (parts.length == 2) { + String namespace = parts[0]; + String from = parts[1]; + + writer.writeLine("- from: " + from); + writer.indent(); + writer.writeLine("namespace: " + namespace); + writer.outdent(); + } + } + writer.writeLine(); + } + writer.writeLine("resources:"); writer.indent(); diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java index 323d74200..a42da19bb 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -26,7 +26,7 @@ public void test_BigBankPlcExample() throws Exception { } @Test - public void test_AmazonWebServicesExample() throws Exception { + void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); workspace.getViews().getConfiguration().getStyles().addElementStyle("Amazon Web Services - Route 53").addProperty(IlographExporter.ILOGRAPH_ICON, "AWS/Networking/Route-53.svg"); @@ -39,7 +39,7 @@ public void test_AmazonWebServicesExample() throws Exception { } @Test - public void test_renderCustomElements() throws Exception { + void test_renderCustomElements() { Workspace workspace = new Workspace("Name", "Description"); Model model = workspace.getModel(); @@ -71,4 +71,23 @@ public void test_renderCustomElements() throws Exception { " color: \"#707070\"\n", export.getDefinition()); } + @Test + void test_imports() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.addProperty(IlographExporter.ILOGRAPH_IMPORTS, "NAMESPACE1:path1,NAMESPACE2:path2"); + + WorkspaceExport export = new IlographExporter().export(workspace); + assertEquals(""" +imports: +- from: path1 + namespace: NAMESPACE1 +- from: path2 + namespace: NAMESPACE2 + +resources: +perspectives: + - name: Static Structure + relations:""", export.getDefinition()); + } + } \ No newline at end of file From 30da167b7cf720d062cbc2391fc9468e412c19d9 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 9 Sep 2024 14:36:08 +0100 Subject: [PATCH 259/418] Adds support for workspace branches (on-premises installation only). --- .../structurizr/api/WorkspaceApiClient.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java index d12adf3a0..159a8fffd 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -32,7 +32,6 @@ import java.text.SimpleDateFormat; import java.util.Base64; import java.util.Date; -import java.util.Properties; /** * A client for the Structurizr workspace API that allows you to get and put Structurizr workspaces in a JSON format. @@ -45,6 +44,7 @@ public class WorkspaceApiClient extends AbstractApiClient { private String apiKey; private String apiSecret; + private String branch = ""; private EncryptionStrategy encryptionStrategy; @@ -111,6 +111,14 @@ protected void setApiSecret(String apiSecret) { this.apiSecret = apiSecret; } + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + /** * Gets the location where a copy of the workspace is archived when it is retrieved from the server. * @@ -224,7 +232,14 @@ public Workspace getWorkspace(long workspaceId) throws StructurizrClientExceptio try (CloseableHttpClient httpClient = HttpClients.createSystem()) { log.info("Getting workspace with ID " + workspaceId); - HttpGet httpGet = new HttpGet(url + WORKSPACE_PATH + "/" + workspaceId); + + HttpGet httpGet; + if (StringUtils.isNullOrEmpty(branch)) { + httpGet = new HttpGet(url + WORKSPACE_PATH + "/" + workspaceId); + } else { + httpGet = new HttpGet(url + WORKSPACE_PATH + "/" + workspaceId + "/branch/" + branch); + } + addHeaders(httpGet, "", ""); debugRequest(httpGet, null); @@ -296,7 +311,12 @@ public void putWorkspace(long workspaceId, Workspace workspace) throws Structuri workspace.setLastModifiedAgent(agent); workspace.setLastModifiedUser(getUser()); - HttpPut httpPut = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId); + HttpPut httpPut; + if (StringUtils.isNullOrEmpty(branch)) { + httpPut = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId); + } else { + httpPut = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId + "/branch/" + branch); + } StringWriter stringWriter = new StringWriter(); if (encryptionStrategy == null) { From be6b7205c5af76c0596112fdbbcb82eec24dd48d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 9 Sep 2024 14:38:03 +0100 Subject: [PATCH 260/418] Update changelog. --- build.gradle | 2 +- changelog.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1f96c1f5e..127bc7d21 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '2.3.0' + version = '3.0.0' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index 695191c84..51173a070 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,8 @@ # Changelog -## unreleased +## 3.0.0 (unreleased) +- structurizr-client: Adds support for [workspace branches](https://docs.structurizr.com/onpremises/workspace-branches) (on-premises installation only). - structurizr-core: Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316). - structurizr-component: Initial rewrite of the original `structurizr-analysis` library - provides a way to automatically find components in a Java codebase. - structurizr-dsl: Adds name-value properties to dynamic view relationship views. From 3fdab62d530cf4bd3d566ebbb6bf48c98a4f557b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 9 Sep 2024 14:43:39 +0100 Subject: [PATCH 261/418] Removes deprecated `!constant` keyword. --- changelog.md | 1 + .../com/structurizr/dsl/StructurizrDslParser.java | 8 +------- .../src/test/java/com/structurizr/dsl/DslTests.java | 13 +++++++++++++ structurizr-dsl/src/test/resources/dsl/constant.dsl | 5 +++++ 4 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/constant.dsl diff --git a/changelog.md b/changelog.md index 51173a070..e5aec5043 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ - structurizr-client: Adds support for [workspace branches](https://docs.structurizr.com/onpremises/workspace-branches) (on-premises installation only). - structurizr-core: Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316). - structurizr-component: Initial rewrite of the original `structurizr-analysis` library - provides a way to automatically find components in a Java codebase. +- structurizr-dsl: Removes deprecated `!constant` keyword. - structurizr-dsl: Adds name-value properties to dynamic view relationship views. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 4a1be28fc..c0ec6c095 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -950,13 +950,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } } else if (CONSTANT_TOKEN.equalsIgnoreCase(firstToken)) { - log.warn("!constant has been deprecated and will be removed in a future release - please use !const or !var instead"); - NameValuePair nameValuePair = new NameValueParser().parseConstant(tokens); - - if (constantsAndVariables.containsKey(nameValuePair.getName())) { - log.warn("A constant \"" + nameValuePair.getName() + "\" already exists"); - } - constantsAndVariables.put(nameValuePair.getName(), nameValuePair); + throw new RuntimeException("!constant was previously deprecated, and has now been removed - please use !const or !var instead"); } else if (CONST_TOKEN.equalsIgnoreCase(firstToken)) { NameValuePair nameValuePair = new NameValueParser().parseConstant(tokens); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index f3db3bb16..b5bc86418 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1096,6 +1096,19 @@ void test_Enterprise() { } } + @Test + void test_Constant() { + File dslFile = new File("src/test/resources/dsl/constant.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("!constant was previously deprecated, and has now been removed - please use !const or !var instead at line 3 of " + dslFile.getAbsolutePath() + ": !constant NAME VALUE", e.getMessage()); + } + } + @Test void test_UnbalancedCurlyBraces() { try { diff --git a/structurizr-dsl/src/test/resources/dsl/constant.dsl b/structurizr-dsl/src/test/resources/dsl/constant.dsl new file mode 100644 index 000000000..714ce0d27 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/constant.dsl @@ -0,0 +1,5 @@ +workspace { + + !constant NAME VALUE + +} \ No newline at end of file From 43ec4c8c64045d19d9246a482c61bfbf395a1abf Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 11 Sep 2024 09:55:25 +0100 Subject: [PATCH 262/418] Better error messages for image views. --- .../dsl/ImageViewContentParser.java | 8 +++ .../java/com/structurizr/dsl/DslTests.java | 14 ++++ .../dsl/ImageViewContentParserTests.java | 64 +++++++++++++++++++ .../src/test/resources/dsl/image-view.dsl | 9 +++ 4 files changed, 95 insertions(+) create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java create mode 100644 structurizr-dsl/src/test/resources/dsl/image-view.dsl diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index 424a0b53f..f59f5fcb5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -52,6 +52,8 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { if (!restricted) { File file = new File(dslFile.getParentFile(), source); new PlantUMLImporter().importDiagram(context.getView(), file); + } else { + throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); } } } catch (Exception e) { @@ -84,6 +86,8 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { if (!restricted) { File file = new File(dslFile.getParentFile(), source); new MermaidImporter().importDiagram(context.getView(), file); + } else { + throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); } } } catch (Exception e) { @@ -117,6 +121,8 @@ void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) { if (!restricted) { File file = new File(dslFile.getParentFile(), source); new KrokiImporter().importDiagram(context.getView(), format, file); + } else { + throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode"); } } } catch (Exception e) { @@ -149,6 +155,8 @@ void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { File file = new File(dslFile.getParentFile(), source); context.getView().setContent(ImageUtils.getImageAsDataUri(file)); context.getView().setTitle(file.getName()); + } else { + throw new RuntimeException("Images must be specified as a URL when running in restricted mode"); } } } catch (Exception e) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index b5bc86418..5e3f112b9 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1254,4 +1254,18 @@ void test_bulkOperations() throws Exception { parser.parse(dslFile); } + @Test + void test_ImageView_WhenParserIsInRestrictedMode() { + File dslFile = new File("src/test/resources/dsl/image-view.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Images must be specified as a URL when running in restricted mode at line 5 of " + dslFile.getAbsolutePath() + ": image image.png", e.getMessage()); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java new file mode 100644 index 000000000..4e693973e --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -0,0 +1,64 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ImageView; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ImageViewContentParserTests extends AbstractTests { + + private ImageViewContentParser parser; + private ImageView imageView; + + @BeforeEach + void setUp() { + imageView = workspace.getViews().createImageView("key"); + } + + @Test + void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { + try { + parser = new ImageViewContentParser(true); + parser.parsePlantUML(new ImageViewDslContext(imageView), null, tokens("plantuml", "image.puml")); + fail(); + } catch (Exception e) { + assertEquals("PlantUML source must be specified as a URL when running in restricted mode", e.getMessage()); + } + } + + @Test + void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { + try { + parser = new ImageViewContentParser(true); + parser.parseMermaid(new ImageViewDslContext(imageView), null, tokens("mermaid", "image.puml")); + fail(); + } catch (Exception e) { + assertEquals("Mermaid source must be specified as a URL when running in restricted mode", e.getMessage()); + } + } + + @Test + void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { + try { + parser = new ImageViewContentParser(true); + parser.parseKroki(new ImageViewDslContext(imageView), null, tokens("kroki", "plantuml", "image.puml")); + fail(); + } catch (Exception e) { + assertEquals("Kroki source must be specified as a URL when running in restricted mode", e.getMessage()); + } + } + + @Test + void test_parseImage_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { + try { + parser = new ImageViewContentParser(true); + parser.parseImage(new ImageViewDslContext(imageView), null, tokens("image", "image.png")); + fail(); + } catch (Exception e) { + assertEquals("Images must be specified as a URL when running in restricted mode", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/image-view.dsl b/structurizr-dsl/src/test/resources/dsl/image-view.dsl new file mode 100644 index 000000000..27633c053 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-view.dsl @@ -0,0 +1,9 @@ +workspace { + + views { + image * "Image" { + image image.png + } + } + +} \ No newline at end of file From 9c5da2cbbec024c76feb39a4972ad9630a51346a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 11 Sep 2024 09:55:45 +0100 Subject: [PATCH 263/418] Removes revision property. --- .../encryption/EncryptedWorkspace.java | 1 - .../com/structurizr/AbstractWorkspace.java | 19 ------------------- 2 files changed, 20 deletions(-) diff --git a/structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java index ba89d409e..7a62ebb94 100644 --- a/structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java +++ b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java @@ -55,7 +55,6 @@ private void init(Workspace workspace, String plaintext, EncryptionStrategy encr setName(workspace.getName()); setDescription(workspace.getDescription()); setVersion(workspace.getVersion()); - setRevision(workspace.getRevision()); setLastModifiedUser(workspace.getLastModifiedUser()); setLastModifiedAgent(workspace.getLastModifiedAgent()); diff --git a/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java index 6bb57bbcd..755cfa1bb 100644 --- a/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java +++ b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java @@ -17,7 +17,6 @@ public abstract class AbstractWorkspace implements PropertyHolder { private String name; private String description; private String version; - private Long revision; private Date lastModifiedDate; private String lastModifiedUser; private String lastModifiedAgent; @@ -111,24 +110,6 @@ public void setVersion(String version) { } - /** - * Gets the revision number of this workspace. - * - * @return the revision number - */ - public Long getRevision() { - return revision; - } - - /** - * Sets the revision number of this workspace. - * - * @param revision a number - */ - public void setRevision(Long revision) { - this.revision = revision; - } - /** * Gets the last modified date of this workspace. * From aef27cc876bb8c4c88e2d8c5ce151e56c90a1d94 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 11 Sep 2024 12:02:22 +0100 Subject: [PATCH 264/418] Workspace branches are also supported by the upcoming cloud service release (paid feature). --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index e5aec5043..61fcddab9 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,7 @@ ## 3.0.0 (unreleased) -- structurizr-client: Adds support for [workspace branches](https://docs.structurizr.com/onpremises/workspace-branches) (on-premises installation only). +- structurizr-client: Adds support to get/put workspace branches on the [cloud service](https://docs.structurizr.com/cloud/workspace-branches) and [on-premises installation](https://docs.structurizr.com/onpremises/workspace-branches). - structurizr-core: Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316). - structurizr-component: Initial rewrite of the original `structurizr-analysis` library - provides a way to automatically find components in a Java codebase. - structurizr-dsl: Removes deprecated `!constant` keyword. From 87b6ff5b0e9427040c511d2fd4f389289aebc6b6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 11 Sep 2024 12:02:56 +0100 Subject: [PATCH 265/418] Adds an example and test for the new dynamic view parallel sequences syntax. --- .../java/com/structurizr/dsl/DslTests.java | 15 ++++++++++ .../src/test/resources/dsl/parallel3.dsl | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 structurizr-dsl/src/test/resources/dsl/parallel3.dsl diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 5e3f112b9..1d3665732 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -555,6 +555,21 @@ void test_parallel2() throws Exception { assertEquals("3", relationships.get(3).getOrder()); } + @Test + void test_parallel3() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/parallel3.dsl")); + + assertFalse(parser.getWorkspace().isEmpty()); + DynamicView view = parser.getWorkspace().getViews().getDynamicViews().iterator().next(); + List relationships = new ArrayList<>(view.getRelationships()); + assertEquals(4, relationships.size()); + assertEquals("1", relationships.get(0).getOrder()); + assertEquals("2", relationships.get(1).getOrder()); + assertEquals("2", relationships.get(2).getOrder()); + assertEquals("3", relationships.get(3).getOrder()); + } + @Test void test_groups() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); diff --git a/structurizr-dsl/src/test/resources/dsl/parallel3.dsl b/structurizr-dsl/src/test/resources/dsl/parallel3.dsl new file mode 100644 index 000000000..0fafca38e --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/parallel3.dsl @@ -0,0 +1,28 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + c = softwareSystem "C" + d = softwareSystem "D" + e = softwareSystem "E" + + a -> b + b -> c + b -> d + b -> e + } + + views { + + dynamic * { + 1: a -> b "Makes a request to" + 2: b -> c "Gets data from" + 2: b -> d "Gets data from" + 3: b -> e "Sends data to" + + autoLayout + } + } + +} \ No newline at end of file From 09d7f89c1ac16ffe4e49e0bb5984456e3551ed1e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 11 Sep 2024 13:04:11 +0100 Subject: [PATCH 266/418] . --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 61fcddab9..0af4373a5 100644 --- a/changelog.md +++ b/changelog.md @@ -9,7 +9,7 @@ - structurizr-dsl: Adds name-value properties to dynamic view relationship views. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. -- structurizr-dsl: Adds support for element technology expressions (e.g. "element.technology==Java"). +- structurizr-dsl: Adds support for element technology expressions (e.g. `element.technology==Java` and `element.technology!=Java`). - structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression. - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. - structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. From becf089d56d5d8cee34f1329c4df79dcad982338 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 13 Sep 2024 18:43:34 +0100 Subject: [PATCH 267/418] A few tweaks to the component finder strategy, and how it's exposed via the DSL. --- .../component/ComponentFinderStrategy.java | 8 +- .../ComponentFinderStrategyBuilder.java | 77 +++++++++++- .../component/matcher/RegexTypeMatcher.java | 2 +- .../ComponentFinderStrategyBuilderTests.java | 119 +++++++++++++++++- .../dsl/ComponentFinderStrategyParser.java | 95 ++++++++------ .../ComponentFinderStrategyParserTests.java | 70 ++++++++--- .../dsl/spring-petclinic/workspace.dsl | 2 +- 7 files changed, 305 insertions(+), 68 deletions(-) diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index 91ca7d1e4..3d5886f2c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -20,7 +20,7 @@ * * Use the {@link ComponentFinderStrategyBuilder} to create an instance of this class. */ -class ComponentFinderStrategy { +public final class ComponentFinderStrategy { private final String technology; private final TypeMatcher typeMatcher; @@ -66,8 +66,12 @@ void visit(Component component) { @Override public String toString() { return "ComponentFinderStrategy{" + - "typeMatcher=" + typeMatcher + + "technology=" + (technology == null ? null : "'" + technology + "'") + + ", typeMatcher=" + typeMatcher + ", typeFilter=" + typeFilter + + ", supportingTypesStrategy=" + supportingTypesStrategy + + ", namingStrategy=" + namingStrategy + + ", componentVisitor=" + componentVisitor + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java index 88adf2efe..a8dcbb9f2 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -9,6 +9,7 @@ import com.structurizr.component.supporting.SupportingTypesStrategy; import com.structurizr.component.visitor.ComponentVisitor; import com.structurizr.component.visitor.DefaultComponentVisitor; +import com.structurizr.util.StringUtils; /** * Provides a way to create a {@link ComponentFinderStrategy} instance. @@ -17,45 +18,93 @@ public final class ComponentFinderStrategyBuilder { private String technology; private TypeMatcher typeMatcher; - private TypeFilter typeFilter = new DefaultTypeFilter(); - private SupportingTypesStrategy supportingTypesStrategy = new DefaultSupportingTypesStrategy(); - private NamingStrategy namingStrategy = new DefaultNamingStrategy(); - private ComponentVisitor componentVisitor = new DefaultComponentVisitor(); + private TypeFilter typeFilter; + private SupportingTypesStrategy supportingTypesStrategy; + private NamingStrategy namingStrategy; + private ComponentVisitor componentVisitor; public ComponentFinderStrategyBuilder() { } public ComponentFinderStrategyBuilder matchedBy(TypeMatcher typeMatcher) { + if (typeMatcher == null) { + throw new IllegalArgumentException("A type matcher must be provided"); + } + + if (this.typeMatcher != null) { + throw new IllegalArgumentException("A type matcher has already been configured"); + } + this.typeMatcher = typeMatcher; return this; } public ComponentFinderStrategyBuilder filteredBy(TypeFilter typeFilter) { + if (typeFilter == null) { + throw new IllegalArgumentException("A type filter must be provided"); + } + + if (this.typeFilter != null) { + throw new IllegalArgumentException("A type filter has already been configured"); + } + this.typeFilter = typeFilter; return this; } public ComponentFinderStrategyBuilder supportedBy(SupportingTypesStrategy supportingTypesStrategy) { + if (supportingTypesStrategy == null) { + throw new IllegalArgumentException("A supporting types strategy must be provided"); + } + + if (this.supportingTypesStrategy != null) { + throw new IllegalArgumentException("A supporting types strategy has already been configured"); + } + this.supportingTypesStrategy = supportingTypesStrategy; return this; } public ComponentFinderStrategyBuilder namedBy(NamingStrategy namingStrategy) { + if (namingStrategy == null) { + throw new IllegalArgumentException("A naming strategy must be provided"); + } + + if (this.namingStrategy != null) { + throw new IllegalArgumentException("A naming strategy has already been configured"); + } + this.namingStrategy = namingStrategy; return this; } public ComponentFinderStrategyBuilder asTechnology(String technology) { + if (StringUtils.isNullOrEmpty(technology)) { + throw new IllegalArgumentException("A technology must be provided"); + } + + if (!StringUtils.isNullOrEmpty(this.technology)) { + throw new IllegalArgumentException("A technology has already been configured"); + } + this.technology = technology; return this; } public ComponentFinderStrategyBuilder forEach(ComponentVisitor componentVisitor) { + if (componentVisitor == null) { + throw new IllegalArgumentException("A component visitor must be provided"); + } + + if (this.componentVisitor != null) { + throw new IllegalArgumentException("A component visitor has already been configured"); + } + this.componentVisitor = componentVisitor; return this; @@ -63,7 +112,23 @@ public ComponentFinderStrategyBuilder forEach(ComponentVisitor componentVisitor) public ComponentFinderStrategy build() { if (typeMatcher == null) { - throw new RuntimeException("A type matcher must be specified"); + throw new RuntimeException("A type matcher must be provided"); + } + + if (typeFilter == null) { + typeFilter = new DefaultTypeFilter(); + } + + if (supportingTypesStrategy == null) { + supportingTypesStrategy = new DefaultSupportingTypesStrategy(); + } + + if (namingStrategy == null) { + namingStrategy = new DefaultNamingStrategy(); + } + + if (componentVisitor == null) { + componentVisitor = new DefaultComponentVisitor(); } return new ComponentFinderStrategy(technology, typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, componentVisitor); @@ -72,7 +137,7 @@ public ComponentFinderStrategy build() { @Override public String toString() { return "ComponentFinderStrategyBuilder{" + - "technology='" + technology + '\'' + + "technology=" + (technology == null ? null : "'" + technology + "'") + ", typeMatcher=" + typeMatcher + ", typeFilter=" + typeFilter + ", supportingTypesStrategy=" + supportingTypesStrategy + diff --git a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java index d55583a56..12bc4a586 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java @@ -32,7 +32,7 @@ public boolean matches(Type type) { @Override public String toString() { return "RegexTypeMatcher{" + - "regex=" + regex + + "regex='" + regex + "'" + '}'; } diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java index 6b578022f..764a304a0 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -1,8 +1,15 @@ package com.structurizr.component; +import com.structurizr.component.filter.ExcludeTypesByRegexFilter; +import com.structurizr.component.filter.IncludeTypesByRegexFilter; +import com.structurizr.component.matcher.NameSuffixTypeMatcher; +import com.structurizr.component.naming.FullyQualifiedNamingStrategy; +import com.structurizr.component.naming.SimpleNamingStrategy; +import com.structurizr.component.supporting.AllTypesInPackageSupportingTypesStrategy; +import com.structurizr.component.supporting.AllTypesUnderPackageSupportingTypesStrategy; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.*; public class ComponentFinderStrategyBuilderTests { @@ -11,4 +18,114 @@ void build_ThrowsAnException_WhenATypeMatcherHasNotBeenConfigured() { assertThrowsExactly(RuntimeException.class, () -> new ComponentFinderStrategyBuilder().build()); } + @Test + void matchedBy_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().matchedBy(null); + fail(); + } catch (Exception e) { + assertEquals("A type matcher must be provided", e.getMessage()); + } + } + + @Test + void matchedBy_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().matchedBy(new NameSuffixTypeMatcher("X")).matchedBy(new NameSuffixTypeMatcher("Y")); + fail(); + } catch (Exception e) { + assertEquals("A type matcher has already been configured", e.getMessage()); + } + } + + @Test + void filteredBy_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().filteredBy(null); + fail(); + } catch (Exception e) { + assertEquals("A type filter must be provided", e.getMessage()); + } + } + + @Test + void filteredBy_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(".*")).filteredBy(new ExcludeTypesByRegexFilter(".*")); + fail(); + } catch (Exception e) { + assertEquals("A type filter has already been configured", e.getMessage()); + } + } + + @Test + void supportedBy_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().supportedBy(null); + fail(); + } catch (Exception e) { + assertEquals("A supporting types strategy must be provided", e.getMessage()); + } + } + + @Test + void supportedBy_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().supportedBy(new AllTypesInPackageSupportingTypesStrategy()).supportedBy(new AllTypesUnderPackageSupportingTypesStrategy()); + fail(); + } catch (Exception e) { + assertEquals("A supporting types strategy has already been configured", e.getMessage()); + } + } + + @Test + void namedBy_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().namedBy(null); + fail(); + } catch (Exception e) { + assertEquals("A naming strategy must be provided", e.getMessage()); + } + } + + @Test + void namedBy_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().namedBy(new SimpleNamingStrategy()).namedBy(new FullyQualifiedNamingStrategy()); + fail(); + } catch (Exception e) { + assertEquals("A naming strategy has already been configured", e.getMessage()); + } + } + + @Test + void asTechnology_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().asTechnology(null); + fail(); + } catch (Exception e) { + assertEquals("A technology must be provided", e.getMessage()); + } + } + + @Test + void asTechnology_ThrowsAnException_WhenPassedAnEmptyString() { + try { + new ComponentFinderStrategyBuilder().asTechnology(""); + fail(); + } catch (Exception e) { + assertEquals("A technology must be provided", e.getMessage()); + } + } + + @Test + void asTechnology_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().asTechnology("X").asTechnology("Y"); + fail(); + } catch (Exception e) { + assertEquals("A technology has already been configured", e.getMessage()); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index 030fc3ff3..bb345bc40 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -6,27 +6,43 @@ import com.structurizr.component.naming.DefaultPackageNamingStrategy; import com.structurizr.component.naming.SimpleNamingStrategy; import com.structurizr.component.naming.FullyQualifiedNamingStrategy; -import com.structurizr.component.supporting.AllReferencedTypesInPackageSupportingTypesStrategy; -import com.structurizr.component.supporting.AllReferencedTypesSupportingTypesStrategy; -import com.structurizr.component.supporting.AllTypesInPackageSupportingTypesStrategy; -import com.structurizr.component.supporting.AllTypesUnderPackageSupportingTypesStrategy; +import com.structurizr.component.supporting.*; import java.io.File; +import java.util.List; final class ComponentFinderStrategyParser extends AbstractParser { private static final String TECHNOLOGY_GRAMMAR = "technology "; - private static final String MATCHER_GRAMMAR = "matcher [parameters]"; + private static final String MATCHER_ANNOTATION = "annotation"; + private static final String MATCHER_EXTENDS = "extends"; + private static final String MATCHER_IMPLEMENTS = "implements"; + private static final String MATCHER_NAME_SUFFIX = "name-suffix"; + private static final String MATCHER_FQN_REGEX = "fqn-regex"; + private static final String MATCHER_GRAMMAR = "matcher <" + String.join("|", List.of(MATCHER_ANNOTATION, MATCHER_EXTENDS, MATCHER_IMPLEMENTS, MATCHER_NAME_SUFFIX, MATCHER_FQN_REGEX)) + "> [parameters]"; private static final String MATCHER_ANNOTATION_GRAMMAR = "matcher annotation "; private static final String MATCHER_EXTENDS_GRAMMAR = "matcher extends "; private static final String MATCHER_IMPLEMENTS_GRAMMAR = "matcher implements "; - private static final String MATCHER_NAMESUFFIX_GRAMMAR = "matcher namesuffix "; - private static final String MATCHER_REGEX_GRAMMAR = "matcher regex "; - - private static final String FILTER_GRAMMAR = "filter [parameters]"; - private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes [parameters]"; - private static final String NAMING_GRAMMAR = "naming "; + private static final String MATCHER_NAMESUFFIX_GRAMMAR = "matcher name-suffix "; + private static final String MATCHER_REGEX_GRAMMAR = "matcher fqn-regex "; + + private static final String FILTER_INCLUDE = "include"; + private static final String FILTER_EXCLUDE = "exclude"; + private static final String FILTER_FQN_REGEX = "fqn-regex"; + private static final String FILTER_GRAMMAR = "filter <" + FILTER_INCLUDE + "|" + FILTER_EXCLUDE + "> <" + FILTER_FQN_REGEX + "> [parameters]"; + + private static final String SUPPORTING_TYPES_ALL_REFERENCED = "all-referenced"; + private static final String SUPPORTING_TYPES_REFERENCED_IN_PACKAGE = "referenced-in-package"; + private static final String SUPPORTING_TYPES_IN_PACKAGE = "in-package"; + private static final String SUPPORTING_TYPES_UNDER_PACKAGE = "under-package"; + private static final String SUPPORTING_TYPES_NONE = "none"; + private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_NONE)) + "> [parameters]"; + + private static final String NAMING_NAME = "name"; + private static final String NAMING_FQN = "fqn"; + private static final String NAMING_PACKAGE = "package"; + private static final String NAMING_GRAMMAR = "naming <" + String.join("|", List.of(NAMING_NAME, NAMING_FQN, NAMING_PACKAGE)) + ">"; void parseTechnology(ComponentFinderStrategyDslContext context, Tokens tokens) { if (tokens.size() != 2) { @@ -44,7 +60,7 @@ void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File String type = tokens.get(1); switch (type.toLowerCase()) { - case "annotation": + case MATCHER_ANNOTATION: if (tokens.size() == 3) { String name = tokens.get(2); @@ -53,7 +69,7 @@ void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File throw new RuntimeException("Expected: " + MATCHER_ANNOTATION_GRAMMAR); } break; - case "extends": + case MATCHER_EXTENDS: if (tokens.size() == 3) { String name = tokens.get(2); @@ -62,7 +78,7 @@ void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File throw new RuntimeException("Expected: " + MATCHER_EXTENDS_GRAMMAR); } break; - case "implements": + case MATCHER_IMPLEMENTS: if (tokens.size() == 3) { String name = tokens.get(2); @@ -71,7 +87,7 @@ void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File throw new RuntimeException("Expected: " + MATCHER_IMPLEMENTS_GRAMMAR); } break; - case "namesuffix": + case MATCHER_NAME_SUFFIX: if (tokens.size() == 3) { String suffix = tokens.get(2); @@ -80,7 +96,7 @@ void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File throw new RuntimeException("Expected: " + MATCHER_NAMESUFFIX_GRAMMAR); } break; - case "regex": + case MATCHER_FQN_REGEX: if (tokens.size() == 3) { String regex = tokens.get(2); @@ -109,26 +125,26 @@ void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File } void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { - if (tokens.size() < 2) { + if (tokens.size() < 3) { throw new RuntimeException("Too few tokens, expected: " + FILTER_GRAMMAR); } - String type = tokens.get(1).toLowerCase(); - switch (type) { - case "includeregex": - if (tokens.size() == 3) { - String regex = tokens.get(2); + String includeOrExclude = tokens.get(1).toLowerCase(); + if (!"include".equalsIgnoreCase(includeOrExclude) && !"exclude".equalsIgnoreCase(includeOrExclude)) { + throw new RuntimeException("Filter mode should be \"" + FILTER_INCLUDE + "\" or \"" + FILTER_EXCLUDE + "\": " + FILTER_GRAMMAR); + } - context.getComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(regex)); - } else { - throw new RuntimeException("Expected: " + FILTER_GRAMMAR); - } - break; - case "excluderegex": - if (tokens.size() == 3) { - String regex = tokens.get(2); + String type = tokens.get(2).toLowerCase(); + switch (type) { + case FILTER_FQN_REGEX: + if (tokens.size() == 4) { + String regex = tokens.get(3); - context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeTypesByRegexFilter(regex)); + if (FILTER_INCLUDE.equalsIgnoreCase(includeOrExclude)) { + context.getComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(regex)); + } else { + context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeTypesByRegexFilter(regex)); + } } else { throw new RuntimeException("Expected: " + FILTER_GRAMMAR); } @@ -145,18 +161,21 @@ void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens toke String type = tokens.get(1).toLowerCase(); switch (type) { - case "referenced": + case SUPPORTING_TYPES_ALL_REFERENCED: context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesSupportingTypesStrategy()); break; - case "referencedinpackage": + case SUPPORTING_TYPES_REFERENCED_IN_PACKAGE: context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesInPackageSupportingTypesStrategy()); break; - case "inpackage": + case SUPPORTING_TYPES_IN_PACKAGE: context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesInPackageSupportingTypesStrategy()); break; - case "underpackage": + case SUPPORTING_TYPES_UNDER_PACKAGE: context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesUnderPackageSupportingTypesStrategy()); break; + case SUPPORTING_TYPES_NONE: + context.getComponentFinderStrategyBuilder().supportedBy(new DefaultSupportingTypesStrategy()); + break; default: throw new IllegalArgumentException("Unknown supporting types strategy: " + type); } @@ -169,13 +188,13 @@ void parseNaming(ComponentFinderStrategyDslContext context, Tokens tokens, File String type = tokens.get(1).toLowerCase(); switch (type) { - case "name": + case NAMING_NAME: context.getComponentFinderStrategyBuilder().namedBy(new SimpleNamingStrategy()); break; - case "fqn": + case NAMING_FQN: context.getComponentFinderStrategyBuilder().namedBy(new FullyQualifiedNamingStrategy()); break; - case "package": + case NAMING_PACKAGE: context.getComponentFinderStrategyBuilder().namedBy(new DefaultPackageNamingStrategy()); break; default: diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index ad95614e9..beaa5db9f 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -39,7 +39,7 @@ void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseTechnology() { parser.parseTechnology(context, tokens("technology", "name")); - assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -48,7 +48,7 @@ void test_parseMatcher_ThrowsAnException_WhenNoTypeIsSpecified() { parser.parseMatcher(context, tokens("matcher"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: matcher [parameters]", e.getMessage()); + assertEquals("Too few tokens, expected: matcher [parameters]", e.getMessage()); } } @@ -65,7 +65,7 @@ void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "annotation", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=AnnotationTypeMatcher{annotationType='Lcom/example/Component;'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=AnnotationTypeMatcher{annotationType='Lcom/example/Component;'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -81,7 +81,7 @@ void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsedAndThereAreTooFewTokens() @Test void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "extends", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=ExtendsTypeMatcher{className='com.example.Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ExtendsTypeMatcher{className='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -97,39 +97,39 @@ void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "implements", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=ImplementsTypeMatcher{interfaceName='com.example.Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ImplementsTypeMatcher{interfaceName='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsedAndThereAreTooFewTokens() { try { - parser.parseMatcher(context, tokens("matcher", "namesuffix"), null); + parser.parseMatcher(context, tokens("matcher", "name-suffix"), null); fail(); } catch (Exception e) { - assertEquals("Expected: matcher namesuffix ", e.getMessage()); + assertEquals("Expected: matcher name-suffix ", e.getMessage()); } } @Test void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsed() { - parser.parseMatcher(context, tokens("matcher", "namesuffix", "Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + parser.parseMatcher(context, tokens("matcher", "name-suffix", "Component"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test - void test_parseMatcher_WhenTheRegexTypeMatcherIsUsedAndThereAreTooFewTokens() { + void test_parseMatcher_WhenTheFullyQualifiedNameRegexTypeMatcherIsUsedAndThereAreTooFewTokens() { try { - parser.parseMatcher(context, tokens("matcher", "regex"), null); + parser.parseMatcher(context, tokens("matcher", "fqn-regex"), null); fail(); } catch (Exception e) { - assertEquals("Expected: matcher regex ", e.getMessage()); + assertEquals("Expected: matcher fqn-regex ", e.getMessage()); } } @Test void test_parseMatcher_WhenTheRegexTypeMatcherIsUsed() { - parser.parseMatcher(context, tokens("matcher", "regex", ".*Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=RegexTypeMatcher{regex=.*Component}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + parser.parseMatcher(context, tokens("matcher", "fqn-regex", ".*Component"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=RegexTypeMatcher{regex='.*Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -145,32 +145,64 @@ void test_parseMatcher_WhenACustomTypeMatcherIsUsedButCannotBeLoaded() { @Test void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithoutParameters() { parser.parseMatcher(context, tokens("matcher", "com.structurizr.dsl.example.CustomTypeMatcher"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=CustomTypeMatcher{}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=CustomTypeMatcher{}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithAParameter() { parser.parseMatcher(context, tokens("matcher", NameSuffixTypeMatcher.class.getCanonicalName(), "Component"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology='null', typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=DefaultTypeFilter{}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=DefaultNamingStrategy{}, componentVisitor=DefaultComponentVisitor{}}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test - void test_parseFilter_ThrowsAnException_WhenNoTypeIsSpecified() { + void test_parseFilter_ThrowsAnException_WhenNoModeAndTypeAreSpecified() { try { parser.parseFilter(context, tokens("filter"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseFilter(context, tokens("filter", "include"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_ThrowsAnException_WhenInvalidModeIsSpecified() { + try { + parser.parseFilter(context, tokens("filter", "mode", "fqn-regex"), null); + fail(); + } catch (Exception e) { + assertEquals("Filter mode should be \"include\" or \"exclude\": filter [parameters]", e.getMessage()); } } + @Test + void test_parseFilter_WhenIncludeFullyQualifiedNameRegexTypeFilterIsUsed() { + parser.parseFilter(context, tokens("filter", "include", "fqn-regex", ".*"), new File(".")); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseFilter_WhenExcludeFullyQualifiedNameRegexTypeFilterIsUsed() { + parser.parseFilter(context, tokens("filter", "exclude", "fqn-regex", ".*"), new File(".")); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + @Test void test_parseSupportingTypes_ThrowsAnException_WhenNoTypeIsSpecified() { try { parser.parseSupportingTypes(context, tokens("supportingTypes"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: supportingTypes [parameters]", e.getMessage()); + assertEquals("Too few tokens, expected: supportingTypes [parameters]", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index abf7e451a..8446171cb 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -25,7 +25,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt strategy { technology "Spring MVC Controller" matcher annotation "org.springframework.stereotype.Controller" - filter excludeRegex ".*.CrashController" + filter exclude fqn-regex ".*.CrashController" forEach { clinicEmployee -> this "Uses" tag "Spring MVC Controller" From 1e2b19c854c1327eb22a405e4365266a6b20a74d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 17 Sep 2024 08:45:01 +0100 Subject: [PATCH 268/418] Adds a way to customise how component descriptions are set; also some tidy-up/renaming. --- .../component/ComponentFinderStrategy.java | 9 +- .../ComponentFinderStrategyBuilder.java | 28 +++++- .../DefaultDescriptionStrategy.java | 21 +++++ .../description/DescriptionStrategy.java | 9 ++ .../FirstSentenceDescriptionStrategy.java | 28 ++++++ .../TruncatedDescriptionStrategy.java | 42 +++++++++ ...gStrategy.java => TypeNamingStrategy.java} | 9 +- .../provider/JavadocCommentFilter.java | 18 +--- .../provider/SourceDirectoryTypeProvider.java | 17 ++-- .../ComponentFinderStrategyBuilderTests.java | 72 +++++++++++---- ...FirstSentenceDescriptionStrategyTests.java | 24 +++++ .../TruncatedDescriptionStrategyTests.java | 36 ++++++++ ...ests.java => TypeNamingStrategyTests.java} | 4 +- .../provider/JavadocCommentFilterTests.java | 40 +++------ .../ComponentFinderStrategyDslContext.java | 2 +- ...ponentFinderStrategyForEachDslContext.java | 1 - .../dsl/ComponentFinderStrategyParser.java | 64 +++++++++---- .../structurizr/dsl/StructurizrDslParser.java | 7 +- .../structurizr/dsl/StructurizrDslTokens.java | 3 +- .../ComponentFinderStrategyParserTests.java | 89 ++++++++++++++++--- .../java/com/structurizr/dsl/DslTests.java | 1 + .../dsl/spring-petclinic/workspace.dsl | 1 + 22 files changed, 410 insertions(+), 115 deletions(-) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/description/DefaultDescriptionStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/description/TruncatedDescriptionStrategy.java rename structurizr-component/src/main/java/com/structurizr/component/naming/{SimpleNamingStrategy.java => TypeNamingStrategy.java} (61%) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/description/TruncatedDescriptionStrategyTests.java rename structurizr-component/src/test/java/com/structurizr/component/naming/{SimpleNamingStrategyTests.java => TypeNamingStrategyTests.java} (60%) diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index 3d5886f2c..774f10ba5 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -1,5 +1,7 @@ package com.structurizr.component; +import com.structurizr.component.description.DefaultDescriptionStrategy; +import com.structurizr.component.description.DescriptionStrategy; import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.matcher.TypeMatcher; import com.structurizr.component.naming.NamingStrategy; @@ -27,14 +29,16 @@ public final class ComponentFinderStrategy { private final TypeFilter typeFilter; private final SupportingTypesStrategy supportingTypesStrategy; private final NamingStrategy namingStrategy; + private final DescriptionStrategy descriptionStrategy; private final ComponentVisitor componentVisitor; - ComponentFinderStrategy(String technology, TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy, ComponentVisitor componentVisitor) { + ComponentFinderStrategy(String technology, TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy, DescriptionStrategy descriptionStrategy, ComponentVisitor componentVisitor) { this.technology = technology; this.typeMatcher = typeMatcher; this.typeFilter = typeFilter; this.supportingTypesStrategy = supportingTypesStrategy; this.namingStrategy = namingStrategy; + this.descriptionStrategy = descriptionStrategy; this.componentVisitor = componentVisitor; } @@ -45,7 +49,7 @@ Set findComponents(TypeRepository typeRepository) { for (Type type : types) { if (typeMatcher.matches(type) && typeFilter.accept(type)) { DiscoveredComponent component = new DiscoveredComponent(namingStrategy.nameOf(type), type); - component.setDescription(type.getDescription()); + component.setDescription(descriptionStrategy.descriptionOf(type)); component.setTechnology(this.technology); component.setComponentFinderStrategy(this); components.add(component); @@ -71,6 +75,7 @@ public String toString() { ", typeFilter=" + typeFilter + ", supportingTypesStrategy=" + supportingTypesStrategy + ", namingStrategy=" + namingStrategy + + ", descriptionStrategy=" + descriptionStrategy + ", componentVisitor=" + componentVisitor + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java index a8dcbb9f2..eab3e25c2 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -1,5 +1,7 @@ package com.structurizr.component; +import com.structurizr.component.description.DefaultDescriptionStrategy; +import com.structurizr.component.description.DescriptionStrategy; import com.structurizr.component.filter.DefaultTypeFilter; import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.matcher.TypeMatcher; @@ -21,6 +23,7 @@ public final class ComponentFinderStrategyBuilder { private TypeFilter typeFilter; private SupportingTypesStrategy supportingTypesStrategy; private NamingStrategy namingStrategy; + private DescriptionStrategy descriptionStrategy; private ComponentVisitor componentVisitor; public ComponentFinderStrategyBuilder() { @@ -68,7 +71,7 @@ public ComponentFinderStrategyBuilder supportedBy(SupportingTypesStrategy suppor return this; } - public ComponentFinderStrategyBuilder namedBy(NamingStrategy namingStrategy) { + public ComponentFinderStrategyBuilder withName(NamingStrategy namingStrategy) { if (namingStrategy == null) { throw new IllegalArgumentException("A naming strategy must be provided"); } @@ -82,7 +85,21 @@ public ComponentFinderStrategyBuilder namedBy(NamingStrategy namingStrategy) { return this; } - public ComponentFinderStrategyBuilder asTechnology(String technology) { + public ComponentFinderStrategyBuilder withDescription(DescriptionStrategy descriptionStrategy) { + if (descriptionStrategy == null) { + throw new IllegalArgumentException("A description strategy must be provided"); + } + + if (this.descriptionStrategy != null) { + throw new IllegalArgumentException("A description strategy has already been configured"); + } + + this.descriptionStrategy = descriptionStrategy; + + return this; + } + + public ComponentFinderStrategyBuilder forTechnology(String technology) { if (StringUtils.isNullOrEmpty(technology)) { throw new IllegalArgumentException("A technology must be provided"); } @@ -127,11 +144,15 @@ public ComponentFinderStrategy build() { namingStrategy = new DefaultNamingStrategy(); } + if (descriptionStrategy == null) { + descriptionStrategy = new DefaultDescriptionStrategy(); + } + if (componentVisitor == null) { componentVisitor = new DefaultComponentVisitor(); } - return new ComponentFinderStrategy(technology, typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, componentVisitor); + return new ComponentFinderStrategy(technology, typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, descriptionStrategy, componentVisitor); } @Override @@ -142,6 +163,7 @@ public String toString() { ", typeFilter=" + typeFilter + ", supportingTypesStrategy=" + supportingTypesStrategy + ", namingStrategy=" + namingStrategy + + ", descriptionStrategy=" + descriptionStrategy + ", componentVisitor=" + componentVisitor + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/description/DefaultDescriptionStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/description/DefaultDescriptionStrategy.java new file mode 100644 index 000000000..f2e1ef0ff --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/description/DefaultDescriptionStrategy.java @@ -0,0 +1,21 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; +import com.structurizr.component.naming.NamingStrategy; + +/** + * Uses the type description as-is. + */ +public class DefaultDescriptionStrategy implements DescriptionStrategy { + + @Override + public String descriptionOf(Type type) { + return type.getDescription(); + } + + @Override + public String toString() { + return "DefaultDescriptionStrategy{}"; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java new file mode 100644 index 000000000..c255a73f4 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java @@ -0,0 +1,9 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; + +public interface DescriptionStrategy { + + String descriptionOf(Type type); + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java new file mode 100644 index 000000000..accb274f6 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java @@ -0,0 +1,28 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; + +/** + * Uses the first sentence of the type description, or the description as-is if there are no sentences. + */ +public class FirstSentenceDescriptionStrategy implements DescriptionStrategy { + + @Override + public String descriptionOf(Type type) { + String description = type.getDescription(); + + int index = description.indexOf('.'); + if (index == -1) { + return description; + } else { + return description.substring(0, index+1); + } + } + + @Override + public String toString() { + return "FirstSentenceDescriptionStrategy{}"; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/description/TruncatedDescriptionStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/description/TruncatedDescriptionStrategy.java new file mode 100644 index 000000000..a006ec504 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/description/TruncatedDescriptionStrategy.java @@ -0,0 +1,42 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; + +/** + * Truncates the type description to the max length, appending "..." when truncated. + */ +public class TruncatedDescriptionStrategy implements DescriptionStrategy { + + private final int maxLength; + + public TruncatedDescriptionStrategy(int maxLength) { + if (maxLength < 1) { + throw new IllegalArgumentException("Max length must be greater than 0"); + } + + this.maxLength = maxLength; + } + + @Override + public String descriptionOf(Type type) { + String description = type.getDescription(); + + if (StringUtils.isNullOrEmpty(description)) { + return description; + } + + if (description.length() > maxLength) { + return description.substring(0, maxLength) + "..."; + } else { + return description; + } + } + + @Override + public String toString() { + return "TruncatedDescriptionStrategy{" + + "maxLength=" + maxLength + + '}'; + } +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/SimpleNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/TypeNamingStrategy.java similarity index 61% rename from structurizr-component/src/main/java/com/structurizr/component/naming/SimpleNamingStrategy.java rename to structurizr-component/src/main/java/com/structurizr/component/naming/TypeNamingStrategy.java index 7b60b58de..47ffe3b90 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/naming/SimpleNamingStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/TypeNamingStrategy.java @@ -5,10 +5,15 @@ /** * Uses the simple/short name of the type (i.e. without the package name). */ -public class SimpleNamingStrategy implements NamingStrategy { +public class TypeNamingStrategy implements NamingStrategy { public String nameOf(Type type) { return type.getName(); } -} + @Override + public String toString() { + return "TypeNamingStrategy{}"; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java b/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java index c89b3fc34..51b02900b 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java @@ -5,17 +5,7 @@ */ class JavadocCommentFilter { - private final Integer maxCommentLength; - - JavadocCommentFilter(Integer maxCommentLength) { - if (maxCommentLength != null && maxCommentLength < 1) { - throw new IllegalArgumentException("Maximum comment length must be greater than 0."); - } - - this.maxCommentLength = maxCommentLength; - } - - String filterAndTruncate(String s) { + String filter(String s) { if (s == null) { return null; } @@ -25,11 +15,7 @@ String filterAndTruncate(String s) { s = s.replaceAll("\\{@link (\\S*)\\}", "$1"); s = s.replaceAll("\\{@link (\\S*) (.*?)\\}", "$2"); - if (maxCommentLength != null && s.length() > maxCommentLength) { - return s.substring(0, maxCommentLength-3) + "..."; - } else { - return s; - } + return s; } } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java index f8d8db07f..6bce2bb1c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -23,17 +23,11 @@ public final class SourceDirectoryTypeProvider implements TypeProvider { private static final Log log = LogFactory.getLog(SourceDirectoryTypeProvider.class); private static final String JAVA_FILE_EXTENSION = ".java"; - private static final int DEFAULT_DESCRIPTION_LENGTH = 60; private final File directory; - private final int maximumDescriptionLength; private final Set types = new LinkedHashSet<>(); public SourceDirectoryTypeProvider(File directory) { - this(directory, DEFAULT_DESCRIPTION_LENGTH); - } - - public SourceDirectoryTypeProvider(File directory, int maximumDescriptionLength) { if (directory == null) { throw new IllegalArgumentException("A directory must be supplied"); } @@ -47,24 +41,23 @@ public SourceDirectoryTypeProvider(File directory, int maximumDescriptionLength) } this.directory = directory; - this.maximumDescriptionLength = maximumDescriptionLength; StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); } @Override public Set getTypes() { - parse(directory, maximumDescriptionLength); + parse(directory); return new LinkedHashSet<>(types); } - private void parse(File path, int maximumDescriptionLength) { + private void parse(File path) { if (path.isDirectory()) { File[] files = path.listFiles(); if (files != null) { for (File file : files) { try { - parse(file, maximumDescriptionLength); + parse(file); } catch (Exception e) { log.warn("Error parsing " + file.getAbsolutePath(), e); } @@ -85,7 +78,7 @@ public void visit(ClassOrInterfaceDeclaration n, Object arg) { JavadocComment javadocComment = (JavadocComment) n.getComment().get(); String description = javadocComment.parse().getDescription().toText(); - type.setDescription(new JavadocCommentFilter(maximumDescriptionLength).filterAndTruncate(description)); + type.setDescription(new JavadocCommentFilter().filter(description)); } types.add(type); } @@ -107,7 +100,7 @@ public void visit(PackageDeclaration n, Object arg) { JavadocComment javadocComment = (JavadocComment)rootNode.getComment().get(); String description = javadocComment.parse().getDescription().toText(); - type.setDescription(new JavadocCommentFilter(maximumDescriptionLength).filterAndTruncate(description)); + type.setDescription(new JavadocCommentFilter().filter(description)); } types.add(type); diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java index 764a304a0..cb770a312 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -1,10 +1,12 @@ package com.structurizr.component; +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +import com.structurizr.component.description.TruncatedDescriptionStrategy; import com.structurizr.component.filter.ExcludeTypesByRegexFilter; import com.structurizr.component.filter.IncludeTypesByRegexFilter; import com.structurizr.component.matcher.NameSuffixTypeMatcher; import com.structurizr.component.naming.FullyQualifiedNamingStrategy; -import com.structurizr.component.naming.SimpleNamingStrategy; +import com.structurizr.component.naming.TypeNamingStrategy; import com.structurizr.component.supporting.AllTypesInPackageSupportingTypesStrategy; import com.structurizr.component.supporting.AllTypesUnderPackageSupportingTypesStrategy; import org.junit.jupiter.api.Test; @@ -13,11 +15,6 @@ public class ComponentFinderStrategyBuilderTests { - @Test - void build_ThrowsAnException_WhenATypeMatcherHasNotBeenConfigured() { - assertThrowsExactly(RuntimeException.class, () -> new ComponentFinderStrategyBuilder().build()); - } - @Test void matchedBy_ThrowsAnException_WhenPassedNull() { try { @@ -79,9 +76,9 @@ void supportedBy_ThrowsAnException_WhenCalledTwice() { } @Test - void namedBy_ThrowsAnException_WhenPassedNull() { + void withName_ThrowsAnException_WhenPassedNull() { try { - new ComponentFinderStrategyBuilder().namedBy(null); + new ComponentFinderStrategyBuilder().withName(null); fail(); } catch (Exception e) { assertEquals("A naming strategy must be provided", e.getMessage()); @@ -89,9 +86,9 @@ void namedBy_ThrowsAnException_WhenPassedNull() { } @Test - void namedBy_ThrowsAnException_WhenCalledTwice() { + void withName_ThrowsAnException_WhenCalledTwice() { try { - new ComponentFinderStrategyBuilder().namedBy(new SimpleNamingStrategy()).namedBy(new FullyQualifiedNamingStrategy()); + new ComponentFinderStrategyBuilder().withName(new TypeNamingStrategy()).withName(new FullyQualifiedNamingStrategy()); fail(); } catch (Exception e) { assertEquals("A naming strategy has already been configured", e.getMessage()); @@ -99,9 +96,29 @@ void namedBy_ThrowsAnException_WhenCalledTwice() { } @Test - void asTechnology_ThrowsAnException_WhenPassedNull() { + void withDescription_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().withDescription(null); + fail(); + } catch (Exception e) { + assertEquals("A description strategy must be provided", e.getMessage()); + } + } + + @Test + void withDescription_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().withDescription(new TruncatedDescriptionStrategy(50)).withDescription(new FirstSentenceDescriptionStrategy()); + fail(); + } catch (Exception e) { + assertEquals("A description strategy has already been configured", e.getMessage()); + } + } + + @Test + void forTechnology_ThrowsAnException_WhenPassedNull() { try { - new ComponentFinderStrategyBuilder().asTechnology(null); + new ComponentFinderStrategyBuilder().forTechnology(null); fail(); } catch (Exception e) { assertEquals("A technology must be provided", e.getMessage()); @@ -109,9 +126,9 @@ void asTechnology_ThrowsAnException_WhenPassedNull() { } @Test - void asTechnology_ThrowsAnException_WhenPassedAnEmptyString() { + void forTechnology_ThrowsAnException_WhenPassedAnEmptyString() { try { - new ComponentFinderStrategyBuilder().asTechnology(""); + new ComponentFinderStrategyBuilder().forTechnology(""); fail(); } catch (Exception e) { assertEquals("A technology must be provided", e.getMessage()); @@ -119,13 +136,36 @@ void asTechnology_ThrowsAnException_WhenPassedAnEmptyString() { } @Test - void asTechnology_ThrowsAnException_WhenCalledTwice() { + void forTechnology_ThrowsAnException_WhenCalledTwice() { try { - new ComponentFinderStrategyBuilder().asTechnology("X").asTechnology("Y"); + new ComponentFinderStrategyBuilder().forTechnology("X").forTechnology("Y"); fail(); } catch (Exception e) { assertEquals("A technology has already been configured", e.getMessage()); } } + @Test + void build_ThrowsAnException_WhenATypeMatcherHasNotBeenConfigured() { + try { + new ComponentFinderStrategyBuilder().build(); + fail(); + } catch (Exception e) { + assertEquals("A type matcher must be provided", e.getMessage()); + } + } + + @Test + void build() { + ComponentFinderStrategy strategy = new ComponentFinderStrategyBuilder() + .forTechnology("Spring MVC Controller") + .matchedBy(new NameSuffixTypeMatcher("Controller")) + .filteredBy(new IncludeTypesByRegexFilter("com.example.web.\\.*")) + .withName(new TypeNamingStrategy()) + .withDescription(new FirstSentenceDescriptionStrategy()) + .build(); + + assertEquals("ComponentFinderStrategy{technology='Spring MVC Controller', typeMatcher=NameSuffixTypeMatcher{suffix='Controller'}, typeFilter=IncludeTypesByRegexFilter{regex='com.example.web.\\.*'}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=FirstSentenceDescriptionStrategy{}, componentVisitor=DefaultComponentVisitor{}}", strategy.toString()); + } + } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java new file mode 100644 index 000000000..f9c2515e1 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java @@ -0,0 +1,24 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FirstSentenceDescriptionStrategyTests { + + @Test + void descriptionOf_WhenThereIsASentence() { + Type type = new Type("com.example.ClassName"); + type.setDescription("This is the first sentence. And this is the second."); + assertEquals("This is the first sentence.", new FirstSentenceDescriptionStrategy().descriptionOf(type)); + } + + @Test + void descriptionOf_WhenThereIsNotASentence() { + Type type = new Type("com.example.ClassName"); + type.setDescription("This is just lots of text without any punctuation"); + assertEquals("This is just lots of text without any punctuation", new FirstSentenceDescriptionStrategy().descriptionOf(type)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/description/TruncatedDescriptionStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/description/TruncatedDescriptionStrategyTests.java new file mode 100644 index 000000000..358e78020 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/description/TruncatedDescriptionStrategyTests.java @@ -0,0 +1,36 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TruncatedDescriptionStrategyTests { + + @Test + public void test_construction_ThrowsAnIllegalArgumentException_WhenZeroIsSpecified() { + try { + new TruncatedDescriptionStrategy(0); + } catch (Exception e) { + assertEquals("Max length must be greater than 0", e.getMessage()); + } + } + + @Test + public void test_construction_ThrowsAnIllegalArgumentException_WhenANegativeNumberIsSpecified() { + try { + new TruncatedDescriptionStrategy(-1); + } catch (Exception e) { + assertEquals("Max length must be greater than 0", e.getMessage()); + } + } + + @Test + public void test_descriptionOf_TruncatesTheDescription() + { + Type type = new Type("Name"); + type.setDescription("Here is some text."); + assertEquals("Here...", new TruncatedDescriptionStrategy(4).descriptionOf(type)); + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/naming/SimpleNamingStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/naming/TypeNamingStrategyTests.java similarity index 60% rename from structurizr-component/src/test/java/com/structurizr/component/naming/SimpleNamingStrategyTests.java rename to structurizr-component/src/test/java/com/structurizr/component/naming/TypeNamingStrategyTests.java index a85aae823..b86749322 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/naming/SimpleNamingStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/naming/TypeNamingStrategyTests.java @@ -5,11 +5,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -public class SimpleNamingStrategyTests { +public class TypeNamingStrategyTests { @Test void nameOf() { - assertEquals("ClassName", new SimpleNamingStrategy().nameOf(new Type("com.example.ClassName"))); + assertEquals("ClassName", new TypeNamingStrategy().nameOf(new Type("com.example.ClassName"))); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java index 265a888d6..fa1cc0b39 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java @@ -7,54 +7,38 @@ public class JavadocCommentFilterTests { @Test - public void test_construction_ThrowsAnIllegalArgumentException_WhenZeroIsSpecified() { - assertThrowsExactly(IllegalArgumentException.class, () -> new JavadocCommentFilter(0)); + public void test_filter_ReturnsNull_WhenGivenNull() { + assertNull(new JavadocCommentFilter().filter(null)); } @Test - public void test_construction_ThrowsAnIllegalArgumentException_WhenANegativeNumberIsSpecified() { - assertThrowsExactly(IllegalArgumentException.class, () -> new JavadocCommentFilter(-1)); - } - - @Test - public void test_filterAndTruncate_ReturnsNull_WhenGivenNull() { - assertNull(new JavadocCommentFilter(null).filterAndTruncate(null)); - } - - @Test - public void test_filterAndTruncate_ReturnsTheOriginalText_WhenNoMaxLengthHasBeenSpecified() - { - assertEquals("Here is some text.", new JavadocCommentFilter(null).filterAndTruncate("Here is some text.")); - } - - @Test - public void test_filterAndTruncate_TruncatesTheTextWhenAMaxLengthHasBeenSpecified() + public void test_filter_ReturnsTheOriginalText_WhenNoMaxLengthHasBeenSpecified() { - assertEquals("Here...", new JavadocCommentFilter(7).filterAndTruncate("Here is some text.")); + assertEquals("Here is some text.", new JavadocCommentFilter().filter("Here is some text.")); } @Test - public void test_filterAndTruncate_FiltersJavadocLinkTags() + public void test_filter_FiltersJavadocLinkTags() { - assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses {@link SomeClass} and {@link AnotherClass} to do some work.")); + assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter().filter("Uses {@link SomeClass} and {@link AnotherClass} to do some work.")); } @Test - public void test_filterAndTruncate_FiltersJavadocLinkTagsWithLabels() + public void test_filter_FiltersJavadocLinkTagsWithLabels() { - assertEquals("Uses some class and another class to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses {@link SomeClass some class} and {@link AnotherClass another class} to do some work.")); + assertEquals("Uses some class and another class to do some work.", new JavadocCommentFilter().filter("Uses {@link SomeClass some class} and {@link AnotherClass another class} to do some work.")); } @Test - public void test_filterAndTruncate_FiltersHtml() + public void test_filter_FiltersHtml() { - assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses SomeClass and AnotherClass to do some work.")); + assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter().filter("Uses SomeClass and AnotherClass to do some work.")); } @Test - public void test_filterAndTruncate_FiltersLineBreaks() + public void test_filter_FiltersLineBreaks() { - assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter(null).filterAndTruncate("Uses SomeClass and AnotherClass\nto do some work.")); + assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter().filter("Uses SomeClass and AnotherClass\nto do some work.")); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java index 762d4c383..2336fa006 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java @@ -19,7 +19,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FILTER_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN, - StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_NAMING_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_NAME_TOKEN, StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java index ff2db4158..2e600b353 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import java.io.File; import java.util.ArrayList; import java.util.List; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index bb345bc40..97d3cbf61 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -1,10 +1,12 @@ package com.structurizr.dsl; +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +import com.structurizr.component.description.TruncatedDescriptionStrategy; import com.structurizr.component.filter.ExcludeTypesByRegexFilter; import com.structurizr.component.filter.IncludeTypesByRegexFilter; import com.structurizr.component.matcher.*; import com.structurizr.component.naming.DefaultPackageNamingStrategy; -import com.structurizr.component.naming.SimpleNamingStrategy; +import com.structurizr.component.naming.TypeNamingStrategy; import com.structurizr.component.naming.FullyQualifiedNamingStrategy; import com.structurizr.component.supporting.*; @@ -39,10 +41,15 @@ final class ComponentFinderStrategyParser extends AbstractParser { private static final String SUPPORTING_TYPES_NONE = "none"; private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_NONE)) + "> [parameters]"; - private static final String NAMING_NAME = "name"; - private static final String NAMING_FQN = "fqn"; - private static final String NAMING_PACKAGE = "package"; - private static final String NAMING_GRAMMAR = "naming <" + String.join("|", List.of(NAMING_NAME, NAMING_FQN, NAMING_PACKAGE)) + ">"; + private static final String NAME_TYPE_NAME = "type-name"; + private static final String NAME_FQN = "fqn"; + private static final String NAME_PACKAGE = "package"; + private static final String NAME_GRAMMAR = "name <" + String.join("|", List.of(NAME_TYPE_NAME, NAME_FQN, NAME_PACKAGE)) + ">"; + + private static final String DESCRIPTION_TRUNCATED = "truncated"; + private static final String DESCRIPTION_FIRST_SENTENCE = "first-sentence"; + private static final String DESCRIPTION_GRAMMAR = "description <" + String.join("|", List.of(DESCRIPTION_FIRST_SENTENCE, DESCRIPTION_TRUNCATED)) + ">"; + private static final String DESCRIPTION_TRUNCATED_GRAMMAR = "description truncated "; void parseTechnology(ComponentFinderStrategyDslContext context, Tokens tokens) { if (tokens.size() != 2) { @@ -50,7 +57,7 @@ void parseTechnology(ComponentFinderStrategyDslContext context, Tokens tokens) { } String name = tokens.get(1); - context.getComponentFinderStrategyBuilder().asTechnology(name); + context.getComponentFinderStrategyBuilder().forTechnology(name); } void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { @@ -181,24 +188,51 @@ void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens toke } } - void parseNaming(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { + void parseName(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { if (tokens.size() < 2) { - throw new RuntimeException("Too few tokens, expected: " + NAMING_GRAMMAR); + throw new RuntimeException("Too few tokens, expected: " + NAME_GRAMMAR); } String type = tokens.get(1).toLowerCase(); switch (type) { - case NAMING_NAME: - context.getComponentFinderStrategyBuilder().namedBy(new SimpleNamingStrategy()); + case NAME_TYPE_NAME: + context.getComponentFinderStrategyBuilder().withName(new TypeNamingStrategy()); + break; + case NAME_FQN: + context.getComponentFinderStrategyBuilder().withName(new FullyQualifiedNamingStrategy()); break; - case NAMING_FQN: - context.getComponentFinderStrategyBuilder().namedBy(new FullyQualifiedNamingStrategy()); + case NAME_PACKAGE: + context.getComponentFinderStrategyBuilder().withName(new DefaultPackageNamingStrategy()); + break; + default: + throw new IllegalArgumentException("Unknown name strategy: " + type); + } + } + + void parseDescription(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { + if (tokens.size() < 2) { + throw new RuntimeException("Too few tokens, expected: " + DESCRIPTION_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case DESCRIPTION_FIRST_SENTENCE: + context.getComponentFinderStrategyBuilder().withDescription(new FirstSentenceDescriptionStrategy()); break; - case NAMING_PACKAGE: - context.getComponentFinderStrategyBuilder().namedBy(new DefaultPackageNamingStrategy()); + case DESCRIPTION_TRUNCATED: + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + DESCRIPTION_TRUNCATED_GRAMMAR); + } + + try { + int maxLength = Integer.parseInt(tokens.get(2)); + context.getComponentFinderStrategyBuilder().withDescription(new TruncatedDescriptionStrategy(maxLength)); + } catch (NumberFormatException e) { + throw new RuntimeException("Max length must be an integer"); + } break; default: - throw new IllegalArgumentException("Unknown naming strategy: " + type); + throw new IllegalArgumentException("Unknown description strategy: " + type); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index c0ec6c095..6ed663556 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -450,8 +450,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { new ComponentFinderStrategyParser().parseSupportingTypes(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); - } else if (COMPONENT_FINDER_STRATEGY_NAMING_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { - new ComponentFinderStrategyParser().parseNaming(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); + } else if (COMPONENT_FINDER_STRATEGY_NAME_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseName(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); + + } else if (COMPONENT_FINDER_STRATEGY_DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseDescription(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); } else if (COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { if (shouldStartContext(tokens)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 5e26d24df..f8d11e5ec 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -121,7 +121,8 @@ class StructurizrDslTokens { static final String COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN = "matcher"; static final String COMPONENT_FINDER_STRATEGY_FILTER_TOKEN = "filter"; static final String COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN = "supportingTypes"; - static final String COMPONENT_FINDER_STRATEGY_NAMING_TOKEN = "naming"; + static final String COMPONENT_FINDER_STRATEGY_NAME_TOKEN = "name"; + static final String COMPONENT_FINDER_STRATEGY_DESCRIPTION_TOKEN = "description"; static final String COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN = "forEach"; } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index beaa5db9f..7dee0f92c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -1,7 +1,6 @@ package com.structurizr.dsl; import com.structurizr.component.ComponentFinderBuilder; -import com.structurizr.component.matcher.AnnotationTypeMatcher; import com.structurizr.component.matcher.NameSuffixTypeMatcher; import org.junit.jupiter.api.Test; @@ -39,7 +38,7 @@ void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseTechnology() { parser.parseTechnology(context, tokens("technology", "name")); - assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -65,7 +64,7 @@ void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "annotation", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=AnnotationTypeMatcher{annotationType='Lcom/example/Component;'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=AnnotationTypeMatcher{annotationType='Lcom/example/Component;'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -81,7 +80,7 @@ void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsedAndThereAreTooFewTokens() @Test void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "extends", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ExtendsTypeMatcher{className='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ExtendsTypeMatcher{className='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -97,7 +96,7 @@ void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "implements", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ImplementsTypeMatcher{interfaceName='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ImplementsTypeMatcher{interfaceName='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -113,7 +112,7 @@ void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "name-suffix", "Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -129,7 +128,7 @@ void test_parseMatcher_WhenTheFullyQualifiedNameRegexTypeMatcherIsUsedAndThereAr @Test void test_parseMatcher_WhenTheRegexTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "fqn-regex", ".*Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=RegexTypeMatcher{regex='.*Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=RegexTypeMatcher{regex='.*Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -145,13 +144,13 @@ void test_parseMatcher_WhenACustomTypeMatcherIsUsedButCannotBeLoaded() { @Test void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithoutParameters() { parser.parseMatcher(context, tokens("matcher", "com.structurizr.dsl.example.CustomTypeMatcher"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=CustomTypeMatcher{}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=CustomTypeMatcher{}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithAParameter() { parser.parseMatcher(context, tokens("matcher", NameSuffixTypeMatcher.class.getCanonicalName(), "Component"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -187,13 +186,13 @@ void test_parseFilter_ThrowsAnException_WhenInvalidModeIsSpecified() { @Test void test_parseFilter_WhenIncludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "include", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseFilter_WhenExcludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "exclude", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -207,13 +206,75 @@ void test_parseSupportingTypes_ThrowsAnException_WhenNoTypeIsSpecified() { } @Test - void test_parseNaming_ThrowsAnException_WhenNoTypeIsSpecified() { + void test_parseName_ThrowsAnException_WhenNoTypeIsSpecified() { try { - parser.parseNaming(context, tokens("naming"), null); + parser.parseName(context, tokens("name"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: naming ", e.getMessage()); + assertEquals("Too few tokens, expected: name ", e.getMessage()); } } + @Test + void test_parseName() { + parser.parseName(context, tokens("name", "type-name"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseDescription_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseDescription(context, tokens("description"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: description ", e.getMessage()); + } + } + + @Test + void test_parseDescription_FirstSentence() { + parser.parseDescription(context, tokens("description", "first-sentence"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=FirstSentenceDescriptionStrategy{}, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseDescription_Truncated_ThrowsAnException_WhenNoMaxLengthIsSpecified() { + try { + parser.parseDescription(context, tokens("description", "truncated"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: description truncated ", e.getMessage()); + } + } + + @Test + void test_parseDescription_Truncated_ThrowsAnException_WhenAnInvalidMaxLengthIsSpecified() { + try { + parser.parseDescription(context, tokens("description", "truncated", "invalid"), null); + fail(); + } catch (Exception e) { + assertEquals("Max length must be an integer", e.getMessage()); + } + + try { + parser.parseDescription(context, tokens("description", "truncated", "-1"), null); + fail(); + } catch (Exception e) { + assertEquals("Max length must be greater than 0", e.getMessage()); + } + + try { + parser.parseDescription(context, tokens("description", "truncated", "0"), null); + fail(); + } catch (Exception e) { + assertEquals("Max length must be greater than 0", e.getMessage()); + } + } + + @Test + void test_parseDescription_Truncated() { + parser.parseDescription(context, tokens("description", "truncated", "50"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=TruncatedDescriptionStrategy{maxLength=50}, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 1d3665732..0dcdc835b 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1232,6 +1232,7 @@ void springPetClinic() throws Exception { Component ownerRepository = webApplication.getComponentWithName("Owner Repository"); assertNotNull(ownerRepository); + assertEquals("Repository class for Owner domain objects All method names are compliant with Spring Data naming conventions so this interface can easily be extended for Spring Data.", ownerRepository.getDescription()); assertEquals("org.springframework.samples.petclinic.owner.OwnerRepository", ownerRepository.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index 8446171cb..fd2f20a78 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -34,6 +34,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt strategy { technology "Spring Data Repository" matcher implements "org.springframework.data.repository.Repository" + description first-sentence forEach { -> relationalDatabaseSchema "Reads from and writes to" tag "Spring Data Repository" From e12096e075bbdc0e13930acc357f327d1fd00951 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 17 Sep 2024 08:55:50 +0100 Subject: [PATCH 269/418] Extra assertions. --- .../src/test/java/com/structurizr/dsl/DslTests.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 0dcdc835b..27204a8b6 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1237,15 +1237,16 @@ void springPetClinic() throws Exception { assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); assertSame(ownerRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerrepository")); - assertTrue(ownerRepository.hasEfferentRelationshipWith(relationalDatabaseSchema)); + assertTrue(ownerRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); Component vetRepository = webApplication.getComponentWithName("Vet Repository"); assertNotNull(vetRepository); + assertEquals("Repository class for Vet domain objects All method names are compliant with Spring Data naming conventions so this interface can easily be extended for Spring Data.", vetRepository.getDescription()); assertEquals("org.springframework.samples.petclinic.vet.VetRepository", vetRepository.getProperties().get("component.type")); assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getUrl()); assertSame(vetRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetrepository")); - assertTrue(vetRepository.hasEfferentRelationshipWith(relationalDatabaseSchema)); + assertTrue(vetRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); assertTrue(welcomeController.getRelationships().isEmpty()); assertNotNull(petController.getEfferentRelationshipWith(ownerRepository)); From 3872cfeb004159cad39f90d42c38c86d1a3cdcf6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 17 Sep 2024 10:54:54 +0100 Subject: [PATCH 270/418] More tidy up + adds a way to customise component URLs without using a script. --- .gitignore | 4 +- .../component/ComponentFinder.java | 1 + .../component/ComponentFinderStrategy.java | 9 ++-- .../ComponentFinderStrategyBuilder.java | 28 ++++++++-- .../component/DiscoveredComponent.java | 9 ++++ .../provider/SourceDirectoryTypeProvider.java | 16 +++++- .../component/url/DefaultUrlStrategy.java | 17 ++++++ .../component/url/PrefixUrlStrategy.java | 34 ++++++++++++ .../component/url/UrlStrategy.java | 9 ++++ .../ComponentFinderStrategyBuilderTests.java | 16 +++--- .../dsl/ComponentFinderStrategyParser.java | 27 +++++++++- .../structurizr/dsl/StructurizrDslParser.java | 3 ++ .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../ComponentFinderStrategyParserTests.java | 52 ++++++++++++++----- .../java/com/structurizr/dsl/DslTests.java | 14 ++--- .../dsl/spring-petclinic/workspace.dsl | 6 +-- 16 files changed, 204 insertions(+), 42 deletions(-) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/url/PrefixUrlStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java diff --git a/.gitignore b/.gitignore index 30458f2b7..a5e1d9c61 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ bin hs_err_pid* structurizr-dsl/src/test/resources/dsl/spring-petclinic/.structurizr -structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.json \ No newline at end of file +structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.json + +**/structurizr.properties \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index 541d2c082..1055c453e 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -119,6 +119,7 @@ public Set findComponents() { } component.setDescription(discoveredComponent.getDescription()); component.setTechnology(discoveredComponent.getTechnology()); + component.setUrl(discoveredComponent.getUrl()); componentMap.put(discoveredComponent, component); componentSet.add(component); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index 774f10ba5..d1dc8f87a 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -1,15 +1,14 @@ package com.structurizr.component; -import com.structurizr.component.description.DefaultDescriptionStrategy; import com.structurizr.component.description.DescriptionStrategy; import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.matcher.TypeMatcher; import com.structurizr.component.naming.NamingStrategy; import com.structurizr.component.supporting.SupportingTypesStrategy; +import com.structurizr.component.url.UrlStrategy; import com.structurizr.component.visitor.ComponentVisitor; import com.structurizr.model.Component; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; @@ -30,15 +29,17 @@ public final class ComponentFinderStrategy { private final SupportingTypesStrategy supportingTypesStrategy; private final NamingStrategy namingStrategy; private final DescriptionStrategy descriptionStrategy; + private final UrlStrategy urlStrategy; private final ComponentVisitor componentVisitor; - ComponentFinderStrategy(String technology, TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy, DescriptionStrategy descriptionStrategy, ComponentVisitor componentVisitor) { + ComponentFinderStrategy(String technology, TypeMatcher typeMatcher, TypeFilter typeFilter, SupportingTypesStrategy supportingTypesStrategy, NamingStrategy namingStrategy, DescriptionStrategy descriptionStrategy, UrlStrategy urlStrategy, ComponentVisitor componentVisitor) { this.technology = technology; this.typeMatcher = typeMatcher; this.typeFilter = typeFilter; this.supportingTypesStrategy = supportingTypesStrategy; this.namingStrategy = namingStrategy; this.descriptionStrategy = descriptionStrategy; + this.urlStrategy = urlStrategy; this.componentVisitor = componentVisitor; } @@ -51,6 +52,7 @@ Set findComponents(TypeRepository typeRepository) { DiscoveredComponent component = new DiscoveredComponent(namingStrategy.nameOf(type), type); component.setDescription(descriptionStrategy.descriptionOf(type)); component.setTechnology(this.technology); + component.setUrl(urlStrategy.urlOf(type)); component.setComponentFinderStrategy(this); components.add(component); @@ -76,6 +78,7 @@ public String toString() { ", supportingTypesStrategy=" + supportingTypesStrategy + ", namingStrategy=" + namingStrategy + ", descriptionStrategy=" + descriptionStrategy + + ", urlStrategy=" + urlStrategy + ", componentVisitor=" + componentVisitor + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java index eab3e25c2..6f692dc30 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -9,6 +9,8 @@ import com.structurizr.component.naming.NamingStrategy; import com.structurizr.component.supporting.DefaultSupportingTypesStrategy; import com.structurizr.component.supporting.SupportingTypesStrategy; +import com.structurizr.component.url.DefaultUrlStrategy; +import com.structurizr.component.url.UrlStrategy; import com.structurizr.component.visitor.ComponentVisitor; import com.structurizr.component.visitor.DefaultComponentVisitor; import com.structurizr.util.StringUtils; @@ -18,12 +20,13 @@ */ public final class ComponentFinderStrategyBuilder { - private String technology; private TypeMatcher typeMatcher; private TypeFilter typeFilter; private SupportingTypesStrategy supportingTypesStrategy; private NamingStrategy namingStrategy; private DescriptionStrategy descriptionStrategy; + private String technology; + private UrlStrategy urlStrategy; private ComponentVisitor componentVisitor; public ComponentFinderStrategyBuilder() { @@ -99,7 +102,7 @@ public ComponentFinderStrategyBuilder withDescription(DescriptionStrategy descri return this; } - public ComponentFinderStrategyBuilder forTechnology(String technology) { + public ComponentFinderStrategyBuilder withTechnology(String technology) { if (StringUtils.isNullOrEmpty(technology)) { throw new IllegalArgumentException("A technology must be provided"); } @@ -113,6 +116,20 @@ public ComponentFinderStrategyBuilder forTechnology(String technology) { return this; } + public ComponentFinderStrategyBuilder withUrl(UrlStrategy urlStrategy) { + if (urlStrategy == null) { + throw new IllegalArgumentException("A URL strategy must be provided"); + } + + if (this.urlStrategy != null) { + throw new IllegalArgumentException("A url strategy has already been configured"); + } + + this.urlStrategy = urlStrategy; + + return this; + } + public ComponentFinderStrategyBuilder forEach(ComponentVisitor componentVisitor) { if (componentVisitor == null) { throw new IllegalArgumentException("A component visitor must be provided"); @@ -148,11 +165,15 @@ public ComponentFinderStrategy build() { descriptionStrategy = new DefaultDescriptionStrategy(); } + if (urlStrategy == null) { + urlStrategy = new DefaultUrlStrategy(); + } + if (componentVisitor == null) { componentVisitor = new DefaultComponentVisitor(); } - return new ComponentFinderStrategy(technology, typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, descriptionStrategy, componentVisitor); + return new ComponentFinderStrategy(technology, typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, descriptionStrategy, urlStrategy, componentVisitor); } @Override @@ -164,6 +185,7 @@ public String toString() { ", supportingTypesStrategy=" + supportingTypesStrategy + ", namingStrategy=" + namingStrategy + ", descriptionStrategy=" + descriptionStrategy + + ", urlStrategy=" + urlStrategy + ", componentVisitor=" + componentVisitor + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java index 79bba4d51..70443932c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java +++ b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java @@ -9,6 +9,7 @@ final class DiscoveredComponent { private final String name; private String description; private String technology; + private String url; private final Set supportingTypes = new HashSet<>(); private ComponentFinderStrategy componentFinderStrategy; @@ -46,6 +47,14 @@ void setTechnology(String technology) { this.technology = technology; } + String getUrl() { + return url; + } + + void setUrl(String url) { + this.url = url; + } + Set getSupportingTypes() { return new HashSet<>(supportingTypes); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java index 6bce2bb1c..96f934f5c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -72,7 +72,7 @@ public void visit(ClassOrInterfaceDeclaration n, Object arg) { if (n.getFullyQualifiedName().isPresent()) { String fullyQualifiedName = n.getFullyQualifiedName().get(); Type type = new Type(fullyQualifiedName); - type.setSource(path.getAbsolutePath()); + type.setSource(relativePath(path)); if (n.getComment().isPresent() && n.getComment().get() instanceof JavadocComment) { JavadocComment javadocComment = (JavadocComment) n.getComment().get(); @@ -93,7 +93,7 @@ public void visit(PackageDeclaration n, Object arg) { String fullyQualifiedName = n.getName().asString() + PACKAGE_INFO_SUFFIX; Type type = new Type(fullyQualifiedName); - type.setSource(path.getAbsolutePath()); + type.setSource(relativePath(path)); Node rootNode = n.findRootNode(); if (rootNode != null && rootNode.getComment().isPresent() && rootNode.getComment().get() instanceof JavadocComment) { @@ -116,4 +116,16 @@ public void visit(PackageDeclaration n, Object arg) { } } + private String relativePath(File path) { + String relativePath = path.getAbsolutePath().replace(directory.getAbsolutePath(), ""); + + String pathSeparator = System.getProperty("file.separator"); + + if (relativePath.startsWith(pathSeparator)) { + relativePath = relativePath.substring(1); + } + + return relativePath; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java new file mode 100644 index 000000000..445b4c4cc --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java @@ -0,0 +1,17 @@ +package com.structurizr.component.url; + +import com.structurizr.component.Type; + +public class DefaultUrlStrategy implements UrlStrategy { + + @Override + public String urlOf(Type type) { + return null; + } + + @Override + public String toString() { + return "DefaultUrlStrategy{}"; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/PrefixUrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixUrlStrategy.java new file mode 100644 index 000000000..503be9b2c --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixUrlStrategy.java @@ -0,0 +1,34 @@ +package com.structurizr.component.url; + +import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; + +public class PrefixUrlStrategy implements UrlStrategy { + + private final String prefix; + + public PrefixUrlStrategy(String prefix) { + if (StringUtils.isNullOrEmpty(prefix)) { + throw new IllegalArgumentException("A prefix must be supplied"); + } + + if (!prefix.endsWith("/")) { + prefix = prefix + "/"; + } + + this.prefix = prefix; + } + + @Override + public String urlOf(Type type) { + return prefix + type.getSource(); + } + + @Override + public String toString() { + return "PrefixUrlStrategy{" + + "prefix='" + prefix + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java new file mode 100644 index 000000000..7d91b07ae --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java @@ -0,0 +1,9 @@ +package com.structurizr.component.url; + +import com.structurizr.component.Type; + +public interface UrlStrategy { + + String urlOf(Type type); + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java index cb770a312..e71590dfe 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -116,9 +116,9 @@ void withDescription_ThrowsAnException_WhenCalledTwice() { } @Test - void forTechnology_ThrowsAnException_WhenPassedNull() { + void withTechnology_ThrowsAnException_WhenPassedNull() { try { - new ComponentFinderStrategyBuilder().forTechnology(null); + new ComponentFinderStrategyBuilder().withTechnology(null); fail(); } catch (Exception e) { assertEquals("A technology must be provided", e.getMessage()); @@ -126,9 +126,9 @@ void forTechnology_ThrowsAnException_WhenPassedNull() { } @Test - void forTechnology_ThrowsAnException_WhenPassedAnEmptyString() { + void withTechnology_ThrowsAnException_WhenPassedAnEmptyString() { try { - new ComponentFinderStrategyBuilder().forTechnology(""); + new ComponentFinderStrategyBuilder().withTechnology(""); fail(); } catch (Exception e) { assertEquals("A technology must be provided", e.getMessage()); @@ -136,9 +136,9 @@ void forTechnology_ThrowsAnException_WhenPassedAnEmptyString() { } @Test - void forTechnology_ThrowsAnException_WhenCalledTwice() { + void withTechnology_ThrowsAnException_WhenCalledTwice() { try { - new ComponentFinderStrategyBuilder().forTechnology("X").forTechnology("Y"); + new ComponentFinderStrategyBuilder().withTechnology("X").withTechnology("Y"); fail(); } catch (Exception e) { assertEquals("A technology has already been configured", e.getMessage()); @@ -158,14 +158,14 @@ void build_ThrowsAnException_WhenATypeMatcherHasNotBeenConfigured() { @Test void build() { ComponentFinderStrategy strategy = new ComponentFinderStrategyBuilder() - .forTechnology("Spring MVC Controller") + .withTechnology("Spring MVC Controller") .matchedBy(new NameSuffixTypeMatcher("Controller")) .filteredBy(new IncludeTypesByRegexFilter("com.example.web.\\.*")) .withName(new TypeNamingStrategy()) .withDescription(new FirstSentenceDescriptionStrategy()) .build(); - assertEquals("ComponentFinderStrategy{technology='Spring MVC Controller', typeMatcher=NameSuffixTypeMatcher{suffix='Controller'}, typeFilter=IncludeTypesByRegexFilter{regex='com.example.web.\\.*'}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=FirstSentenceDescriptionStrategy{}, componentVisitor=DefaultComponentVisitor{}}", strategy.toString()); + assertEquals("ComponentFinderStrategy{technology='Spring MVC Controller', typeMatcher=NameSuffixTypeMatcher{suffix='Controller'}, typeFilter=IncludeTypesByRegexFilter{regex='com.example.web.\\.*'}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=FirstSentenceDescriptionStrategy{}, urlStrategy=DefaultUrlStrategy{}, componentVisitor=DefaultComponentVisitor{}}", strategy.toString()); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index 97d3cbf61..f5b566f8d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -9,6 +9,7 @@ import com.structurizr.component.naming.TypeNamingStrategy; import com.structurizr.component.naming.FullyQualifiedNamingStrategy; import com.structurizr.component.supporting.*; +import com.structurizr.component.url.PrefixUrlStrategy; import java.io.File; import java.util.List; @@ -51,13 +52,17 @@ final class ComponentFinderStrategyParser extends AbstractParser { private static final String DESCRIPTION_GRAMMAR = "description <" + String.join("|", List.of(DESCRIPTION_FIRST_SENTENCE, DESCRIPTION_TRUNCATED)) + ">"; private static final String DESCRIPTION_TRUNCATED_GRAMMAR = "description truncated "; + private static final String URL_PREFIX = "prefix"; + private static final String URL_GRAMMAR = "url <" + String.join("|", List.of(URL_PREFIX)) + ">"; + private static final String URL_PREFIX_GRAMMAR = "url prefix "; + void parseTechnology(ComponentFinderStrategyDslContext context, Tokens tokens) { if (tokens.size() != 2) { throw new RuntimeException("Expected: " + TECHNOLOGY_GRAMMAR); } String name = tokens.get(1); - context.getComponentFinderStrategyBuilder().forTechnology(name); + context.getComponentFinderStrategyBuilder().withTechnology(name); } void parseMatcher(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { @@ -236,4 +241,24 @@ void parseDescription(ComponentFinderStrategyDslContext context, Tokens tokens, } } + void parseUrl(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { + if (tokens.size() < 2) { + throw new RuntimeException("Too few tokens, expected: " + URL_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case URL_PREFIX: + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + URL_PREFIX_GRAMMAR); + } + + String prefix = tokens.get(2); + context.getComponentFinderStrategyBuilder().withUrl(new PrefixUrlStrategy(prefix)); + break; + default: + throw new IllegalArgumentException("Unknown URL strategy: " + type); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 6ed663556..abbaa8a65 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -456,6 +456,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (COMPONENT_FINDER_STRATEGY_DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { new ComponentFinderStrategyParser().parseDescription(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); + } else if (COMPONENT_FINDER_STRATEGY_URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + new ComponentFinderStrategyParser().parseUrl(getContext(ComponentFinderStrategyDslContext.class), tokens, dslFile); + } else if (COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { if (shouldStartContext(tokens)) { startContext(new ComponentFinderStrategyForEachDslContext(getContext(ComponentFinderStrategyDslContext.class), this)); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index f8d11e5ec..fa3829934 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -123,6 +123,7 @@ class StructurizrDslTokens { static final String COMPONENT_FINDER_STRATEGY_SUPPORTING_TYPES_TOKEN = "supportingTypes"; static final String COMPONENT_FINDER_STRATEGY_NAME_TOKEN = "name"; static final String COMPONENT_FINDER_STRATEGY_DESCRIPTION_TOKEN = "description"; + static final String COMPONENT_FINDER_STRATEGY_URL_TOKEN = "url"; static final String COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN = "forEach"; } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index 7dee0f92c..dac5d6733 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -38,7 +38,7 @@ void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseTechnology() { parser.parseTechnology(context, tokens("technology", "name")); - assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -64,7 +64,7 @@ void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "annotation", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=AnnotationTypeMatcher{annotationType='Lcom/example/Component;'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=AnnotationTypeMatcher{annotationType='Lcom/example/Component;'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -80,7 +80,7 @@ void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsedAndThereAreTooFewTokens() @Test void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "extends", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ExtendsTypeMatcher{className='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ExtendsTypeMatcher{className='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -96,7 +96,7 @@ void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "implements", "com.example.Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ImplementsTypeMatcher{interfaceName='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=ImplementsTypeMatcher{interfaceName='com.example.Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -112,7 +112,7 @@ void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsedAndThereAreTooFewTokens @Test void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "name-suffix", "Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -128,7 +128,7 @@ void test_parseMatcher_WhenTheFullyQualifiedNameRegexTypeMatcherIsUsedAndThereAr @Test void test_parseMatcher_WhenTheRegexTypeMatcherIsUsed() { parser.parseMatcher(context, tokens("matcher", "fqn-regex", ".*Component"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=RegexTypeMatcher{regex='.*Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=RegexTypeMatcher{regex='.*Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -144,13 +144,13 @@ void test_parseMatcher_WhenACustomTypeMatcherIsUsedButCannotBeLoaded() { @Test void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithoutParameters() { parser.parseMatcher(context, tokens("matcher", "com.structurizr.dsl.example.CustomTypeMatcher"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=CustomTypeMatcher{}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=CustomTypeMatcher{}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseMatcher_WhenACustomTypeMatcherIsUsedWithAParameter() { parser.parseMatcher(context, tokens("matcher", NameSuffixTypeMatcher.class.getCanonicalName(), "Component"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=NameSuffixTypeMatcher{suffix='Component'}, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -186,13 +186,13 @@ void test_parseFilter_ThrowsAnException_WhenInvalidModeIsSpecified() { @Test void test_parseFilter_WhenIncludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "include", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseFilter_WhenExcludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "exclude", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -218,7 +218,7 @@ void test_parseName_ThrowsAnException_WhenNoTypeIsSpecified() { @Test void test_parseName() { parser.parseName(context, tokens("name", "type-name"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -234,7 +234,7 @@ void test_parseDescription_ThrowsAnException_WhenNoTypeIsSpecified() { @Test void test_parseDescription_FirstSentence() { parser.parseDescription(context, tokens("description", "first-sentence"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=FirstSentenceDescriptionStrategy{}, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=FirstSentenceDescriptionStrategy{}, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test @@ -274,7 +274,33 @@ void test_parseDescription_Truncated_ThrowsAnException_WhenAnInvalidMaxLengthIsS @Test void test_parseDescription_Truncated() { parser.parseDescription(context, tokens("description", "truncated", "50"), null); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=TruncatedDescriptionStrategy{maxLength=50}, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=TruncatedDescriptionStrategy{maxLength=50}, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseUrl_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseUrl(context, tokens("url"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: url ", e.getMessage()); + } + } + + @Test + void test_parseUrl_Prefix_ThrowsAnException_WhenNoPrefixIsSpecified() { + try { + parser.parseUrl(context, tokens("url", "prefix"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: url prefix ", e.getMessage()); + } + } + + @Test + void test_parseUrl_Prefix() { + parser.parseUrl(context, tokens("url", "prefix", "https://example.com"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=PrefixUrlStrategy{prefix='https://example.com/'}, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 27204a8b6..e5f22175b 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1193,7 +1193,7 @@ void springPetClinic() throws Exception { Component welcomeController = webApplication.getComponentWithName("Welcome Controller"); assertNotNull(welcomeController); assertEquals("org.springframework.samples.petclinic.system.WelcomeController", welcomeController.getProperties().get("component.type")); - assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java").getAbsolutePath(), welcomeController.getProperties().get("component.src")); + assertEquals("org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getUrl()); assertSame(welcomeController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.welcomecontroller")); assertTrue(clinicEmployee.hasEfferentRelationshipWith(welcomeController)); @@ -1201,7 +1201,7 @@ void springPetClinic() throws Exception { Component ownerController = webApplication.getComponentWithName("Owner Controller"); assertNotNull(ownerController); assertEquals("org.springframework.samples.petclinic.owner.OwnerController", ownerController.getProperties().get("component.type")); - assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java").getAbsolutePath(), ownerController.getProperties().get("component.src")); + assertEquals("org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getUrl()); assertSame(ownerController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerController")); assertTrue(clinicEmployee.hasEfferentRelationshipWith(ownerController)); @@ -1209,7 +1209,7 @@ void springPetClinic() throws Exception { Component petController = webApplication.getComponentWithName("Pet Controller"); assertNotNull(petController); assertEquals("org.springframework.samples.petclinic.owner.PetController", petController.getProperties().get("component.type")); - assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/PetController.java").getAbsolutePath(), petController.getProperties().get("component.src")); + assertEquals("org/springframework/samples/petclinic/owner/PetController.java", petController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetController.java", petController.getUrl()); assertSame(petController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.petcontroller")); assertTrue(clinicEmployee.hasEfferentRelationshipWith(petController)); @@ -1217,7 +1217,7 @@ void springPetClinic() throws Exception { Component vetController = webApplication.getComponentWithName("Vet Controller"); assertNotNull(vetController); assertEquals("org.springframework.samples.petclinic.vet.VetController", vetController.getProperties().get("component.type")); - assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetController.java").getAbsolutePath(), vetController.getProperties().get("component.src")); + assertEquals("org/springframework/samples/petclinic/vet/VetController.java", vetController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetController.java", vetController.getUrl()); assertSame(vetController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetcontroller")); assertTrue(clinicEmployee.hasEfferentRelationshipWith(vetController)); @@ -1225,7 +1225,7 @@ void springPetClinic() throws Exception { Component visitController = webApplication.getComponentWithName("Visit Controller"); assertNotNull(visitController); assertEquals("org.springframework.samples.petclinic.owner.VisitController", visitController.getProperties().get("component.type")); - assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/VisitController.java").getAbsolutePath(), visitController.getProperties().get("component.src")); + assertEquals("org/springframework/samples/petclinic/owner/VisitController.java", visitController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java", visitController.getUrl()); assertSame(visitController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.visitcontroller")); assertTrue(clinicEmployee.hasEfferentRelationshipWith(visitController)); @@ -1234,7 +1234,7 @@ void springPetClinic() throws Exception { assertNotNull(ownerRepository); assertEquals("Repository class for Owner domain objects All method names are compliant with Spring Data naming conventions so this interface can easily be extended for Spring Data.", ownerRepository.getDescription()); assertEquals("org.springframework.samples.petclinic.owner.OwnerRepository", ownerRepository.getProperties().get("component.type")); - assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java").getAbsolutePath(), ownerRepository.getProperties().get("component.src")); + assertEquals("org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); assertSame(ownerRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerrepository")); assertTrue(ownerRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); @@ -1243,7 +1243,7 @@ void springPetClinic() throws Exception { assertNotNull(vetRepository); assertEquals("Repository class for Vet domain objects All method names are compliant with Spring Data naming conventions so this interface can easily be extended for Spring Data.", vetRepository.getDescription()); assertEquals("org.springframework.samples.petclinic.vet.VetRepository", vetRepository.getProperties().get("component.type")); - assertEquals(new File(springPetClinicHome, "src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java").getAbsolutePath(), vetRepository.getProperties().get("component.src")); + assertEquals("org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getUrl()); assertSame(vetRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetrepository")); assertTrue(vetRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index fd2f20a78..844f372c4 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -26,6 +26,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt technology "Spring MVC Controller" matcher annotation "org.springframework.stereotype.Controller" filter exclude fqn-regex ".*.CrashController" + url prefix "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" forEach { clinicEmployee -> this "Uses" tag "Spring MVC Controller" @@ -35,16 +36,13 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt technology "Spring Data Repository" matcher implements "org.springframework.data.repository.Repository" description first-sentence + url prefix "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" forEach { -> relationalDatabaseSchema "Reads from and writes to" tag "Spring Data Repository" } } } - - !script groovy { - element.components.each { it.url = it.properties["component.src"].replace(System.getenv("SPRING_PETCLINIC_HOME") + "/src/main/java", "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java") } - } } } } From a185ea69da875a9791a647a25baf1a8b8592701e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 17 Sep 2024 13:44:35 +0100 Subject: [PATCH 271/418] "url prefix" -> "url prefix-src" --- .../component/url/DefaultUrlStrategy.java | 3 +++ ...lStrategy.java => PrefixSourceUrlStrategy.java} | 7 +++++-- .../com/structurizr/component/url/UrlStrategy.java | 3 +++ .../dsl/ComponentFinderStrategyParser.java | 14 +++++++------- .../dsl/ComponentFinderStrategyParserTests.java | 8 ++++---- .../resources/dsl/spring-petclinic/workspace.dsl | 4 ++-- 6 files changed, 24 insertions(+), 15 deletions(-) rename structurizr-component/src/main/java/com/structurizr/component/url/{PrefixUrlStrategy.java => PrefixSourceUrlStrategy.java} (78%) diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java index 445b4c4cc..2e6fa6d1e 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java @@ -2,6 +2,9 @@ import com.structurizr.component.Type; +/** + * Generates a null URL. + */ public class DefaultUrlStrategy implements UrlStrategy { @Override diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/PrefixUrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java similarity index 78% rename from structurizr-component/src/main/java/com/structurizr/component/url/PrefixUrlStrategy.java rename to structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java index 503be9b2c..6419161d5 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/url/PrefixUrlStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java @@ -3,11 +3,14 @@ import com.structurizr.component.Type; import com.structurizr.util.StringUtils; -public class PrefixUrlStrategy implements UrlStrategy { +/** + * Adds a given prefix to the component source location. + */ +public class PrefixSourceUrlStrategy implements UrlStrategy { private final String prefix; - public PrefixUrlStrategy(String prefix) { + public PrefixSourceUrlStrategy(String prefix) { if (StringUtils.isNullOrEmpty(prefix)) { throw new IllegalArgumentException("A prefix must be supplied"); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java index 7d91b07ae..e066b3c1f 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java @@ -2,6 +2,9 @@ import com.structurizr.component.Type; +/** + * Provides a way customise how component URLs are generated. + */ public interface UrlStrategy { String urlOf(Type type); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index f5b566f8d..e84800882 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -9,7 +9,7 @@ import com.structurizr.component.naming.TypeNamingStrategy; import com.structurizr.component.naming.FullyQualifiedNamingStrategy; import com.structurizr.component.supporting.*; -import com.structurizr.component.url.PrefixUrlStrategy; +import com.structurizr.component.url.PrefixSourceUrlStrategy; import java.io.File; import java.util.List; @@ -52,9 +52,9 @@ final class ComponentFinderStrategyParser extends AbstractParser { private static final String DESCRIPTION_GRAMMAR = "description <" + String.join("|", List.of(DESCRIPTION_FIRST_SENTENCE, DESCRIPTION_TRUNCATED)) + ">"; private static final String DESCRIPTION_TRUNCATED_GRAMMAR = "description truncated "; - private static final String URL_PREFIX = "prefix"; - private static final String URL_GRAMMAR = "url <" + String.join("|", List.of(URL_PREFIX)) + ">"; - private static final String URL_PREFIX_GRAMMAR = "url prefix "; + private static final String URL_PREFIX_SRC = "prefix-src"; + private static final String URL_GRAMMAR = "url <" + String.join("|", List.of(URL_PREFIX_SRC)) + ">"; + private static final String URL_PREFIX_SRC_GRAMMAR = "url prefix-src "; void parseTechnology(ComponentFinderStrategyDslContext context, Tokens tokens) { if (tokens.size() != 2) { @@ -248,13 +248,13 @@ void parseUrl(ComponentFinderStrategyDslContext context, Tokens tokens, File dsl String type = tokens.get(1).toLowerCase(); switch (type) { - case URL_PREFIX: + case URL_PREFIX_SRC: if (tokens.size() < 3) { - throw new RuntimeException("Too few tokens, expected: " + URL_PREFIX_GRAMMAR); + throw new RuntimeException("Too few tokens, expected: " + URL_PREFIX_SRC_GRAMMAR); } String prefix = tokens.get(2); - context.getComponentFinderStrategyBuilder().withUrl(new PrefixUrlStrategy(prefix)); + context.getComponentFinderStrategyBuilder().withUrl(new PrefixSourceUrlStrategy(prefix)); break; default: throw new IllegalArgumentException("Unknown URL strategy: " + type); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index dac5d6733..17d9cf0a7 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -283,23 +283,23 @@ void test_parseUrl_ThrowsAnException_WhenNoTypeIsSpecified() { parser.parseUrl(context, tokens("url"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: url ", e.getMessage()); + assertEquals("Too few tokens, expected: url ", e.getMessage()); } } @Test void test_parseUrl_Prefix_ThrowsAnException_WhenNoPrefixIsSpecified() { try { - parser.parseUrl(context, tokens("url", "prefix"), null); + parser.parseUrl(context, tokens("url", "prefix-src"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: url prefix ", e.getMessage()); + assertEquals("Too few tokens, expected: url prefix-src ", e.getMessage()); } } @Test void test_parseUrl_Prefix() { - parser.parseUrl(context, tokens("url", "prefix", "https://example.com"), null); + parser.parseUrl(context, tokens("url", "prefix-src", "https://example.com"), null); assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=PrefixUrlStrategy{prefix='https://example.com/'}, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index 844f372c4..3638bcc0f 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -26,7 +26,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt technology "Spring MVC Controller" matcher annotation "org.springframework.stereotype.Controller" filter exclude fqn-regex ".*.CrashController" - url prefix "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" + url prefix-src "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" forEach { clinicEmployee -> this "Uses" tag "Spring MVC Controller" @@ -36,7 +36,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt technology "Spring Data Repository" matcher implements "org.springframework.data.repository.Repository" description first-sentence - url prefix "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" + url prefix-src "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" forEach { -> relationalDatabaseSchema "Reads from and writes to" tag "Spring Data Repository" From 069f6ea46dd0005a70d801e08e587f0da5c72b17 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 17 Sep 2024 13:44:44 +0100 Subject: [PATCH 272/418] Docs. --- .../structurizr/component/description/DescriptionStrategy.java | 3 +++ .../java/com/structurizr/component/naming/NamingStrategy.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java index c255a73f4..cf8129d2c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java @@ -2,6 +2,9 @@ import com.structurizr.component.Type; +/** + * Provides a way customise how component descriptions are generated. + */ public interface DescriptionStrategy { String descriptionOf(Type type); diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java index 35f0efab5..1520d7313 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/NamingStrategy.java @@ -3,7 +3,7 @@ import com.structurizr.component.Type; /** - * Provides a way to map a fully qualified type name to a component name. + * Provides a way customise how component names are generated. */ public interface NamingStrategy { From 9529188608e5ad08efc3a25a0fcea5d7933bcb96 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 17 Sep 2024 14:39:18 +0100 Subject: [PATCH 273/418] Adds a full component finder test. --- .../component/SpringPetClinicTests.java | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java diff --git a/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java new file mode 100644 index 000000000..e8d70c0f9 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java @@ -0,0 +1,129 @@ +package com.structurizr.component; + +import com.structurizr.Workspace; +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +import com.structurizr.component.filter.ExcludeTypesByRegexFilter; +import com.structurizr.component.matcher.AnnotationTypeMatcher; +import com.structurizr.component.matcher.ImplementsTypeMatcher; +import com.structurizr.component.url.PrefixSourceUrlStrategy; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.util.StringUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class SpringPetClinicTests { + + @Test + void springPetClinic() { + String springPetClinicHome = System.getenv().getOrDefault("SPRING_PETCLINIC_HOME", ""); + System.out.println(springPetClinicHome); + if (!StringUtils.isNullOrEmpty(springPetClinicHome)) { + System.out.println("Running Spring PetClinic example..."); + + Workspace workspace = new Workspace("Spring PetClinic", "Description"); + Person clinicEmployee = workspace.getModel().addPerson("Clinic Employee"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Spring PetClinic"); + Container webApplication = softwareSystem.addContainer("Web Application"); + Container relationalDatabaseSchema = softwareSystem.addContainer("Relational Database Schema"); + + ComponentFinder componentFinder = new ComponentFinderBuilder() + .forContainer(webApplication) + .fromClasses(new File(springPetClinicHome, "target/spring-petclinic-3.3.0-SNAPSHOT.jar")) + .fromSource(new File(springPetClinicHome, "src/main/java")) + .withStrategy( + new ComponentFinderStrategyBuilder() + .matchedBy(new AnnotationTypeMatcher("org.springframework.stereotype.Controller")) + .filteredBy(new ExcludeTypesByRegexFilter(".*.CrashController")) + .withTechnology("Spring MVC Controller") + .withUrl(new PrefixSourceUrlStrategy("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java")) + .forEach((component -> { + clinicEmployee.uses(component, "Uses"); + component.addTags(component.getTechnology()); + })) + .build() + ) + .withStrategy( + new ComponentFinderStrategyBuilder() + .matchedBy(new ImplementsTypeMatcher("org.springframework.data.repository.Repository")) + .withDescription(new FirstSentenceDescriptionStrategy()) + .withTechnology("Spring Data Repository") + .withUrl(new PrefixSourceUrlStrategy("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java")) + .forEach((component -> { + component.uses(relationalDatabaseSchema, "Reads from and writes to"); + component.addTags(component.getTechnology()); + })) + .build() + ) + .build(); + + componentFinder.findComponents(); + assertEquals(7, webApplication.getComponents().size()); + + Component welcomeController = webApplication.getComponentWithName("Welcome Controller"); + assertNotNull(welcomeController); + assertEquals("org.springframework.samples.petclinic.system.WelcomeController", welcomeController.getProperties().get("component.type")); + assertEquals("org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getUrl()); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(welcomeController)); + + Component ownerController = webApplication.getComponentWithName("Owner Controller"); + assertNotNull(ownerController); + assertEquals("org.springframework.samples.petclinic.owner.OwnerController", ownerController.getProperties().get("component.type")); + assertEquals("org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getUrl()); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(ownerController)); + + Component petController = webApplication.getComponentWithName("Pet Controller"); + assertNotNull(petController); + assertEquals("org.springframework.samples.petclinic.owner.PetController", petController.getProperties().get("component.type")); + assertEquals("org/springframework/samples/petclinic/owner/PetController.java", petController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetController.java", petController.getUrl()); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(petController)); + + Component vetController = webApplication.getComponentWithName("Vet Controller"); + assertNotNull(vetController); + assertEquals("org.springframework.samples.petclinic.vet.VetController", vetController.getProperties().get("component.type")); + assertEquals("org/springframework/samples/petclinic/vet/VetController.java", vetController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetController.java", vetController.getUrl()); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(vetController)); + + Component visitController = webApplication.getComponentWithName("Visit Controller"); + assertNotNull(visitController); + assertEquals("org.springframework.samples.petclinic.owner.VisitController", visitController.getProperties().get("component.type")); + assertEquals("org/springframework/samples/petclinic/owner/VisitController.java", visitController.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java", visitController.getUrl()); + assertTrue(clinicEmployee.hasEfferentRelationshipWith(visitController)); + + Component ownerRepository = webApplication.getComponentWithName("Owner Repository"); + assertNotNull(ownerRepository); + assertEquals("Repository class for Owner domain objects All method names are compliant with Spring Data naming conventions so this interface can easily be extended for Spring Data.", ownerRepository.getDescription()); + assertEquals("org.springframework.samples.petclinic.owner.OwnerRepository", ownerRepository.getProperties().get("component.type")); + assertEquals("org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); + assertTrue(ownerRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); + + Component vetRepository = webApplication.getComponentWithName("Vet Repository"); + assertNotNull(vetRepository); + assertEquals("Repository class for Vet domain objects All method names are compliant with Spring Data naming conventions so this interface can easily be extended for Spring Data.", vetRepository.getDescription()); + assertEquals("org.springframework.samples.petclinic.vet.VetRepository", vetRepository.getProperties().get("component.type")); + assertEquals("org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getProperties().get("component.src")); + assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getUrl()); + assertTrue(vetRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); + + assertTrue(welcomeController.getRelationships().isEmpty()); + assertNotNull(petController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(visitController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(ownerController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(vetController.getEfferentRelationshipWith(vetRepository)); + } else { + System.out.println("Skipping Spring PetClinic example..."); + } + } + +} From 63cb000ef854a9a1dc8bcf2c8782495be86d8dd4 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 15:08:31 +0100 Subject: [PATCH 274/418] A few fixes, plus some refactoring to make it easier to debug what's happening when the component finder is running. --- logging.properties | 12 +++ settings.gradle | 1 + structurizr-annotation/README.md | 4 + structurizr-annotation/build.gradle | 3 + .../structurizr/annotation/Properties.java | 15 ++++ .../com/structurizr/annotation/Property.java | 17 ++++ .../java/com/structurizr/annotation/Tag.java | 16 ++++ .../java/com/structurizr/annotation/Tags.java | 15 ++++ structurizr-component/build.gradle | 1 + .../component/ComponentFinder.java | 90 ++++++------------- .../component/ComponentFinderBuilder.java | 25 +++++- .../component/ComponentFinderStrategy.java | 38 +++++++- .../component/DiscoveredComponent.java | 21 ++++- .../java/com/structurizr/component/Type.java | 65 +++++++++++++- .../component/TypeDependencyFinder.java | 53 +++++++++++ .../com/structurizr/component/TypeFinder.java | 46 ++++++++++ .../FirstSentenceDescriptionStrategy.java | 8 +- ...ExcludeFullyQualifiedNameRegexFilter.java} | 6 +- ...IncludeFullyQualifiedNameRegexFilter.java} | 6 +- .../provider/ClassDirectoryTypeProvider.java | 6 ++ .../provider/ClassJarFileTypeProvider.java | 7 ++ .../provider/SourceDirectoryTypeProvider.java | 7 ++ .../ComponentFinderStrategyBuilderTests.java | 10 +-- .../component/ComponentFinderTests.java | 64 +++++++++++++ .../component/SpringPetClinicTests.java | 8 +- .../com/structurizr/component/TypeTests.java | 57 +++++++++++- ...FirstSentenceDescriptionStrategyTests.java | 15 ++++ .../component/example/Controller.java | 4 + .../component/example/ExampleController.java | 12 +++ .../component/example/ExampleRepository.java | 4 + .../component/example/Repository.java | 4 + ...deFullyQualifiedNameRegexFilterTests.java} | 12 +-- ...deFullyQualifiedNameRegexFilterTests.java} | 12 +-- .../ClassDirectoryTypeProviderTests.java | 2 +- .../component/types/TypeWithProperties.java | 9 ++ .../component/types/TypeWithProperty.java | 7 ++ .../component/types/TypeWithTag.java | 7 ++ .../component/types/TypeWithTags.java | 9 ++ .../dsl/ComponentFinderDslContext.java | 2 +- .../dsl/ComponentFinderParser.java | 38 ++++++++ .../dsl/ComponentFinderStrategyParser.java | 8 +- .../structurizr/dsl/StructurizrDslParser.java | 3 + .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../dsl/ComponentFinderParserTests.java | 59 ++++++++++++ .../ComponentFinderStrategyParserTests.java | 4 +- .../dsl/spring-petclinic/workspace.dsl | 1 + 46 files changed, 706 insertions(+), 108 deletions(-) create mode 100644 logging.properties create mode 100644 structurizr-annotation/README.md create mode 100644 structurizr-annotation/build.gradle create mode 100644 structurizr-annotation/src/main/java/com/structurizr/annotation/Properties.java create mode 100644 structurizr-annotation/src/main/java/com/structurizr/annotation/Property.java create mode 100644 structurizr-annotation/src/main/java/com/structurizr/annotation/Tag.java create mode 100644 structurizr-annotation/src/main/java/com/structurizr/annotation/Tags.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/TypeDependencyFinder.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java rename structurizr-component/src/main/java/com/structurizr/component/filter/{ExcludeTypesByRegexFilter.java => ExcludeFullyQualifiedNameRegexFilter.java} (77%) rename structurizr-component/src/main/java/com/structurizr/component/filter/{IncludeTypesByRegexFilter.java => IncludeFullyQualifiedNameRegexFilter.java} (77%) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/Controller.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/ExampleController.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/ExampleRepository.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/example/Repository.java rename structurizr-component/src/test/java/com/structurizr/component/filter/{ExcludeTypesByRegexFilterTests.java => ExcludeFullyQualifiedNameRegexFilterTests.java} (60%) rename structurizr-component/src/test/java/com/structurizr/component/filter/{IncludeTypesByRegexFilterTests.java => IncludeFullyQualifiedNameRegexFilterTests.java} (60%) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperties.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperty.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTag.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTags.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java diff --git a/logging.properties b/logging.properties new file mode 100644 index 000000000..f6c3fb46c --- /dev/null +++ b/logging.properties @@ -0,0 +1,12 @@ +# Logging +handlers = java.util.logging.ConsoleHandler +.level = INFO + +java.util.logging.SimpleFormatter.format=%2$s: %5$s%n + +java.util.logging.ConsoleHandler.level = ALL + +com.structurizr.component.ComponentFinder.level = ALL +com.structurizr.component.TypeFinder.level = ALL +com.structurizr.component.TypeDependencyFinder.level = ALL +com.structurizr.component.ComponentFinderStrategy.level = ALL diff --git a/settings.gradle b/settings.gradle index 8035bdd02..092c80c08 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'structurizr-java' +include 'structurizr-annotation' include 'structurizr-autolayout' include 'structurizr-client' include 'structurizr-component' diff --git a/structurizr-annotation/README.md b/structurizr-annotation/README.md new file mode 100644 index 000000000..a8626a374 --- /dev/null +++ b/structurizr-annotation/README.md @@ -0,0 +1,4 @@ +# structurizr-annotation + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-annotation.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-annotation) + diff --git a/structurizr-annotation/build.gradle b/structurizr-annotation/build.gradle new file mode 100644 index 000000000..0ce6a1661 --- /dev/null +++ b/structurizr-annotation/build.gradle @@ -0,0 +1,3 @@ +dependencies { + +} \ No newline at end of file diff --git a/structurizr-annotation/src/main/java/com/structurizr/annotation/Properties.java b/structurizr-annotation/src/main/java/com/structurizr/annotation/Properties.java new file mode 100644 index 000000000..b477c2dd1 --- /dev/null +++ b/structurizr-annotation/src/main/java/com/structurizr/annotation/Properties.java @@ -0,0 +1,15 @@ +package com.structurizr.annotation; + +import java.lang.annotation.*; + +/** + * A wrapper for @Property annotations. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Properties { + + Property[] value(); + +} \ No newline at end of file diff --git a/structurizr-annotation/src/main/java/com/structurizr/annotation/Property.java b/structurizr-annotation/src/main/java/com/structurizr/annotation/Property.java new file mode 100644 index 000000000..405f62414 --- /dev/null +++ b/structurizr-annotation/src/main/java/com/structurizr/annotation/Property.java @@ -0,0 +1,17 @@ +package com.structurizr.annotation; + +import java.lang.annotation.*; + +/** + * A type-level annotation that can be used to add a name-value property to the model element represented by the type. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(Properties.class) +public @interface Property { + + String name(); + String value(); + +} \ No newline at end of file diff --git a/structurizr-annotation/src/main/java/com/structurizr/annotation/Tag.java b/structurizr-annotation/src/main/java/com/structurizr/annotation/Tag.java new file mode 100644 index 000000000..5da192346 --- /dev/null +++ b/structurizr-annotation/src/main/java/com/structurizr/annotation/Tag.java @@ -0,0 +1,16 @@ +package com.structurizr.annotation; + +import java.lang.annotation.*; + +/** + * A type-level annotation that can be used to add a tag to the model element represented by the type. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(Tags.class) +public @interface Tag { + + String name(); + +} \ No newline at end of file diff --git a/structurizr-annotation/src/main/java/com/structurizr/annotation/Tags.java b/structurizr-annotation/src/main/java/com/structurizr/annotation/Tags.java new file mode 100644 index 000000000..0af4f4b2f --- /dev/null +++ b/structurizr-annotation/src/main/java/com/structurizr/annotation/Tags.java @@ -0,0 +1,15 @@ +package com.structurizr.annotation; + +import java.lang.annotation.*; + +/** + * A wrapper for @Tag annotations. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Tags { + + Tag[] value(); + +} \ No newline at end of file diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle index 3a2fd6c68..6c49d8797 100644 --- a/structurizr-component/build.gradle +++ b/structurizr-component/build.gradle @@ -4,6 +4,7 @@ dependencies { implementation 'org.apache.bcel:bcel:6.8.1' implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.1' + testImplementation project(':structurizr-annotation') testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index 1055c453e..c73e0228b 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -1,13 +1,11 @@ package com.structurizr.component; +import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.provider.TypeProvider; import com.structurizr.model.Component; import com.structurizr.model.Container; import com.structurizr.util.StringUtils; import org.apache.bcel.Repository; -import org.apache.bcel.classfile.ConstantPool; -import org.apache.bcel.classfile.Method; -import org.apache.bcel.generic.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -28,83 +26,40 @@ public final class ComponentFinder { private final Container container; private final List componentFinderStrategies = new ArrayList<>(); - ComponentFinder(Container container, Collection typeProviders, List componentFinderStrategies) { + ComponentFinder(Container container, TypeFilter typeFilter, Collection typeProviders, List componentFinderStrategies) { this.container = container; this.componentFinderStrategies.addAll(componentFinderStrategies); - findTypes(typeProviders); - } - - private void findTypes(Collection typeProviders) { + log.debug("Initialising component finder:"); + log.debug(" - for: " + container.getCanonicalName()); for (TypeProvider typeProvider : typeProviders) { - Set types = typeProvider.getTypes(); - for (com.structurizr.component.Type type : types) { - if (type.getJavaClass() != null) { - // this is the BCEL identified type - typeRepository.add(type); - } else { - // this is the source code identified type - com.structurizr.component.Type bcelType = typeRepository.getType(type.getFullyQualifiedName()); - if (bcelType != null) { - bcelType.setDescription(type.getDescription()); - bcelType.setSource(type.getSource()); - } - } - } + log.debug(" - from: " + typeProvider); + } + log.debug(" - filtered by: " + typeFilter); + for (ComponentFinderStrategy strategy : componentFinderStrategies) { + log.debug(" - with strategy: " + strategy); } + new TypeFinder().run(typeProviders, typeFilter, typeRepository); Repository.clearCache(); - for (com.structurizr.component.Type type : typeRepository.getTypes()) { + for (Type type : typeRepository.getTypes()) { if (type.getJavaClass() != null) { Repository.addClass(type.getJavaClass()); - findDependencies(type); - } - } - } - - private void findDependencies(com.structurizr.component.Type type) { - ConstantPool cp = type.getJavaClass().getConstantPool(); - ConstantPoolGen cpg = new ConstantPoolGen(cp); - for (Method m : type.getJavaClass().getMethods()) { - MethodGen mg = new MethodGen(m, type.getJavaClass().getClassName(), cpg); - InstructionList il = mg.getInstructionList(); - if (il == null) { - continue; - } - - InstructionHandle[] instructionHandles = il.getInstructionHandles(); - for (InstructionHandle instructionHandle : instructionHandles) { - Instruction instruction = instructionHandle.getInstruction(); - if (!(instruction instanceof InvokeInstruction)) { - continue; - } - - InvokeInstruction invokeInstruction = (InvokeInstruction)instruction; - ReferenceType referenceType = invokeInstruction.getReferenceType(cpg); - if (!(referenceType instanceof ObjectType)) { - continue; - } - - ObjectType objectType = (ObjectType)referenceType; - String referencedClassName = objectType.getClassName(); - com.structurizr.component.Type referencedType = typeRepository.getType(referencedClassName); - if (referencedType != null) { - type.addDependency(referencedType); - } + new TypeDependencyFinder().run(type, typeRepository); } } } /** - * Find components, using all configured rules, in the order they were added. + * Find components, using all configured strategies, in the order they were added. */ - public Set findComponents() { + public Set run() { Set discoveredComponents = new LinkedHashSet<>(); Map componentMap = new HashMap<>(); Set componentSet = new LinkedHashSet<>(); for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - Set set = componentFinderStrategy.findComponents(typeRepository); + Set set = componentFinderStrategy.run(typeRepository); if (set.isEmpty()) { throw new RuntimeException("No components were found by " + componentFinderStrategy); } @@ -120,28 +75,41 @@ public Set findComponents() { component.setDescription(discoveredComponent.getDescription()); component.setTechnology(discoveredComponent.getTechnology()); component.setUrl(discoveredComponent.getUrl()); + + component.addTags(discoveredComponent.getTags().toArray(new String[0])); + for (String name : discoveredComponent.getProperties().keySet()) { + component.addProperty(name, discoveredComponent.getProperties().get(name)); + } + componentMap.put(discoveredComponent, component); componentSet.add(component); } // find dependencies between all components for (DiscoveredComponent discoveredComponent : discoveredComponents) { + Component component = componentMap.get(discoveredComponent); + log.debug("Component dependencies for \"" + component.getName() + "\":"); Set typeDependencies = discoveredComponent.getAllDependencies(); for (Type typeDependency : typeDependencies) { for (DiscoveredComponent c : discoveredComponents) { if (c != discoveredComponent) { if (c.getAllTypes().contains(typeDependency)) { Component componentDependency = componentMap.get(c); - componentMap.get(discoveredComponent).uses(componentDependency, ""); + log.debug(" -> " + componentDependency.getName()); + component.uses(componentDependency, ""); } } } } + if (component.getRelationships().isEmpty()) { + log.debug(" - none"); + } } // now visit all components for (DiscoveredComponent discoveredComponent : componentMap.keySet()) { Component component = componentMap.get(discoveredComponent); + log.debug("Visiting \"" + component.getName() + "\""); discoveredComponent.getComponentFinderStrategy().visit(component); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java index 86cd0a843..21f1a37e1 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java @@ -1,5 +1,7 @@ package com.structurizr.component; +import com.structurizr.component.filter.DefaultTypeFilter; +import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.provider.ClassDirectoryTypeProvider; import com.structurizr.component.provider.ClassJarFileTypeProvider; import com.structurizr.component.provider.SourceDirectoryTypeProvider; @@ -19,6 +21,7 @@ public class ComponentFinderBuilder { private Container container; private final List typeProviders = new ArrayList<>(); + private TypeFilter typeFilter; private final List componentFinderStrategies = new ArrayList<>(); public ComponentFinderBuilder forContainer(Container container) { @@ -57,6 +60,12 @@ public ComponentFinderBuilder fromSource(File path) { return this; } + public ComponentFinderBuilder filteredBy(TypeFilter typeFilter) { + this.typeFilter = typeFilter; + + return this; + } + public ComponentFinderBuilder withStrategy(ComponentFinderStrategy componentFinderStrategy) { this.componentFinderStrategies.add(componentFinderStrategy); @@ -72,11 +81,25 @@ public ComponentFinder build() { throw new RuntimeException("One or more type providers must be configured"); } + if (typeFilter == null) { + typeFilter = new DefaultTypeFilter(); + } + if (componentFinderStrategies.isEmpty()) { throw new RuntimeException("One or more component finder strategies must be configured"); } - return new ComponentFinder(container, typeProviders, componentFinderStrategies); + return new ComponentFinder(container, typeFilter, typeProviders, componentFinderStrategies); + } + + @Override + public String toString() { + return "ComponentFinderBuilder{" + + "container=" + container + + ", typeProviders=" + typeProviders + + ", typeFilter=" + typeFilter + + ", componentFinderStrategies=" + componentFinderStrategies + + '}'; } } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index d1dc8f87a..6862e7abe 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -8,8 +8,11 @@ import com.structurizr.component.url.UrlStrategy; import com.structurizr.component.visitor.ComponentVisitor; import com.structurizr.model.Component; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; /** @@ -23,6 +26,8 @@ */ public final class ComponentFinderStrategy { + private static final Log log = LogFactory.getLog(ComponentFinderStrategy.class); + private final String technology; private final TypeMatcher typeMatcher; private final TypeFilter typeFilter; @@ -43,22 +48,49 @@ public final class ComponentFinderStrategy { this.componentVisitor = componentVisitor; } - Set findComponents(TypeRepository typeRepository) { + Set run(TypeRepository typeRepository) { Set components = new LinkedHashSet<>(); + log.debug("Running " + this.toString()); Set types = typeRepository.getTypes(); for (Type type : types) { - if (typeMatcher.matches(type) && typeFilter.accept(type)) { + + boolean matched = typeMatcher.matches(type); + boolean accepted = typeFilter.accept(type); + + if (matched) { + if (accepted) { + log.debug(" + " + type.getFullyQualifiedName() + " (matched=true, accepted=true)"); + } else { + log.debug(" - " + type.getFullyQualifiedName() + " (matched=true, accepted=false)"); + } + } else { + log.debug(" - " + type.getFullyQualifiedName() + " (matched=false)"); + } + + if (matched && accepted) { DiscoveredComponent component = new DiscoveredComponent(namingStrategy.nameOf(type), type); component.setDescription(descriptionStrategy.descriptionOf(type)); component.setTechnology(this.technology); component.setUrl(urlStrategy.urlOf(type)); + component.addTags(type.getTags()); + Map properties = type.getProperties(); + for (String name : properties.keySet()) { + component.addProperty(name, properties.get(name)); + } component.setComponentFinderStrategy(this); components.add(component); // now find supporting types Set supportingTypes = supportingTypesStrategy.findSupportingTypes(type, typeRepository); - component.addSupportingTypes(supportingTypes); + if (supportingTypes.isEmpty()) { + log.debug(" - none"); + } else { + for (Type supportingType : supportingTypes) { + log.debug(" + supporting type: " + supportingType.getFullyQualifiedName()); + } + component.addSupportingTypes(supportingTypes); + } } } diff --git a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java index 70443932c..cd77da3c1 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java +++ b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java @@ -1,7 +1,6 @@ package com.structurizr.component; -import java.util.HashSet; -import java.util.Set; +import java.util.*; final class DiscoveredComponent { @@ -10,6 +9,8 @@ final class DiscoveredComponent { private String description; private String technology; private String url; + private final List tags = new ArrayList<>(); + private final Map properties = new HashMap<>(); private final Set supportingTypes = new HashSet<>(); private ComponentFinderStrategy componentFinderStrategy; @@ -55,6 +56,22 @@ void setUrl(String url) { this.url = url; } + void addTags(List tags) { + this.tags.addAll(tags); + } + + List getTags() { + return List.copyOf(tags); + } + + void addProperty(String key, String value) { + properties.put(key, value); + } + + Map getProperties() { + return Map.copyOf(properties); + } + Set getSupportingTypes() { return new HashSet<>(supportingTypes); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index 3c9fafe01..f267a0e82 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -1,17 +1,21 @@ package com.structurizr.component; import com.structurizr.util.StringUtils; -import org.apache.bcel.classfile.JavaClass; +import org.apache.bcel.classfile.*; -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; /** * Represents a Java type (e.g. class or interface) - it's a wrapper around a BCEL JavaClass. */ public class Type { + private static final String STRUCTURIZR_TAG_ANNOTATION = "Lcom/structurizr/annotation/Tag;"; + private static final String STRUCTURIZR_TAGS_ANNOTATION = "Lcom/structurizr/annotation/Tags;"; + + private static final String STRUCTURIZR_PROPERTY_ANNOTATION = "Lcom/structurizr/annotation/Property;"; + private static final String STRUCTURIZR_PROPERTIES_ANNOTATION = "Lcom/structurizr/annotation/Properties;"; + private final JavaClass javaClass; private final String fullyQualifiedName; private String description; @@ -76,10 +80,63 @@ public Set getDependencies() { return new LinkedHashSet<>(dependencies); } + public boolean hasDependency(Type type) { + return dependencies.contains(type); + } + public boolean isAbstractClass() { return javaClass.isAbstract() && javaClass.isClass(); } + public List getTags() { + List tags = new ArrayList<>(); + + AnnotationEntry[] annotationEntries = javaClass.getAnnotationEntries(); + for (AnnotationEntry annotationEntry : annotationEntries) { + if (STRUCTURIZR_TAG_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + ElementValuePair elementValuePair = annotationEntry.getElementValuePairs()[0]; + String tag = elementValuePair.getValue().stringifyValue(); + tags.add(tag); + } else if (STRUCTURIZR_TAGS_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + ElementValuePair elementValuePair = annotationEntry.getElementValuePairs()[0]; + ArrayElementValue elementValue = (ArrayElementValue)elementValuePair.getValue(); + for (ElementValue value : elementValue.getElementValuesArray()) { + AnnotationElementValue annotationElementValue = (AnnotationElementValue)value; + AnnotationEntry tagAannotationEntry = annotationElementValue.getAnnotationEntry(); + String tag = tagAannotationEntry.getElementValuePairs()[0].getValue().stringifyValue(); + tags.add(tag); + } + } + } + + return tags; + } + + public Map getProperties() { + Map properties = new HashMap<>(); + + AnnotationEntry[] annotationEntries = javaClass.getAnnotationEntries(); + for (AnnotationEntry annotationEntry : annotationEntries) { + if (STRUCTURIZR_PROPERTY_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + String name = annotationEntry.getElementValuePairs()[0].getValue().stringifyValue(); + String value = annotationEntry.getElementValuePairs()[1].getValue().stringifyValue(); + properties.put(name, value); + } else if (STRUCTURIZR_PROPERTIES_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + ArrayElementValue arrayElementValue = (ArrayElementValue)annotationEntry.getElementValuePairs()[0].getValue(); + for (ElementValue elementValue : arrayElementValue.getElementValuesArray()) { + AnnotationElementValue annotationElementValue = (AnnotationElementValue)elementValue; + AnnotationEntry tagAannotationEntry = annotationElementValue.getAnnotationEntry(); + + String name = tagAannotationEntry.getElementValuePairs()[0].getValue().stringifyValue(); + String value = tagAannotationEntry.getElementValuePairs()[1].getValue().stringifyValue(); + properties.put(name, value); + } + } + } + + return properties; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeDependencyFinder.java b/structurizr-component/src/main/java/com/structurizr/component/TypeDependencyFinder.java new file mode 100644 index 000000000..2b8352b4d --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeDependencyFinder.java @@ -0,0 +1,53 @@ +package com.structurizr.component; + +import org.apache.bcel.classfile.ConstantPool; +import org.apache.bcel.classfile.Method; +import org.apache.bcel.generic.*; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +class TypeDependencyFinder { + + private static final Log log = LogFactory.getLog(TypeDependencyFinder.class); + + void run(Type type, TypeRepository typeRepository) { + log.debug("Type dependencies for " + type.getFullyQualifiedName() + ":"); + ConstantPool cp = type.getJavaClass().getConstantPool(); + ConstantPoolGen cpg = new ConstantPoolGen(cp); + for (Method m : type.getJavaClass().getMethods()) { + MethodGen mg = new MethodGen(m, type.getJavaClass().getClassName(), cpg); + InstructionList il = mg.getInstructionList(); + if (il == null) { + continue; + } + + InstructionHandle[] instructionHandles = il.getInstructionHandles(); + for (InstructionHandle instructionHandle : instructionHandles) { + Instruction instruction = instructionHandle.getInstruction(); + if (!(instruction instanceof InvokeInstruction)) { + continue; + } + + InvokeInstruction invokeInstruction = (InvokeInstruction)instruction; + ReferenceType referenceType = invokeInstruction.getReferenceType(cpg); + if (!(referenceType instanceof ObjectType)) { + continue; + } + + ObjectType objectType = (ObjectType)referenceType; + String referencedClassName = objectType.getClassName(); + com.structurizr.component.Type referencedType = typeRepository.getType(referencedClassName); + if (referencedType != null && !referencedType.getFullyQualifiedName().equals(type.getFullyQualifiedName()) && !type.hasDependency(referencedType)) { + log.debug(" + " + referencedType.getFullyQualifiedName()); + + type.addDependency(referencedType); + } + } + } + + if (type.getDependencies().isEmpty()) { + log.debug(" - none"); + } + } + +} diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java b/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java new file mode 100644 index 000000000..d1b843464 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java @@ -0,0 +1,46 @@ +package com.structurizr.component; + +import com.structurizr.component.filter.TypeFilter; +import com.structurizr.component.provider.TypeProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.Collection; +import java.util.Set; + +class TypeFinder { + + private static final Log log = LogFactory.getLog(TypeFinder.class); + + void run(Collection typeProviders, TypeFilter typeFilter, TypeRepository typeRepository) { + for (TypeProvider typeProvider : typeProviders) { + log.debug("Running " + typeProvider.toString()); + + Set types = typeProvider.getTypes(); + for (com.structurizr.component.Type type : types) { + + boolean accepted = typeFilter.accept(type); + if (accepted) { + log.debug(" + " + type.getFullyQualifiedName() + " (accepted=true)"); + } else { + log.debug(" - " + type.getFullyQualifiedName() + " (accepted=false)"); + } + + if (accepted) { + if (type.getJavaClass() != null) { + // this is the BCEL identified type + typeRepository.add(type); + } else { + // this is the source code identified type + Type bcelType = typeRepository.getType(type.getFullyQualifiedName()); + if (bcelType != null) { + bcelType.setDescription(type.getDescription()); + bcelType.setSource(type.getSource()); + } + } + } + } + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java index accb274f6..bdeccb146 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java @@ -12,11 +12,15 @@ public class FirstSentenceDescriptionStrategy implements DescriptionStrategy { public String descriptionOf(Type type) { String description = type.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + return ""; + } + int index = description.indexOf('.'); if (index == -1) { - return description; + return description.trim(); } else { - return description.substring(0, index+1); + return description.trim().substring(0, index+1); } } diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java similarity index 77% rename from structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java rename to structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java index 52e7d2843..c1ddaa9df 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java @@ -6,11 +6,11 @@ /** * A type filter that excludes by matching a regex against the fully qualified type name. */ -public class ExcludeTypesByRegexFilter implements TypeFilter { +public class ExcludeFullyQualifiedNameRegexFilter implements TypeFilter { private final String regex; - public ExcludeTypesByRegexFilter(String regex) { + public ExcludeFullyQualifiedNameRegexFilter(String regex) { if (StringUtils.isNullOrEmpty(regex)) { throw new IllegalArgumentException("A regex must be supplied"); } @@ -25,7 +25,7 @@ public boolean accept(Type type) { @Override public String toString() { - return "ExcludeTypesByRegexFilter{" + + return "ExcludeFullyQualifiedNameRegexFilter{" + "regex='" + regex + '\'' + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java similarity index 77% rename from structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java rename to structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java index 3de92d287..dcf3a9597 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java @@ -6,11 +6,11 @@ /** * A type filter that includes by matching a regex against the fully qualified type name. */ -public class IncludeTypesByRegexFilter implements TypeFilter { +public class IncludeFullyQualifiedNameRegexFilter implements TypeFilter { private final String regex; - public IncludeTypesByRegexFilter(String regex) { + public IncludeFullyQualifiedNameRegexFilter(String regex) { if (StringUtils.isNullOrEmpty(regex)) { throw new IllegalArgumentException("A regex must be supplied"); } @@ -25,7 +25,7 @@ public boolean accept(Type type) { @Override public String toString() { - return "IncludeTypesByRegexFilter{" + + return "IncludeFullyQualifiedNameRegexFilter{" + "regex='" + regex + '\'' + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java index 1c18fb9d5..a25606763 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java @@ -72,5 +72,11 @@ private Set findClassFiles(File path) { return classFiles; } + @Override + public String toString() { + return "ClassDirectoryTypeProvider{" + + "directory=" + directory + + '}'; + } } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java index 10f00de7e..908f2c34b 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java @@ -58,4 +58,11 @@ public Set getTypes() { return types; } + @Override + public String toString() { + return "ClassJarFileTypeProvider{" + + "jarFile=" + jarFile + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java index 96f934f5c..94a0a986c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -128,4 +128,11 @@ private String relativePath(File path) { return relativePath; } + @Override + public String toString() { + return "SourceDirectoryTypeProvider{" + + "directory=" + directory + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java index e71590dfe..b7937f09b 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -2,8 +2,8 @@ import com.structurizr.component.description.FirstSentenceDescriptionStrategy; import com.structurizr.component.description.TruncatedDescriptionStrategy; -import com.structurizr.component.filter.ExcludeTypesByRegexFilter; -import com.structurizr.component.filter.IncludeTypesByRegexFilter; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.NameSuffixTypeMatcher; import com.structurizr.component.naming.FullyQualifiedNamingStrategy; import com.structurizr.component.naming.TypeNamingStrategy; @@ -48,7 +48,7 @@ void filteredBy_ThrowsAnException_WhenPassedNull() { @Test void filteredBy_ThrowsAnException_WhenCalledTwice() { try { - new ComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(".*")).filteredBy(new ExcludeTypesByRegexFilter(".*")); + new ComponentFinderStrategyBuilder().filteredBy(new IncludeFullyQualifiedNameRegexFilter(".*")).filteredBy(new ExcludeFullyQualifiedNameRegexFilter(".*")); fail(); } catch (Exception e) { assertEquals("A type filter has already been configured", e.getMessage()); @@ -160,12 +160,12 @@ void build() { ComponentFinderStrategy strategy = new ComponentFinderStrategyBuilder() .withTechnology("Spring MVC Controller") .matchedBy(new NameSuffixTypeMatcher("Controller")) - .filteredBy(new IncludeTypesByRegexFilter("com.example.web.\\.*")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com.example.web.\\.*")) .withName(new TypeNamingStrategy()) .withDescription(new FirstSentenceDescriptionStrategy()) .build(); - assertEquals("ComponentFinderStrategy{technology='Spring MVC Controller', typeMatcher=NameSuffixTypeMatcher{suffix='Controller'}, typeFilter=IncludeTypesByRegexFilter{regex='com.example.web.\\.*'}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=FirstSentenceDescriptionStrategy{}, urlStrategy=DefaultUrlStrategy{}, componentVisitor=DefaultComponentVisitor{}}", strategy.toString()); + assertEquals("ComponentFinderStrategy{technology='Spring MVC Controller', typeMatcher=NameSuffixTypeMatcher{suffix='Controller'}, typeFilter=IncludeFullyQualifiedNameRegexFilter{regex='com.example.web.\\.*'}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=FirstSentenceDescriptionStrategy{}, urlStrategy=DefaultUrlStrategy{}, componentVisitor=DefaultComponentVisitor{}}", strategy.toString()); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java new file mode 100644 index 000000000..5e7ad05ce --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java @@ -0,0 +1,64 @@ +package com.structurizr.component; + +import com.structurizr.Workspace; +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.matcher.ImplementsTypeMatcher; +import com.structurizr.component.naming.TypeNamingStrategy; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static com.github.javaparser.utils.Utils.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ComponentFinderTests { + + @Test + void run() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + Container container = softwareSystem.addContainer("Name"); + + ComponentFinder componentFinder = new ComponentFinderBuilder() + .forContainer(container) + .fromClasses(new File("build/classes/java/test")) + .fromSource(new File("src/test/java")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) + .withStrategy(new ComponentFinderStrategyBuilder() + .withTechnology("Web Controller") + .matchedBy(new ImplementsTypeMatcher("com.structurizr.component.example.Controller")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) + .withName(new TypeNamingStrategy()) + .withDescription(new FirstSentenceDescriptionStrategy()) + .build() + ) + .withStrategy(new ComponentFinderStrategyBuilder() + .withTechnology("Data Repository") + .matchedBy(new ImplementsTypeMatcher("com.structurizr.component.example.Repository")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) + .withName(new TypeNamingStrategy()) + .withDescription(new FirstSentenceDescriptionStrategy()) + .build() + ) + .build(); + + componentFinder.run(); + + assertEquals(2, container.getComponents().size()); + Component exampleController = container.getComponentWithName("ExampleController"); + assertNotNull(exampleController); + assertTrue(exampleController.hasTag("Controller")); + assertEquals("https://example.com", exampleController.getProperties().get("Documentation")); + + Component exampleRepository = container.getComponentWithName("ExampleRepository"); + assertNotNull(exampleRepository); + + assertTrue(exampleController.hasEfferentRelationshipWith(exampleRepository)); + } + +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java index e8d70c0f9..a4158f36f 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java @@ -2,7 +2,8 @@ import com.structurizr.Workspace; import com.structurizr.component.description.FirstSentenceDescriptionStrategy; -import com.structurizr.component.filter.ExcludeTypesByRegexFilter; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.AnnotationTypeMatcher; import com.structurizr.component.matcher.ImplementsTypeMatcher; import com.structurizr.component.url.PrefixSourceUrlStrategy; @@ -36,10 +37,11 @@ void springPetClinic() { .forContainer(webApplication) .fromClasses(new File(springPetClinicHome, "target/spring-petclinic-3.3.0-SNAPSHOT.jar")) .fromSource(new File(springPetClinicHome, "src/main/java")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("org\\.springframework\\.samples\\.petclinic\\..*")) .withStrategy( new ComponentFinderStrategyBuilder() .matchedBy(new AnnotationTypeMatcher("org.springframework.stereotype.Controller")) - .filteredBy(new ExcludeTypesByRegexFilter(".*.CrashController")) + .filteredBy(new ExcludeFullyQualifiedNameRegexFilter(".*.CrashController")) .withTechnology("Spring MVC Controller") .withUrl(new PrefixSourceUrlStrategy("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java")) .forEach((component -> { @@ -62,7 +64,7 @@ void springPetClinic() { ) .build(); - componentFinder.findComponents(); + componentFinder.run(); assertEquals(7, webApplication.getComponents().size()); Component welcomeController = webApplication.getComponentWithName("Welcome Controller"); diff --git a/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java b/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java index d73f761ba..1df5874d5 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java @@ -1,8 +1,15 @@ package com.structurizr.component; +import com.structurizr.component.matcher.ImplementsTypeMatcher; +import org.apache.bcel.classfile.ClassParser; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.*; public class TypeTests { @@ -20,4 +27,52 @@ void packageName() { assertEquals("com.example", type.getPackageName()); } + @Test + void getTags_WhenTypeHasOneTag() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithTag.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + List tags = type.getTags(); + assertEquals(1, tags.size()); + assertTrue(tags.contains("Tag 1")); + } + + @Test + void getTags_WhenTypeHasManyTags() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithTags.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + List tags = type.getTags(); + assertEquals(3, tags.size()); + assertEquals("Tag 1", tags.get(0)); + assertEquals("Tag 2", tags.get(1)); + assertEquals("Tag 3", tags.get(2)); + } + + @Test + void getTags_WhenTypeHasOneProperty() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithProperty.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + Map properties = type.getProperties(); + assertEquals(1, properties.size()); + assertEquals("Value", properties.get("Name")); + } + + @Test + void getTags_WhenTypeHasManyProperties() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithProperties.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + Map properties = type.getProperties(); + assertEquals(3, properties.size()); + assertEquals("Value1", properties.get("Name1")); + assertEquals("Value2", properties.get("Name2")); + assertEquals("Value3", properties.get("Name3")); + } + } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java index f9c2515e1..4039cf8f8 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java @@ -4,9 +4,24 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; public class FirstSentenceDescriptionStrategyTests { + @Test + void descriptionOf_WhenTheDescriptionIsNull() { + Type type = new Type("com.example.ClassName"); + type.setDescription(null); + assertEquals("", new FirstSentenceDescriptionStrategy().descriptionOf(type)); + } + + @Test + void descriptionOf_WhenTheDescriptionIsEmpty() { + Type type = new Type("com.example.ClassName"); + type.setDescription(" "); + assertEquals("", new FirstSentenceDescriptionStrategy().descriptionOf(type)); + } + @Test void descriptionOf_WhenThereIsASentence() { Type type = new Type("com.example.ClassName"); diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Controller.java b/structurizr-component/src/test/java/com/structurizr/component/example/Controller.java new file mode 100644 index 000000000..ebaf960c0 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Controller.java @@ -0,0 +1,4 @@ +package com.structurizr.component.example; + +interface Controller { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/ExampleController.java b/structurizr-component/src/test/java/com/structurizr/component/example/ExampleController.java new file mode 100644 index 000000000..f6ff0569b --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/ExampleController.java @@ -0,0 +1,12 @@ +package com.structurizr.component.example; + +import com.structurizr.annotation.Property; +import com.structurizr.annotation.Tag; + +@Tag(name = "Controller") +@Property(name = "Documentation", value = "https://example.com") +class ExampleController implements Controller { + + private Repository exampleRepository = new ExampleRepository(); + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/ExampleRepository.java b/structurizr-component/src/test/java/com/structurizr/component/example/ExampleRepository.java new file mode 100644 index 000000000..ce3afddf3 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/ExampleRepository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.example; + +public class ExampleRepository implements Repository { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Repository.java b/structurizr-component/src/test/java/com/structurizr/component/example/Repository.java new file mode 100644 index 000000000..276ed87a7 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Repository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.example; + +interface Repository { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.java similarity index 60% rename from structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java rename to structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.java index 4a0b459fa..c7d156e8b 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.java @@ -5,28 +5,28 @@ import static org.junit.jupiter.api.Assertions.*; -public class ExcludeTypesByRegexFilterTests { +public class ExcludeFullyQualifiedNameRegexFilterTests { @Test void construction_ThrowsAnException_WhenPassedANullSuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter("")); - assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter(" ")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter(" ")); } @Test void filter_ReturnsTrue_WhenTheTypeDoesNotMatchRegex() { - assertTrue(new ExcludeTypesByRegexFilter(".*Utils").accept(new Type("com.example.CustomerComponent"))); + assertTrue(new ExcludeFullyQualifiedNameRegexFilter(".*Utils").accept(new Type("com.example.CustomerComponent"))); } @Test void filter_ReturnsFalse_WhenTheTypeMatchesRegex() { - assertFalse(new ExcludeTypesByRegexFilter(".*Utils").accept(new Type("com.example.DateUtils"))); + assertFalse(new ExcludeFullyQualifiedNameRegexFilter(".*Utils").accept(new Type("com.example.DateUtils"))); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.java similarity index 60% rename from structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java rename to structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.java index 25b223c54..dfa1ed908 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.java @@ -5,28 +5,28 @@ import static org.junit.jupiter.api.Assertions.*; -public class IncludeTypesByRegexFilterTests { +public class IncludeFullyQualifiedNameRegexFilterTests { @Test void construction_ThrowsAnException_WhenPassedANullSuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter("")); - assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter(" ")); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter(" ")); } @Test void filter_ReturnsFalse_WhenTheTypeDoesNotMatchRegex() { - assertFalse(new IncludeTypesByRegexFilter(".*Component").accept(new Type("com.example.DateUtils"))); + assertFalse(new IncludeFullyQualifiedNameRegexFilter(".*Component").accept(new Type("com.example.DateUtils"))); } @Test void filter_ReturnsTrue_WhenTheTypeMatchesRegex() { - assertTrue(new IncludeTypesByRegexFilter(".*Component").accept(new Type("com.example.CustomerComponent"))); + assertTrue(new IncludeFullyQualifiedNameRegexFilter(".*Component").accept(new Type("com.example.CustomerComponent"))); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java index 60d90a8cd..924ecabe9 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java @@ -32,7 +32,7 @@ void getTypes() { TypeProvider typeProvider = new ClassDirectoryTypeProvider(classes); Set types = typeProvider.getTypes(); - assertTrue(types.size() > 0); + assertFalse(types.isEmpty()); assertNotNull(types.stream().filter(t -> t.getFullyQualifiedName().equals("com.structurizr.component.provider.ClassDirectoryTypeProviderTests"))); } diff --git a/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperties.java b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperties.java new file mode 100644 index 000000000..f9f0157db --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperties.java @@ -0,0 +1,9 @@ +package com.structurizr.component.types; + +import com.structurizr.annotation.Property; + +@Property(name = "Name1", value = "Value1") +@Property(name = "Name2", value = "Value2") +@Property(name = "Name3", value = "Value3") +public class TypeWithProperties { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperty.java b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperty.java new file mode 100644 index 000000000..d6de25773 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperty.java @@ -0,0 +1,7 @@ +package com.structurizr.component.types; + +import com.structurizr.annotation.Property; + +@Property(name = "Name", value = "Value") +public class TypeWithProperty { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTag.java b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTag.java new file mode 100644 index 000000000..74cea4cdb --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTag.java @@ -0,0 +1,7 @@ +package com.structurizr.component.types; + +import com.structurizr.annotation.Tag; + +@Tag(name = "Tag 1") +public class TypeWithTag { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTags.java b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTags.java new file mode 100644 index 000000000..b577d6ca8 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTags.java @@ -0,0 +1,9 @@ +package com.structurizr.component.types; + +import com.structurizr.annotation.Tag; + +@Tag(name = "Tag 1") +@Tag(name = "Tag 2") +@Tag(name = "Tag 3") +public class TypeWithTags { +} diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java index 0172fd657..89f030ff8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java @@ -32,7 +32,7 @@ ComponentFinderBuilder getComponentFinderBuilder() { @Override void end() { - Set components = componentFinderBuilder.build().findComponents(); + Set components = componentFinderBuilder.build().run(); for (Component component : components) { dslParser.registerIdentifier(IdentifiersRegister.toIdentifier(component.getName()), component); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java index 61d0d49d6..f0931bc5a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java @@ -1,10 +1,18 @@ package com.structurizr.dsl; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; + final class ComponentFinderParser extends AbstractParser { private static final String CLASSES_GRAMMAR = "classes "; private static final String SOURCE_GRAMMAR = "source "; + private static final String FILTER_INCLUDE = "include"; + private static final String FILTER_EXCLUDE = "exclude"; + private static final String FILTER_FQN_REGEX = "fqn-regex"; + private static final String FILTER_GRAMMAR = "filter <" + FILTER_INCLUDE + "|" + FILTER_EXCLUDE + "> <" + FILTER_FQN_REGEX + "> [parameters]"; + void parseClasses(ComponentFinderDslContext context, Tokens tokens) { // classes @@ -25,4 +33,34 @@ void parseSource(ComponentFinderDslContext context, Tokens tokens) { context.getComponentFinderBuilder().fromSource(tokens.get(1)); } + void parseFilter(ComponentFinderDslContext context, Tokens tokens) { + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + FILTER_GRAMMAR); + } + + String includeOrExclude = tokens.get(1).toLowerCase(); + if (!"include".equalsIgnoreCase(includeOrExclude) && !"exclude".equalsIgnoreCase(includeOrExclude)) { + throw new RuntimeException("Filter mode should be \"" + FILTER_INCLUDE + "\" or \"" + FILTER_EXCLUDE + "\": " + FILTER_GRAMMAR); + } + + String type = tokens.get(2).toLowerCase(); + switch (type) { + case FILTER_FQN_REGEX: + if (tokens.size() == 4) { + String regex = tokens.get(3); + + if (FILTER_INCLUDE.equalsIgnoreCase(includeOrExclude)) { + context.getComponentFinderBuilder().filteredBy(new IncludeFullyQualifiedNameRegexFilter(regex)); + } else { + context.getComponentFinderBuilder().filteredBy(new ExcludeFullyQualifiedNameRegexFilter(regex)); + } + } else { + throw new RuntimeException("Expected: " + FILTER_GRAMMAR); + } + break; + default: + throw new IllegalArgumentException("Unknown filter: " + type); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index e84800882..340f437e8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -2,8 +2,8 @@ import com.structurizr.component.description.FirstSentenceDescriptionStrategy; import com.structurizr.component.description.TruncatedDescriptionStrategy; -import com.structurizr.component.filter.ExcludeTypesByRegexFilter; -import com.structurizr.component.filter.IncludeTypesByRegexFilter; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.*; import com.structurizr.component.naming.DefaultPackageNamingStrategy; import com.structurizr.component.naming.TypeNamingStrategy; @@ -153,9 +153,9 @@ void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens, File String regex = tokens.get(3); if (FILTER_INCLUDE.equalsIgnoreCase(includeOrExclude)) { - context.getComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(regex)); + context.getComponentFinderStrategyBuilder().filteredBy(new IncludeFullyQualifiedNameRegexFilter(regex)); } else { - context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeTypesByRegexFilter(regex)); + context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeFullyQualifiedNameRegexFilter(regex)); } } else { throw new RuntimeException("Expected: " + FILTER_GRAMMAR); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index abbaa8a65..6b7e307d5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -433,6 +433,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (COMPONENT_FINDER_SOURCE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { new ComponentFinderParser().parseSource(getContext(ComponentFinderDslContext.class), tokens); + } else if (COMPONENT_FINDER_FILTER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { + new ComponentFinderParser().parseFilter(getContext(ComponentFinderDslContext.class), tokens); + } else if (COMPONENT_FINDER_STRATEGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { if (shouldStartContext(tokens)) { startContext(new ComponentFinderStrategyDslContext(getContext(ComponentFinderDslContext.class).getComponentFinderBuilder())); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index fa3829934..696162601 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -116,6 +116,7 @@ class StructurizrDslTokens { static final String COMPONENT_FINDER_TOKEN = "!components"; static final String COMPONENT_FINDER_CLASSES_TOKEN = "classes"; static final String COMPONENT_FINDER_SOURCE_TOKEN = "source"; + static final String COMPONENT_FINDER_FILTER_TOKEN = "filter"; static final String COMPONENT_FINDER_STRATEGY_TOKEN = "strategy"; static final String COMPONENT_FINDER_STRATEGY_TECHNOLOGY_TOKEN = "technology"; static final String COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN = "matcher"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java new file mode 100644 index 000000000..414de072a --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java @@ -0,0 +1,59 @@ +package com.structurizr.dsl; + +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.component.matcher.NameSuffixTypeMatcher; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ComponentFinderParserTests extends AbstractTests { + + private final ComponentFinderParser parser = new ComponentFinderParser(); + private final ComponentFinderDslContext context = new ComponentFinderDslContext(null, null); + + @Test + void test_parseFilter_ThrowsAnException_WhenNoModeAndTypeAreSpecified() { + try { + parser.parseFilter(context, tokens("filter")); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseFilter(context, tokens("filter", "include")); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_ThrowsAnException_WhenInvalidModeIsSpecified() { + try { + parser.parseFilter(context, tokens("filter", "mode", "fqn-regex")); + fail(); + } catch (Exception e) { + assertEquals("Filter mode should be \"include\" or \"exclude\": filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_WhenIncludeFullyQualifiedNameRegexTypeFilterIsUsed() { + parser.parseFilter(context, tokens("filter", "include", "fqn-regex", ".*")); + assertEquals("ComponentFinderBuilder{container=null, typeProviders=[], typeFilter=IncludeFullyQualifiedNameRegexFilter{regex='.*'}, componentFinderStrategies=[]}", context.getComponentFinderBuilder().toString()); + } + + @Test + void test_parseFilter_WhenExcludeFullyQualifiedNameRegexTypeFilterIsUsed() { + parser.parseFilter(context, tokens("filter", "exclude", "fqn-regex", ".*")); + assertEquals("ComponentFinderBuilder{container=null, typeProviders=[], typeFilter=ExcludeFullyQualifiedNameRegexFilter{regex='.*'}, componentFinderStrategies=[]}", context.getComponentFinderBuilder().toString()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index 17d9cf0a7..6ea5eb258 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -186,13 +186,13 @@ void test_parseFilter_ThrowsAnException_WhenInvalidModeIsSpecified() { @Test void test_parseFilter_WhenIncludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "include", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeFullyQualifiedNameRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseFilter_WhenExcludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "exclude", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeFullyQualifiedNameRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index 3638bcc0f..9144e5fce 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -22,6 +22,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt !components { classes "${SPRING_PETCLINIC_HOME}/target/spring-petclinic-3.3.0-SNAPSHOT.jar" source "${SPRING_PETCLINIC_HOME}/src/main/java" + filter include fqn-regex "org.springframework.samples.petclinic..*" strategy { technology "Spring MVC Controller" matcher annotation "org.springframework.stereotype.Controller" From bb68483118e7704d6ac15c28cb3dfcbc162b06f3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 15:09:06 +0100 Subject: [PATCH 275/418] Allows !extend to be used hierarchically. --- .../structurizr/dsl/StructurizrDslParser.java | 2 +- .../java/com/structurizr/dsl/DslTests.java | 14 +++++++++ .../resources/dsl/extend-hierarchical.dsl | 31 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/extend-hierarchical.dsl diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 6b7e307d5..d5fa307c7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -326,7 +326,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new RelationshipsDslContext(getContext(), relationships)); } - } else if ((REF_TOKEN.equalsIgnoreCase(firstToken) || EXTEND_TOKEN.equalsIgnoreCase(firstToken)) && (inContext(ModelDslContext.class))) { + } else if ((REF_TOKEN.equalsIgnoreCase(firstToken) || EXTEND_TOKEN.equalsIgnoreCase(firstToken)) && (inContext(ModelItemDslContext.class) || inContext(ModelDslContext.class))) { ModelItem modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index e5f22175b..0c72b1d7c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1285,4 +1285,18 @@ void test_ImageView_WhenParserIsInRestrictedMode() { } } + @Test + void test_extendHierachical() throws Exception { + File dslFile = new File("src/test/resources/dsl/extend-hierarchical.dsl"); + + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + + Component component = parser.getWorkspace().getModel().getSoftwareSystemWithName("A").getContainerWithName("B").getComponentWithName("C"); + assertEquals("Value1", component.getProperties().get("Name1")); + assertEquals("Value2", component.getProperties().get("Name2")); + assertEquals("Value3", component.getProperties().get("Name3")); + } + + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/extend-hierarchical.dsl b/structurizr-dsl/src/test/resources/dsl/extend-hierarchical.dsl new file mode 100644 index 000000000..c6ed64556 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/extend-hierarchical.dsl @@ -0,0 +1,31 @@ +workspace { + + !identifiers hierarchical + + model { + a = softwareSystem "A" { + b = container "B" { + c = component "C" + + !extend c { + properties { + "Name1" "Value1" + } + } + } + + !extend b.c { + properties { + "Name2" "Value2" + } + } + } + + !extend a.b.c { + properties { + "Name3" "Value3" + } + } + } + +} \ No newline at end of file From f26f63d966a7af19843ff9913d52dd17a9b45974 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 15:47:03 +0100 Subject: [PATCH 276/418] Deprecates `!ref` and `!extend` in favour of `!element` and `!relationship`. --- changelog.md | 3 + .../structurizr/dsl/FindElementParser.java | 48 ++++++++++++++ ...ntsParser.java => FindElementsParser.java} | 2 +- .../dsl/FindRelationshipParser.java | 33 ++++++++++ ...rser.java => FindRelationshipsParser.java} | 4 +- .../structurizr/dsl/StructurizrDslParser.java | 24 +++++-- .../structurizr/dsl/StructurizrDslTokens.java | 12 ++-- .../java/com/structurizr/dsl/DslTests.java | 31 +++++---- .../dsl/FindElementParserTests.java | 64 +++++++++++++++++++ ...ests.java => FindElementsParserTests.java} | 5 +- .../dsl/FindRelationshipParserTests.java | 58 +++++++++++++++++ ...java => FindRelationshipsParserTests.java} | 4 +- .../dsl/deployment-environment-empty.dsl | 2 +- .../extend/extend-workspace-from-dsl-file.dsl | 2 +- .../extend/extend-workspace-from-dsl-url.dsl | 2 +- .../extend-workspace-from-json-file.dsl | 8 +-- .../extend/extend-workspace-from-json-url.dsl | 8 +-- ...ical.dsl => find-element-hierarchical.dsl} | 6 +- .../dsl/{ref.dsl => find-element.dsl} | 8 +-- 19 files changed, 271 insertions(+), 53 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java rename structurizr-dsl/src/main/java/com/structurizr/dsl/{ElementsParser.java => FindElementsParser.java} (94%) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java rename structurizr-dsl/src/main/java/com/structurizr/dsl/{RelationshipsParser.java => FindRelationshipsParser.java} (91%) create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementParserTests.java rename structurizr-dsl/src/test/java/com/structurizr/dsl/{ElementsParserTests.java => FindElementsParserTests.java} (92%) create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java rename structurizr-dsl/src/test/java/com/structurizr/dsl/{RelationshipsParserTests.java => FindRelationshipsParserTests.java} (92%) rename structurizr-dsl/src/test/resources/dsl/{extend-hierarchical.dsl => find-element-hierarchical.dsl} (85%) rename structurizr-dsl/src/test/resources/dsl/{ref.dsl => find-element.dsl} (83%) diff --git a/changelog.md b/changelog.md index 0af4373a5..19fd510c0 100644 --- a/changelog.md +++ b/changelog.md @@ -10,7 +10,10 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/312 (!include doesn't work with files encoded as UTF-8 BOM). - structurizr-dsl: Adds a way to explicitly specify the order of relationships in dynamic views. - structurizr-dsl: Adds support for element technology expressions (e.g. `element.technology==Java` and `element.technology!=Java`). +- structurizr-dsl: Deprecates `!ref` and `!extend`. +- structurizr-dsl: Adds an `!element` keyword that can be used to find a single element by identifier or canonical name (replaces `!ref` and `!extend`). - structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression. +- structurizr-dsl: Adds an `!relationship` keyword that can be used to find a single relationship by identifier (replaces `!ref` and `!extend`). - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. - structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. - structurizr-dsl: Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java new file mode 100644 index 000000000..74532148e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java @@ -0,0 +1,48 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.StaticStructureElement; + +final class FindElementParser extends AbstractParser { + + private static final String GRAMMAR = "!element "; + + private final static int IDENTIFIER_INDEX = 1; + + Element parse(DslContext context, Tokens tokens) { + // !element + + if (tokens.hasMoreThan(IDENTIFIER_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String s = tokens.get(IDENTIFIER_INDEX); + + Element element; + + if (s.contains("://")) { + element = context.getWorkspace().getModel().getElementWithCanonicalName(s); + } else { + element = context.getElement(s); + } + + if (element == null) { + throw new RuntimeException("An element identified by \"" + s + "\" could not be found"); + } + + if (context instanceof GroupableDslContext && element instanceof StaticStructureElement) { + GroupableDslContext groupableDslContext = (GroupableDslContext)context; + StaticStructureElement staticStructureElement = (StaticStructureElement)element; + if (groupableDslContext.hasGroup()) { + staticStructureElement.setGroup(groupableDslContext.getGroup().getName()); + } + } + + return element; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementsParser.java similarity index 94% rename from structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java rename to structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementsParser.java index f205241ce..47dc0f748 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementsParser.java @@ -6,7 +6,7 @@ import java.util.Set; import java.util.stream.Collectors; -final class ElementsParser extends AbstractParser { +final class FindElementsParser extends AbstractParser { private static final String GRAMMAR = "!elements "; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java new file mode 100644 index 000000000..3ee77016b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java @@ -0,0 +1,33 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Relationship; + +final class FindRelationshipParser extends AbstractParser { + + private static final String GRAMMAR = "!relationship "; + + private final static int IDENTIFIER_INDEX = 1; + + Relationship parse(DslContext context, Tokens tokens) { + // !relationship + + if (tokens.hasMoreThan(IDENTIFIER_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String s = tokens.get(IDENTIFIER_INDEX); + + Relationship relationship = context.getRelationship(s); + + if (relationship == null) { + throw new RuntimeException("A relationship identified by \"" + s + "\" could not be found"); + } + + return relationship; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipsParser.java similarity index 91% rename from structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java rename to structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipsParser.java index 890a295d6..2bc4c7d31 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipsParser.java @@ -1,13 +1,12 @@ package com.structurizr.dsl; -import com.structurizr.model.Element; import com.structurizr.model.ModelItem; import com.structurizr.model.Relationship; import java.util.Set; import java.util.stream.Collectors; -final class RelationshipsParser extends AbstractParser { +final class FindRelationshipsParser extends AbstractParser { private static final String GRAMMAR = "!relationships "; @@ -25,7 +24,6 @@ Set parse(DslContext context, Tokens tokens) { Set relationships = modelItems.stream().filter(mi -> mi instanceof Relationship).map(mi -> (Relationship)mi).collect(Collectors.toSet()); - if (relationships.isEmpty()) { throw new RuntimeException("No relationships found for expression \"" + expression + "\""); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index d5fa307c7..80a32c223 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -326,8 +326,20 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new RelationshipsDslContext(getContext(), relationships)); } - } else if ((REF_TOKEN.equalsIgnoreCase(firstToken) || EXTEND_TOKEN.equalsIgnoreCase(firstToken)) && (inContext(ModelItemDslContext.class) || inContext(ModelDslContext.class))) { - ModelItem modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); + } else if ((FIND_ELEMENT_TOKEN.equalsIgnoreCase(firstToken) || FIND_RELATIONSHIP_TOKEN.equalsIgnoreCase(firstToken) || REF_TOKEN.equalsIgnoreCase(firstToken) || EXTEND_TOKEN.equalsIgnoreCase(firstToken)) && (inContext(ModelItemDslContext.class) || inContext(ModelDslContext.class))) { + ModelItem modelItem = null; + + if (REF_TOKEN.equalsIgnoreCase(firstToken)) { + log.warn(REF_TOKEN + " has been deprecated and will be removed in a future release - please use !element or !relationship instead"); + modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); + } else if (EXTEND_TOKEN.equalsIgnoreCase(firstToken)) { + log.warn(EXTEND_TOKEN + " has been deprecated and will be removed in a future release - please use !element or !relationship instead"); + modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); + } else if (FIND_ELEMENT_TOKEN.equalsIgnoreCase(firstToken)) { + modelItem = new FindElementParser().parse(getContext(), tokens.withoutContextStartToken()); + } else if (FIND_RELATIONSHIP_TOKEN.equalsIgnoreCase(firstToken)) { + modelItem = new FindRelationshipParser().parse(getContext(), tokens.withoutContextStartToken()); + } if (shouldStartContext(tokens)) { if (modelItem instanceof Person) { @@ -361,15 +373,15 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } } - } else if (ELEMENTS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { - Set elements = new ElementsParser().parse(getContext(), tokens.withoutContextStartToken()); + } else if (FIND_ELEMENTS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { + Set elements = new FindElementsParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { startContext(new ElementsDslContext(getContext(), elements)); } - } else if (RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { - Set relationships = new RelationshipsParser().parse(getContext(), tokens.withoutContextStartToken()); + } else if (FIND_RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { + Set relationships = new FindRelationshipsParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { startContext(new RelationshipsDslContext(getContext(), relationships)); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 696162601..e17ad379a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -105,11 +105,15 @@ class StructurizrDslTokens { static final String VAR_TOKEN = "!var"; static final String IDENTIFIERS_TOKEN = "!identifiers"; static final String IMPLIED_RELATIONSHIPS_TOKEN = "!impliedRelationships"; - static final String REF_TOKEN = "!ref"; - static final String ELEMENTS_TOKEN = "!elements"; - static final String RELATIONSHIPS_TOKEN = "!relationships"; - static final String EXTEND_TOKEN = "!extend"; + static final String REF_TOKEN = "!ref"; // deprecated + static final String EXTEND_TOKEN = "!extend"; // deprecated + + static final String FIND_ELEMENT_TOKEN = "!element"; + static final String FIND_ELEMENTS_TOKEN = "!elements"; + static final String FIND_RELATIONSHIP_TOKEN = "!relationship"; + static final String FIND_RELATIONSHIPS_TOKEN = "!relationships"; + static final String PLUGIN_TOKEN = "!plugin"; static final String SCRIPT_TOKEN = "!script"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 0c72b1d7c..682753f62 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -516,15 +516,28 @@ void test_extendWorkspaceFromDslFiles() throws Exception { } @Test - void test_ref() throws Exception { + void test_findElement() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); - parser.parse(new File("src/test/resources/dsl/ref.dsl")); + parser.parse(new File("src/test/resources/dsl/find-element.dsl")); assertNotNull(parser.getWorkspace().getModel().getElementWithCanonicalName("InfrastructureNode://Live/Amazon Web Services/New deployment node/New infrastructure node")); assertNotNull(parser.getWorkspace().getModel().getElementWithCanonicalName("InfrastructureNode://Live/Amazon Web Services/US-East-1/New deployment node 1/New infrastructure node 1")); assertNotNull(parser.getWorkspace().getModel().getElementWithCanonicalName("InfrastructureNode://Live/Amazon Web Services/US-East-1/New deployment node 2/New infrastructure node 2")); } + @Test + void test_findElement_Hierachical() throws Exception { + File dslFile = new File("src/test/resources/dsl/find-element-hierarchical.dsl"); + + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + + Component component = parser.getWorkspace().getModel().getSoftwareSystemWithName("A").getContainerWithName("B").getComponentWithName("C"); + assertEquals("Value1", component.getProperties().get("Name1")); + assertEquals("Value2", component.getProperties().get("Name2")); + assertEquals("Value3", component.getProperties().get("Name3")); + } + @Test void test_parallel1() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); @@ -1285,18 +1298,4 @@ void test_ImageView_WhenParserIsInRestrictedMode() { } } - @Test - void test_extendHierachical() throws Exception { - File dslFile = new File("src/test/resources/dsl/extend-hierarchical.dsl"); - - StructurizrDslParser parser = new StructurizrDslParser(); - parser.parse(dslFile); - - Component component = parser.getWorkspace().getModel().getSoftwareSystemWithName("A").getContainerWithName("B").getComponentWithName("C"); - assertEquals("Value1", component.getProperties().get("Name1")); - assertEquals("Value2", component.getProperties().get("Name2")); - assertEquals("Value3", component.getProperties().get("Name3")); - } - - } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementParserTests.java new file mode 100644 index 000000000..f9ff1d3ba --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementParserTests.java @@ -0,0 +1,64 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Person; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class FindElementParserTests extends AbstractTests { + + private final FindElementParser parser = new FindElementParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!element", "name", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !element ", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierOrCanonicalNameIsNotSpecified() { + try { + parser.parse(context(), tokens("!element")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !element ", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheReferencedElementCannotBeFound() { + try { + parser.parse(context(), tokens("!element", "Person://User")); + fail(); + } catch (Exception e) { + assertEquals("An element identified by \"Person://User\" could not be found", e.getMessage()); + } + } + + @Test + void test_parse_FindsAnElementByCanonicalName() { + Person user = workspace.getModel().addPerson("User"); + ModelItem element = parser.parse(context(), tokens("!element", "Person://User")); + + assertSame(user, element); + } + + @Test + void test_parse_FindsAnElementByIdentifier() { + Person user = workspace.getModel().addPerson("User"); + + ModelDslContext context = context(); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("user", user); + context.setIdentifierRegister(register); + + ModelItem modelItem = parser.parse(context, tokens("!element", "user")); + assertSame(modelItem, user); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementsParserTests.java similarity index 92% rename from structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java rename to structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementsParserTests.java index 9eb68b7f8..49bf3ddf2 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementsParserTests.java @@ -3,16 +3,15 @@ import com.structurizr.model.Component; import com.structurizr.model.Container; import com.structurizr.model.Element; -import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; -class ElementsParserTests extends AbstractTests { +class FindElementsParserTests extends AbstractTests { - private final ElementsParser parser = new ElementsParser(); + private final FindElementsParser parser = new FindElementsParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java new file mode 100644 index 000000000..e66cb93d3 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java @@ -0,0 +1,58 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Person; +import com.structurizr.model.Relationship; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class FindRelationshipParserTests extends AbstractTests { + + private final FindRelationshipParser parser = new FindRelationshipParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!relationship", "name", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !relationship ", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierIsNotSpecified() { + try { + parser.parse(context(), tokens("!relationship")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !relationship ", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheReferencedRelationshipCannotBeFound() { + try { + parser.parse(context(), tokens("!relationship", "rel")); + fail(); + } catch (Exception e) { + assertEquals("A relationship identified by \"rel\" could not be found", e.getMessage()); + } + } + + @Test + void test_parse_FindsARelationshipByIdentifier() { + Person user = workspace.getModel().addPerson("User"); + Relationship relationship = user.interactsWith(user, "Description"); + + ModelDslContext context = context(); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("rel", relationship); + context.setIdentifierRegister(register); + + ModelItem modelItem = parser.parse(context, tokens("!relationship", "rel")); + assertSame(modelItem, relationship); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipsParserTests.java similarity index 92% rename from structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java rename to structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipsParserTests.java index b1f42db8a..916f23407 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipsParserTests.java @@ -8,9 +8,9 @@ import static org.junit.jupiter.api.Assertions.*; -class RelationshipsParserTests extends AbstractTests { +class FindRelationshipsParserTests extends AbstractTests { - private final RelationshipsParser parser = new RelationshipsParser(); + private final FindRelationshipsParser parser = new FindRelationshipsParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl index 484323ede..11566eb50 100644 --- a/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl +++ b/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl @@ -2,7 +2,7 @@ workspace { model { de = deploymentEnvironment "DeploymentEnvironment" - !ref de { + !element de { dn = deploymentNode "DeploymentNode" } } diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl index 4260ec1e2..5465ce720 100644 --- a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl @@ -1,7 +1,7 @@ workspace extends workspace.dsl { model { - !ref softwareSystem1 { + !element softwareSystem1 { webapp = container "Web Application" } diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl index d5a24910a..086f56c12 100644 --- a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl @@ -1,7 +1,7 @@ workspace extends https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/extend/workspace.dsl { model { - !ref softwareSystem1 { + !element softwareSystem1 { webapp = container "Web Application" } diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl index 06e3ffc8b..e43dd69a7 100644 --- a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl @@ -1,13 +1,13 @@ workspace extends workspace.json { model { - // !extend with DSL identifier - !extend softwareSystem1 { + // !element with DSL identifier + !element softwareSystem1 { webapp1 = container "Web Application 1" } - // !extend with canonical name - !extend "SoftwareSystem://Software System 1" { + // !element with canonical name + !element "SoftwareSystem://Software System 1" { webapp2 = container "Web Application 2" } diff --git a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl index e84912308..d59fa686c 100644 --- a/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl +++ b/structurizr-dsl/src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl @@ -1,13 +1,13 @@ workspace extends https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/extend/workspace.json { model { - // !extend with DSL identifier - !extend softwareSystem1 { + // !element with DSL identifier + !element softwareSystem1 { webapp1 = container "Web Application 1" } - // !extend with canonical name - !extend "SoftwareSystem://Software System 1" { + // !element with canonical name + !element "SoftwareSystem://Software System 1" { webapp2 = container "Web Application 2" } diff --git a/structurizr-dsl/src/test/resources/dsl/extend-hierarchical.dsl b/structurizr-dsl/src/test/resources/dsl/find-element-hierarchical.dsl similarity index 85% rename from structurizr-dsl/src/test/resources/dsl/extend-hierarchical.dsl rename to structurizr-dsl/src/test/resources/dsl/find-element-hierarchical.dsl index c6ed64556..ecc74c77d 100644 --- a/structurizr-dsl/src/test/resources/dsl/extend-hierarchical.dsl +++ b/structurizr-dsl/src/test/resources/dsl/find-element-hierarchical.dsl @@ -7,21 +7,21 @@ workspace { b = container "B" { c = component "C" - !extend c { + !element c { properties { "Name1" "Value1" } } } - !extend b.c { + !element b.c { properties { "Name2" "Value2" } } } - !extend a.b.c { + !element a.b.c { properties { "Name3" "Value3" } diff --git a/structurizr-dsl/src/test/resources/dsl/ref.dsl b/structurizr-dsl/src/test/resources/dsl/find-element.dsl similarity index 83% rename from structurizr-dsl/src/test/resources/dsl/ref.dsl rename to structurizr-dsl/src/test/resources/dsl/find-element.dsl index cf6ce5a1f..a4b0e66cd 100644 --- a/structurizr-dsl/src/test/resources/dsl/ref.dsl +++ b/structurizr-dsl/src/test/resources/dsl/find-element.dsl @@ -2,7 +2,7 @@ workspace extends amazon-web-services.dsl { model { - !ref "DeploymentNode://Live/Amazon Web Services" { + !element "DeploymentNode://Live/Amazon Web Services" { deploymentNode "New deployment node" { infrastructureNode "New infrastructure node" { -> route53 @@ -10,7 +10,7 @@ workspace extends amazon-web-services.dsl { } } - !ref "DeploymentNode://Live/Amazon Web Services/US-East-1" { + !element "DeploymentNode://Live/Amazon Web Services/US-East-1" { deploymentNode "New deployment node 1" { infrastructureNode "New infrastructure node 1" { -> route53 @@ -18,7 +18,7 @@ workspace extends amazon-web-services.dsl { } } - !ref region { + !element region { deploymentNode "New deployment node 2" { infrastructureNode "New infrastructure node 2" { -> route53 @@ -26,7 +26,7 @@ workspace extends amazon-web-services.dsl { } } - !ref live { + !element live { deploymentNode "New deployment node 3" { infrastructureNode "New infrastructure node 3" { -> route53 From e8dfc59d3bc8f8f668ddd9df51878902e1ad5359 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 17:22:44 +0100 Subject: [PATCH 277/418] Adds a @Component annotation. --- structurizr-annotation/README.md | 6 ++++++ .../java/com/structurizr/annotation/Component.java | 12 ++++++++++++ .../matcher/AnnotationTypeMatcherTests.java | 3 ++- .../matcher/annotationTypeMatcher/Controller.java | 4 ---- .../annotationTypeMatcher/CustomerController.java | 4 +++- 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 structurizr-annotation/src/main/java/com/structurizr/annotation/Component.java delete mode 100644 structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java diff --git a/structurizr-annotation/README.md b/structurizr-annotation/README.md index a8626a374..d57047c20 100644 --- a/structurizr-annotation/README.md +++ b/structurizr-annotation/README.md @@ -2,3 +2,9 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-annotation.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-annotation) +This library defines some custom annotations that you can add to your code. +These serve to either make it explicit how components should be extracted from your codebase (e.g. `@Component`), +or they help supplement the software architecture model (e.g. `@Property`, `@Tag`). + +- This library has no dependencies. +- All annotations have a runtime retention policy, so they will be present in the compiled bytecode. \ No newline at end of file diff --git a/structurizr-annotation/src/main/java/com/structurizr/annotation/Component.java b/structurizr-annotation/src/main/java/com/structurizr/annotation/Component.java new file mode 100644 index 000000000..12fc05230 --- /dev/null +++ b/structurizr-annotation/src/main/java/com/structurizr/annotation/Component.java @@ -0,0 +1,12 @@ +package com.structurizr.annotation; + +import java.lang.annotation.*; + +/** + * A type-level annotation that can be used to indicate the type represents a component. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Component { +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java index 6e9e0ce11..415b00420 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java @@ -1,5 +1,6 @@ package com.structurizr.component.matcher; +import com.structurizr.annotation.Component; import com.structurizr.component.Type; import org.apache.bcel.classfile.ClassParser; import org.junit.jupiter.api.Test; @@ -54,7 +55,7 @@ void matches_ReturnsTrue_WhenThereIsAMatch() throws Exception { ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.class").getAbsolutePath()); Type type = new Type(parser.parse()); - assertTrue(new AnnotationTypeMatcher("com.structurizr.component.matcher.annotationTypeMatcher.Controller").matches(type)); + assertTrue(new AnnotationTypeMatcher(Component.class.getName()).matches(type)); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java deleted file mode 100644 index 967ee7eee..000000000 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/Controller.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.component.matcher.annotationTypeMatcher; - -public @interface Controller { -} diff --git a/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java index a9a6f3d1c..2cd2b65e4 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java @@ -1,5 +1,7 @@ package com.structurizr.component.matcher.annotationTypeMatcher; -@Controller +import com.structurizr.annotation.Component; + +@Component public class CustomerController { } From 6c39ea608a6e9f462944590fccc48981b905d45f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 19:02:14 +0100 Subject: [PATCH 278/418] Adds DB URL. --- .../src/test/resources/dsl/spring-petclinic/workspace.dsl | 1 + 1 file changed, 1 insertion(+) diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index 9144e5fce..b40a2d4bb 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -12,6 +12,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt relationalDatabaseSchema = container "Relational Database Schema" { description "Stores information regarding the veterinarians, the clients, and their pets." technology "Relational Database Schema" + url "https://github.com/spring-projects/spring-petclinic/tree/main/src/main/resources/db" tag "Relational Database Schema" } From ce11bf3966568fa7c5c96dc8cce148b60ba4f361 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 19:03:54 +0100 Subject: [PATCH 279/418] Fixes #337. --- changelog.md | 1 + .../mermaid/MermaidDiagramExporter.java | 10 +- .../export/mermaid/36141-Components.mmd | 2 +- .../export/mermaid/36141-Containers.mmd | 2 +- .../mermaid/36141-DevelopmentDeployment.mmd | 16 +- .../export/mermaid/36141-LiveDeployment.mmd | 26 +-- .../export/mermaid/36141-SignIn.mmd | 2 +- .../export/mermaid/36141-SystemContext.mmd | 2 +- .../export/mermaid/36141-SystemLandscape.mmd | 2 +- .../54915-AmazonWebServicesDeployment.mmd | 12 +- .../mermaid/MermaidDiagramExporterTests.java | 208 +++++++++--------- .../export/mermaid/groups-Components.mmd | 4 +- .../export/mermaid/groups-Containers.mmd | 4 +- .../export/mermaid/groups-SystemLandscape.mmd | 6 +- .../export/mermaid/nested-groups.mmd | 10 +- 15 files changed, 156 insertions(+), 151 deletions(-) diff --git a/changelog.md b/changelog.md index 19fd510c0..b3c01af79 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ - structurizr-dsl: Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). - structurizr-export: Adds support for icons to the Ilograph exporter (https://github.com/structurizr/java/issues/332). - structurizr-export: Adds support for imports to the Ilograph exporter (https://github.com/structurizr/java/issues/332). +- structurizr-export: Fixes https://github.com/structurizr/java/issues/337 (Malformed subgraph name in Mermaid render). ## 2.2.0 (2nd July 2024) diff --git a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java index 54b6b2652..cb0569258 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java @@ -80,7 +80,7 @@ protected void writeFooter(ModelView view, IndentingWriter writer) { @Override protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { - writer.writeLine("subgraph enterprise [" + enterpriseName + "]"); + writer.writeLine("subgraph enterprise [\"" + enterpriseName + "\"]"); writer.indent(); writer.writeLine("style enterprise fill:#ffffff,stroke:#444444,color:#444444"); writer.writeLine(); @@ -118,7 +118,7 @@ protected void startGroupBoundary(ModelView view, String group, IndentingWriter color = elementStyle.getColor(); } - writer.writeLine(String.format("subgraph group%s [" + groupName + "]", groupId)); + writer.writeLine(String.format("subgraph group%s [\"" + groupName + "\"]", groupId)); writer.indent(); writer.writeLine(String.format("style group%s fill:#ffffff,stroke:%s,color:%s,stroke-dasharray:5", groupId, color, color)); writer.writeLine(); @@ -136,7 +136,7 @@ protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwa ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(softwareSystem); String color = elementStyle.getStroke(); - writer.writeLine(String.format("subgraph %s [%s]", softwareSystem.getId(), softwareSystem.getName())); + writer.writeLine(String.format("subgraph %s [\"%s\"]", softwareSystem.getId(), softwareSystem.getName())); writer.indent(); writer.writeLine(String.format("style %s fill:#ffffff,stroke:%s,color:%s", softwareSystem.getId(), color, color)); writer.writeLine(); @@ -154,7 +154,7 @@ protected void startContainerBoundary(ModelView view, Container container, Inden ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(container); String color = elementStyle.getStroke(); - writer.writeLine(String.format("subgraph %s [%s]", container.getId(), container.getName())); + writer.writeLine(String.format("subgraph %s [\"%s\"]", container.getId(), container.getName())); writer.indent(); writer.writeLine(String.format("style %s fill:#ffffff,stroke:%s,color:%s", container.getId(), color, color)); writer.writeLine(); @@ -171,7 +171,7 @@ protected void endContainerBoundary(ModelView view, IndentingWriter writer) { protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(deploymentNode); - writer.writeLine(String.format("subgraph %s [%s]", deploymentNode.getId(), deploymentNode.getName())); + writer.writeLine(String.format("subgraph %s [\"%s\"]", deploymentNode.getId(), deploymentNode.getName())); writer.indent(); writer.writeLine(String.format("style %s fill:#ffffff,stroke:%s,color:%s", deploymentNode.getId(), elementStyle.getStroke(), elementStyle.getColor())); writer.writeLine(); diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd index 7f759f871..505e7fa62 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd @@ -15,7 +15,7 @@ graph TB 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff - subgraph 11 [API Application] + subgraph 11 ["API Application"] style 11 fill:#ffffff,stroke:#2e6295,color:#2e6295 12["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd index 86d53a05a..e962db529 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd @@ -11,7 +11,7 @@ graph TB 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff - subgraph 7 [Internet Banking System] + subgraph 7 ["Internet Banking System"] style 7 fill:#ffffff,stroke:#0b4884,color:#0b4884 10["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd index b933bd4fb..0347c49a8 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd @@ -4,20 +4,20 @@ graph TB subgraph diagram ["Internet Banking System - Deployment - Development"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 50 [Developer Laptop] + subgraph 50 ["Developer Laptop"] style 50 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 51 [Web Browser] + subgraph 51 ["Web Browser"] style 51 fill:#ffffff,stroke:#888888,color:#000000 52["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] style 52 fill:#438dd5,stroke:#2e6295,color:#ffffff end - subgraph 53 [Docker Container - Web Server] + subgraph 53 ["Docker Container - Web Server"] style 53 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 54 [Apache Tomcat] + subgraph 54 ["Apache Tomcat"] style 54 fill:#ffffff,stroke:#888888,color:#000000 55["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] @@ -28,10 +28,10 @@ graph TB end - subgraph 59 [Docker Container - Database Server] + subgraph 59 ["Docker Container - Database Server"] style 59 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 60 [Database Server] + subgraph 60 ["Database Server"] style 60 fill:#ffffff,stroke:#888888,color:#000000 61[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] @@ -42,10 +42,10 @@ graph TB end - subgraph 63 [Big Bank plc] + subgraph 63 ["Big Bank plc"] style 63 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 64 [bigbank-dev001] + subgraph 64 ["bigbank-dev001"] style 64 fill:#ffffff,stroke:#888888,color:#000000 65["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd index e4a56d3be..e4cedb00e 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd @@ -4,17 +4,17 @@ graph TB subgraph diagram ["Internet Banking System - Deployment - Live"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 67 [Customer's mobile device] + subgraph 67 ["Customer's mobile device"] style 67 fill:#ffffff,stroke:#888888,color:#000000 68["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] style 68 fill:#438dd5,stroke:#2e6295,color:#ffffff end - subgraph 69 [Customer's computer] + subgraph 69 ["Customer's computer"] style 69 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 70 [Web Browser] + subgraph 70 ["Web Browser"] style 70 fill:#ffffff,stroke:#888888,color:#000000 71["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] @@ -23,13 +23,13 @@ graph TB end - subgraph 72 [Big Bank plc] + subgraph 72 ["Big Bank plc"] style 72 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 73 [bigbank-web***] + subgraph 73 ["bigbank-web***"] style 73 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 74 [Apache Tomcat] + subgraph 74 ["Apache Tomcat"] style 74 fill:#ffffff,stroke:#888888,color:#000000 75["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] @@ -38,10 +38,10 @@ graph TB end - subgraph 77 [bigbank-api***] + subgraph 77 ["bigbank-api***"] style 77 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 78 [Apache Tomcat] + subgraph 78 ["Apache Tomcat"] style 78 fill:#ffffff,stroke:#888888,color:#000000 79["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] @@ -50,10 +50,10 @@ graph TB end - subgraph 82 [bigbank-db01] + subgraph 82 ["bigbank-db01"] style 82 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 83 [Oracle - Primary] + subgraph 83 ["Oracle - Primary"] style 83 fill:#ffffff,stroke:#888888,color:#000000 84[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] @@ -62,10 +62,10 @@ graph TB end - subgraph 86 [bigbank-db02] + subgraph 86 ["bigbank-db02"] style 86 fill:#ffffff,stroke:#888888,color:#000000 - subgraph 87 [Oracle - Secondary] + subgraph 87 ["Oracle - Secondary"] style 87 fill:#ffffff,stroke:#888888,color:#000000 88[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] @@ -74,7 +74,7 @@ graph TB end - subgraph 90 [bigbank-prod001] + subgraph 90 ["bigbank-prod001"] style 90 fill:#ffffff,stroke:#888888,color:#000000 91["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd index 1715a20f8..378a93c7a 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd @@ -4,7 +4,7 @@ graph TB subgraph diagram ["API Application - Dynamic"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 11 [API Application] + subgraph 11 ["API Application"] style 11 fill:#ffffff,stroke:#2e6295,color:#2e6295 12["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd index 66a0a966a..1dd22f429 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd @@ -4,7 +4,7 @@ graph TB subgraph diagram ["Internet Banking System - System Context"] style diagram fill:#ffffff,stroke:#ffffff - subgraph group1 [Big Bank plc] + subgraph group1 ["Big Bank plc"] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd index f763af979..83023a9e9 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd @@ -4,7 +4,7 @@ graph TB subgraph diagram ["System Landscape"] style diagram fill:#ffffff,stroke:#ffffff - subgraph group1 [Big Bank plc] + subgraph group1 ["Big Bank plc"] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 2["
Customer Service Staff
[Person]
Customer service staff within
the bank.
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd index dac7a2f2e..4af05234c 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd @@ -4,16 +4,16 @@ graph LR subgraph diagram ["Spring PetClinic - Deployment - Live"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 5 [Amazon Web Services] + subgraph 5 ["Amazon Web Services"] style 5 fill:#ffffff,stroke:#232f3e,color:#232f3e - subgraph 6 [US-East-1] + subgraph 6 ["US-East-1"] style 6 fill:#ffffff,stroke:#147eba,color:#147eba - subgraph 12 [Amazon RDS] + subgraph 12 ["Amazon RDS"] style 12 fill:#ffffff,stroke:#3b48cc,color:#3b48cc - subgraph 13 [MySQL] + subgraph 13 ["MySQL"] style 13 fill:#ffffff,stroke:#3b48cc,color:#3b48cc 14[("
Database
[Container: Relational database schema]
Stores information regarding
the veterinarians, the
clients, and their pets.
")] @@ -26,10 +26,10 @@ graph LR style 7 fill:#ffffff,stroke:#693cc5,color:#693cc5 8("
Elastic Load Balancer
[Infrastructure Node]
Automatically distributes
incoming application traffic.
") style 8 fill:#ffffff,stroke:#693cc5,color:#693cc5 - subgraph 9 [Autoscaling group] + subgraph 9 ["Autoscaling group"] style 9 fill:#ffffff,stroke:#cc2264,color:#cc2264 - subgraph 10 [Amazon EC2] + subgraph 10 ["Amazon EC2"] style 10 fill:#ffffff,stroke:#d86613,color:#d86613 11("
Web Application
[Container: Java and Spring Boot]
Allows employees to view and
manage information regarding
the veterinarians, the
clients, and their pets.
") diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java index e01c6ca53..b5b1a8774 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -142,28 +142,29 @@ public void test_renderContainerDiagramWithExternalContainers() { containerView.add(container2); Diagram diagram = new MermaidDiagramExporter().export(containerView); - assertEquals("graph TB\n" + - " linkStyle default fill:#ffffff\n" + - "\n" + - " subgraph diagram [\"Software System 1 - Containers\"]\n" + - " style diagram fill:#ffffff,stroke:#ffffff\n" + - "\n" + - " subgraph 1 [Software System 1]\n" + - " style 1 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + - "\n" + - " 2[\"
Container 1
[Container]
\"]\n" + - " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " subgraph 3 [Software System 2]\n" + - " style 3 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + - "\n" + - " 4[\"
Container 2
[Container]
\"]\n" + - " style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " 2-. \"
Uses
\" .->4\n" + - " end", diagram.getDefinition()); + assertEquals(""" +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Software System 1 - Containers"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 1 ["Software System 1"] + style 1 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a + + 2["
Container 1
[Container]
"] + style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph 3 ["Software System 2"] + style 3 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a + + 4["
Container 2
[Container]
"] + style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 2-. "
Uses
" .->4 + end""", diagram.getDefinition()); } @Test @@ -183,28 +184,29 @@ public void test_renderComponentDiagramWithExternalComponents() { componentView.add(component2); Diagram diagram = new MermaidDiagramExporter().export(componentView); - assertEquals("graph TB\n" + - " linkStyle default fill:#ffffff\n" + - "\n" + - " subgraph diagram [\"Software System 1 - Container 1 - Components\"]\n" + - " style diagram fill:#ffffff,stroke:#ffffff\n" + - "\n" + - " subgraph 2 [Container 1]\n" + - " style 2 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + - "\n" + - " 3[\"
Component 1
[Component]
\"]\n" + - " style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " subgraph 5 [Container 2]\n" + - " style 5 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a\n" + - "\n" + - " 6[\"
Component 2
[Component]
\"]\n" + - " style 6 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " 3-. \"
Uses
\" .->6\n" + - " end", diagram.getDefinition()); + assertEquals(""" +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Software System 1 - Container 1 - Components"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 2 ["Container 1"] + style 2 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a + + 3["
Component 1
[Component]
"] + style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph 5 ["Container 2"] + style 5 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a + + 6["
Component 2
[Component]
"] + style 6 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + 3-. "
Uses
" .->6 + end""", diagram.getDefinition()); } @Test @@ -222,68 +224,70 @@ public void test_renderGroupStyles() { MermaidDiagramExporter exporter = new MermaidDiagramExporter(); Diagram diagram = exporter.export(view); - assertEquals("graph TB\n" + - " linkStyle default fill:#ffffff\n" + - "\n" + - " subgraph diagram [\"System Landscape\"]\n" + - " style diagram fill:#ffffff,stroke:#ffffff\n" + - "\n" + - " subgraph group1 [Group 1]\n" + - " style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5\n" + - "\n" + - " 1[\"
User 1
[Person]
\"]\n" + - " style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " subgraph group2 [Group 2]\n" + - " style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5\n" + - "\n" + - " 2[\"
User 2
[Person]
\"]\n" + - " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " subgraph group3 [Group 3]\n" + - " style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5\n" + - "\n" + - " 3[\"
User 3
[Person]
\"]\n" + - " style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - "\n" + - " end", diagram.getDefinition()); + assertEquals(""" +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 + + 1["
User 1
[Person]
"] + style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph group2 ["Group 2"] + style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5 + + 2["
User 2
[Person]
"] + style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph group3 ["Group 3"] + style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 3["
User 3
[Person]
"] + style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + + end""", diagram.getDefinition()); workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); diagram = exporter.export(view); - assertEquals("graph TB\n" + - " linkStyle default fill:#ffffff\n" + - "\n" + - " subgraph diagram [\"System Landscape\"]\n" + - " style diagram fill:#ffffff,stroke:#ffffff\n" + - "\n" + - " subgraph group1 [Group 1]\n" + - " style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5\n" + - "\n" + - " 1[\"
User 1
[Person]
\"]\n" + - " style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " subgraph group2 [Group 2]\n" + - " style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5\n" + - "\n" + - " 2[\"
User 2
[Person]
\"]\n" + - " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - " subgraph group3 [Group 3]\n" + - " style group3 fill:#ffffff,stroke:#aabbcc,color:#aabbcc,stroke-dasharray:5\n" + - "\n" + - " 3[\"
User 3
[Person]
\"]\n" + - " style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " end\n" + - "\n" + - "\n" + - " end", diagram.getDefinition()); + assertEquals(""" +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 + + 1["
User 1
[Person]
"] + style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph group2 ["Group 2"] + style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5 + + 2["
User 2
[Person]
"] + style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + subgraph group3 ["Group 3"] + style group3 fill:#ffffff,stroke:#aabbcc,color:#aabbcc,stroke-dasharray:5 + + 3["
User 3
[Person]
"] + style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 + end + + + end""", diagram.getDefinition()); } @Test diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd index 8a379ec14..62d5a7742 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd @@ -7,10 +7,10 @@ graph TB 3["
C
[Software System]
"] style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph 6 [F] + subgraph 6 ["F"] style 6 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - subgraph group1 [Group 5] + subgraph group1 ["Group 5"] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 8["
H
[Component]
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd index c11fc9560..768970480 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd @@ -7,10 +7,10 @@ graph TB 3["
C
[Software System]
"] style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph 4 [D] + subgraph 4 ["D"] style 4 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - subgraph group1 [Group 4] + subgraph group1 ["Group 4"] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 6["
F
[Container]
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd index 24cb88486..65efa9926 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd @@ -4,19 +4,19 @@ graph TB subgraph diagram ["System Landscape"] style diagram fill:#ffffff,stroke:#ffffff - subgraph group1 [Group 1] + subgraph group1 ["Group 1"] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 2["
B
[Software System]
"] style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 end - subgraph group2 [Group 2] + subgraph group2 ["Group 2"] style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 3["
C
[Software System]
"] style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph group3 [Group 3] + subgraph group3 ["Group 3"] style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 4["
D
[Software System]
"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd index e48b4e3ec..74a0cfe4d 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd @@ -4,24 +4,24 @@ graph TB subgraph diagram ["System Landscape"] style diagram fill:#ffffff,stroke:#ffffff - subgraph group1 [Organisation 1] + subgraph group1 ["Organisation 1"] style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 3["
Organisation 1
[Software System]
"] style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph group2 [Department 1] + subgraph group2 ["Department 1"] style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 5["
Department 1
[Software System]
"] style 5 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph group3 [Team 1] + subgraph group3 ["Team 1"] style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 1["
Team 1
[Software System]
"] style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 end - subgraph group4 [Team 2] + subgraph group4 ["Team 2"] style group4 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 2["
Team 2
[Software System]
"] @@ -32,7 +32,7 @@ graph TB end - subgraph group5 [Organisation 2] + subgraph group5 ["Organisation 2"] style group5 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 4["
Organisation 2
[Software System]
"] From 71de823160bd45f461d63f8a15ec7aa3cb376064 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 23:51:26 +0100 Subject: [PATCH 280/418] Adds assertions re: URLs. --- .../java/com/structurizr/component/ComponentFinderTests.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java index 5e7ad05ce..68ff4698c 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java @@ -5,6 +5,7 @@ import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.ImplementsTypeMatcher; import com.structurizr.component.naming.TypeNamingStrategy; +import com.structurizr.component.url.PrefixSourceUrlStrategy; import com.structurizr.model.Component; import com.structurizr.model.Container; import com.structurizr.model.SoftwareSystem; @@ -35,6 +36,7 @@ void run() { .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) .withName(new TypeNamingStrategy()) .withDescription(new FirstSentenceDescriptionStrategy()) + .withUrl(new PrefixSourceUrlStrategy("https://example.com/src/main/java")) .build() ) .withStrategy(new ComponentFinderStrategyBuilder() @@ -43,6 +45,7 @@ void run() { .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) .withName(new TypeNamingStrategy()) .withDescription(new FirstSentenceDescriptionStrategy()) + .withUrl(new PrefixSourceUrlStrategy("https://example.com/src/main/java")) .build() ) .build(); @@ -52,11 +55,13 @@ void run() { assertEquals(2, container.getComponents().size()); Component exampleController = container.getComponentWithName("ExampleController"); assertNotNull(exampleController); + assertEquals("https://example.com/src/main/java/com/structurizr/component/example/ExampleController.java", exampleController.getUrl()); assertTrue(exampleController.hasTag("Controller")); assertEquals("https://example.com", exampleController.getProperties().get("Documentation")); Component exampleRepository = container.getComponentWithName("ExampleRepository"); assertNotNull(exampleRepository); + assertEquals("https://example.com/src/main/java/com/structurizr/component/example/ExampleRepository.java", exampleRepository.getUrl()); assertTrue(exampleController.hasEfferentRelationshipWith(exampleRepository)); } From 3c83773348f9252938d0df2d5e5b1f40ff7b252c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 18 Sep 2024 23:58:12 +0100 Subject: [PATCH 281/418] Fixes for Windows file separators. --- .../url/PrefixSourceUrlStrategy.java | 2 +- .../url/PrefixSourceUrlStrategyTests.java | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/url/PrefixSourceUrlStrategyTests.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java index 6419161d5..1fd3047d0 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java @@ -24,7 +24,7 @@ public PrefixSourceUrlStrategy(String prefix) { @Override public String urlOf(Type type) { - return prefix + type.getSource(); + return prefix + (type.getSource().replaceAll("\\\\", "/")); } @Override diff --git a/structurizr-component/src/test/java/com/structurizr/component/url/PrefixSourceUrlStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/url/PrefixSourceUrlStrategyTests.java new file mode 100644 index 000000000..d4de1065c --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/url/PrefixSourceUrlStrategyTests.java @@ -0,0 +1,26 @@ +package com.structurizr.component.url; + +import com.structurizr.component.Type; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PrefixSourceUrlStrategyTests { + + @Test + void test_urlOf_WhenTheSourceUsesForwardSlashFileSeparators() { + Type type = new Type("com.example.ClassName"); + type.setSource("com/example/ClassName.java"); + + assertEquals("https://example.com/src/main/java/com/example/ClassName.java", new PrefixSourceUrlStrategy("https://example.com/src/main/java").urlOf(type)); + } + + @Test + void test_urlOf_WhenTheSourceUsesBackslashFileSeparators() { + Type type = new Type("com.example.ClassName"); + type.setSource("com\\example\\ClassName.java"); + + assertEquals("https://example.com/src/main/java/com/example/ClassName.java", new PrefixSourceUrlStrategy("https://example.com/src/main/java").urlOf(type)); + } + +} \ No newline at end of file From 12ddd83421fe073ba7fa7284aba4fe182e842ad0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 19 Sep 2024 09:03:08 +0100 Subject: [PATCH 282/418] It now doesn't matter to the component finder what the ordering of source or class type providers is. --- .../java/com/structurizr/component/Type.java | 6 ++- .../com/structurizr/component/TypeFinder.java | 12 +---- .../structurizr/component/TypeRepository.java | 15 +++++- .../url/PrefixSourceUrlStrategy.java | 6 ++- .../component/ComponentFinderTests.java | 2 +- .../component/TypeRepositoryTests.java | 52 +++++++++++++++++++ 6 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 structurizr-component/src/test/java/com/structurizr/component/TypeRepositoryTests.java diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index f267a0e82..3e3be19ec 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -16,7 +16,7 @@ public class Type { private static final String STRUCTURIZR_PROPERTY_ANNOTATION = "Lcom/structurizr/annotation/Property;"; private static final String STRUCTURIZR_PROPERTIES_ANNOTATION = "Lcom/structurizr/annotation/Properties;"; - private final JavaClass javaClass; + private JavaClass javaClass = null; private final String fullyQualifiedName; private String description; private String source; @@ -72,6 +72,10 @@ public JavaClass getJavaClass() { return this.javaClass; } + void setJavaClass(JavaClass javaClass) { + this.javaClass = javaClass; + } + public void addDependency(Type type) { this.dependencies.add(type); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java b/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java index d1b843464..5ee4dcf3f 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java @@ -27,17 +27,7 @@ void run(Collection typeProviders, TypeFilter typeFilter, TypeRepo } if (accepted) { - if (type.getJavaClass() != null) { - // this is the BCEL identified type - typeRepository.add(type); - } else { - // this is the source code identified type - Type bcelType = typeRepository.getType(type.getFullyQualifiedName()); - if (bcelType != null) { - bcelType.setDescription(type.getDescription()); - bcelType.setSource(type.getSource()); - } - } + typeRepository.add(type); } } } diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java index faa707b57..33683165e 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java @@ -8,7 +8,20 @@ public final class TypeRepository { private final Set types = new LinkedHashSet<>(); public void add(Type type) { - this.types.add(type); + Type t = getType(type.getFullyQualifiedName()); + if (t == null) { + // type isn't yet registered, so add it + types.add(type); + } else { + if (type.getJavaClass() != null) { + // this is the BCEL identified type + t.setJavaClass(type.getJavaClass()); + } else { + // this is the source code identified type + t.setDescription(type.getDescription()); + t.setSource(type.getSource()); + } + } } public Set getTypes() { diff --git a/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java index 1fd3047d0..5aa951cd5 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java @@ -24,7 +24,11 @@ public PrefixSourceUrlStrategy(String prefix) { @Override public String urlOf(Type type) { - return prefix + (type.getSource().replaceAll("\\\\", "/")); + if (type.getSource() != null) { + return prefix + (type.getSource().replaceAll("\\\\", "/")); + } else { + return null; + } } @Override diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java index 68ff4698c..6b1689f04 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java @@ -27,8 +27,8 @@ void run() { ComponentFinder componentFinder = new ComponentFinderBuilder() .forContainer(container) - .fromClasses(new File("build/classes/java/test")) .fromSource(new File("src/test/java")) + .fromClasses(new File("build/classes/java/test")) .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) .withStrategy(new ComponentFinderStrategyBuilder() .withTechnology("Web Controller") diff --git a/structurizr-component/src/test/java/com/structurizr/component/TypeRepositoryTests.java b/structurizr-component/src/test/java/com/structurizr/component/TypeRepositoryTests.java new file mode 100644 index 000000000..53cdedc0b --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/TypeRepositoryTests.java @@ -0,0 +1,52 @@ +package com.structurizr.component; + +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class TypeRepositoryTests { + + @Test + void add_MergesClassInformation() throws Exception { + String fqn = "com.structurizr.component.TypeRepositoryTests"; + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/TypeRepositoryTests.class").getAbsolutePath()); + JavaClass javaClass = parser.parse(); + + Type classType = new Type(javaClass); + Type sourceType = new Type(fqn); + sourceType.setSource("source path"); + + TypeRepository typeRepository = new TypeRepository(); + typeRepository.add(sourceType); // source first + typeRepository.add(classType); + + assertSame(javaClass, typeRepository.getType(fqn).getJavaClass()); + assertEquals("source path", typeRepository.getType(fqn).getSource()); + } + + @Test + void add_MergesSourceInformation() throws Exception { + String fqn = "com.structurizr.component.TypeRepositoryTests"; + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/TypeRepositoryTests.class").getAbsolutePath()); + JavaClass javaClass = parser.parse(); + + Type classType = new Type(javaClass); + Type sourceType = new Type(fqn); + sourceType.setSource("source path"); + + TypeRepository typeRepository = new TypeRepository(); + typeRepository.add(classType); // class first + typeRepository.add(sourceType); + + assertSame(javaClass, typeRepository.getType(fqn).getJavaClass()); + assertEquals("source path", typeRepository.getType(fqn).getSource()); + } + +} \ No newline at end of file From 24c5cf6912e251ec731263abcf86fd4d1492b02f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 19 Sep 2024 09:30:54 +0100 Subject: [PATCH 283/418] Closes #336. --- .../java/com/structurizr/model/Model.java | 26 +++++++++++- .../com/structurizr/model/ModelTests.java | 42 ++++++++++++++++--- .../structurizr/dsl/FindElementParser.java | 3 +- .../dsl/FindRelationshipParser.java | 12 ++++-- .../dsl/FindRelationshipParserTests.java | 24 ++++++++--- 5 files changed, 90 insertions(+), 17 deletions(-) diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 6ecb81675..111d124c1 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.structurizr.PropertyHolder; import com.structurizr.WorkspaceValidationException; +import com.structurizr.util.StringUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -937,12 +938,12 @@ private void replicateElementRelationships(StaticStructureElementInstance elemen /** * Gets the element with the specified canonical name. * - * @param canonicalName the canonical name (e.g. /SoftwareSystem/Container) + * @param canonicalName the canonical name * @return the Element with the given canonical name, or null if one doesn't exist * @throws IllegalArgumentException if the canonical name is null or empty */ public Element getElementWithCanonicalName(String canonicalName) { - if (canonicalName == null || canonicalName.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(canonicalName)) { throw new IllegalArgumentException("A canonical name must be specified."); } @@ -955,6 +956,27 @@ public Element getElementWithCanonicalName(String canonicalName) { return null; } + /** + * Gets the relationship with the specified canonical name. + * + * @param canonicalName the canonical name + * @return the Relationship with the given canonical name, or null if one doesn't exist + * @throws IllegalArgumentException if the canonical name is null or empty + */ + public Relationship getRelationshipWithCanonicalName(String canonicalName) { + if (StringUtils.isNullOrEmpty(canonicalName)) { + throw new IllegalArgumentException("A canonical name must be specified."); + } + + for (Relationship relationship : getRelationships()) { + if (relationship.getCanonicalName().equals(canonicalName)) { + return relationship; + } + } + + return null; + } + /** * Sets the ID generator associated with this model. * diff --git a/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java b/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java index 36454db16..3bda19147 100644 --- a/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java +++ b/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java @@ -583,16 +583,48 @@ void getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpe @Test void getElementWithCanonicalName_ReturnsNull_WhenAnElementWithTheSpecifiedCanonicalNameDoesNotExist() { - assertNull(model.getElementWithCanonicalName("Software System")); + assertNull(model.getElementWithCanonicalName("SoftwareSystem://A")); } @Test void getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpecifiedCanonicalNameExists() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Container container = softwareSystem.addContainer("Web Application", "Description", "Technology"); + SoftwareSystem a = model.addSoftwareSystem("A"); + Container b = a.addContainer("B"); - assertSame(softwareSystem, model.getElementWithCanonicalName("SoftwareSystem://Software System")); - assertSame(container, model.getElementWithCanonicalName("Container://Software System.Web Application")); + assertSame(a, model.getElementWithCanonicalName("SoftwareSystem://A")); + assertSame(b, model.getElementWithCanonicalName("Container://A.B")); + } + + @Test + void getRelationshipWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIsSpecified() { + try { + model.getRelationshipWithCanonicalName(null); + } catch (IllegalArgumentException iae) { + assertEquals("A canonical name must be specified.", iae.getMessage()); + } + } + + @Test + void getRelationshipWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpecified() { + try { + model.getRelationshipWithCanonicalName(" "); + } catch (IllegalArgumentException iae) { + assertEquals("A canonical name must be specified.", iae.getMessage()); + } + } + + @Test + void getRelationshipWithCanonicalName_ReturnsNull_WhenARelationshipWithTheSpecifiedCanonicalNameDoesNotExist() { + assertNull(model.getRelationshipWithCanonicalName("Relationship://SoftwareSystem://A -> SoftwareSystem://B (Uses)")); + } + + @Test + void getRelationshipWithCanonicalName_ReturnsTheRelationship_WhenARelationshipWithTheSpecifiedCanonicalNameExists() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship r = a.uses(b, "Uses"); + + assertSame(r, model.getRelationshipWithCanonicalName("Relationship://SoftwareSystem://A -> SoftwareSystem://B (Uses)")); } @Test diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java index 74532148e..a6737a585 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java @@ -20,10 +20,9 @@ Element parse(DslContext context, Tokens tokens) { throw new RuntimeException("Expected: " + GRAMMAR); } - String s = tokens.get(IDENTIFIER_INDEX); - Element element; + String s = tokens.get(IDENTIFIER_INDEX); if (s.contains("://")) { element = context.getWorkspace().getModel().getElementWithCanonicalName(s); } else { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java index 3ee77016b..9643e57b8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java @@ -1,10 +1,11 @@ package com.structurizr.dsl; +import com.structurizr.model.Element; import com.structurizr.model.Relationship; final class FindRelationshipParser extends AbstractParser { - private static final String GRAMMAR = "!relationship "; + private static final String GRAMMAR = "!relationship "; private final static int IDENTIFIER_INDEX = 1; @@ -19,9 +20,14 @@ Relationship parse(DslContext context, Tokens tokens) { throw new RuntimeException("Expected: " + GRAMMAR); } - String s = tokens.get(IDENTIFIER_INDEX); + Relationship relationship; - Relationship relationship = context.getRelationship(s); + String s = tokens.get(IDENTIFIER_INDEX); + if (s.startsWith("Relationship://")) { + relationship = context.getWorkspace().getModel().getRelationshipWithCanonicalName(s); + } else { + relationship = context.getRelationship(s); + } if (relationship == null) { throw new RuntimeException("A relationship identified by \"" + s + "\" could not be found"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java index e66cb93d3..91d9f6dd2 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java @@ -3,6 +3,7 @@ import com.structurizr.model.ModelItem; import com.structurizr.model.Person; import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -17,17 +18,17 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { parser.parse(context(), tokens("!relationship", "name", "tokens")); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: !relationship ", e.getMessage()); + assertEquals("Too many tokens, expected: !relationship ", e.getMessage()); } } @Test - void test_parse_ThrowsAnException_WhenTheIdentifierIsNotSpecified() { + void test_parse_ThrowsAnException_WhenTheIdentifierOrCanonicalNameIsNotSpecified() { try { parser.parse(context(), tokens("!relationship")); fail(); } catch (Exception e) { - assertEquals("Expected: !relationship ", e.getMessage()); + assertEquals("Expected: !relationship ", e.getMessage()); } } @@ -41,10 +42,23 @@ void test_parse_ThrowsAnException_WhenTheReferencedRelationshipCannotBeFound() { } } + @Test + void test_parse_FindsARelationshipByCanonicalName() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Description"); + + ModelDslContext context = context(); + + ModelItem modelItem = parser.parse(context, tokens("!relationship", "Relationship://SoftwareSystem://A -> SoftwareSystem://B (Description)")); + assertSame(modelItem, relationship); + } + @Test void test_parse_FindsARelationshipByIdentifier() { - Person user = workspace.getModel().addPerson("User"); - Relationship relationship = user.interactsWith(user, "Description"); + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Description"); ModelDslContext context = context(); IdentifiersRegister register = new IdentifiersRegister(); From a52e8f93870238ae10bc70a055a946088e98e414 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 19 Sep 2024 10:23:21 +0100 Subject: [PATCH 284/418] structurizr-dsl: An exception is now thrown when trying to use disallowed features in restricted mode (e.g. `!docs`, `!include `, etc). --- changelog.md | 1 + .../structurizr/dsl/StructurizrDslParser.java | 28 +++++++ .../java/com/structurizr/dsl/DslTests.java | 77 +++++++++++++++++-- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index b3c01af79..9052a7c63 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,7 @@ - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. - structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. - structurizr-dsl: Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). +- structurizr-dsl: An exception is now thrown when trying to use disallowed features in restricted mode (e.g. `!docs`, `!include `, etc). - structurizr-export: Adds support for icons to the Ilograph exporter (https://github.com/structurizr/java/issues/332). - structurizr-export: Adds support for imports to the Ilograph exporter (https://github.com/structurizr/java/issues/332). - structurizr-export: Fixes https://github.com/structurizr/java/issues/337 (Malformed subgraph name in Mermaid render). diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 80a32c223..1d1d0e759 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -255,6 +255,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn parse(paddedLines, includedFile.getFile(), true, true); } + } else { + throwRestrictedModeException(firstToken + " "); } // include the !include in the parser DSL as: # !include ... @@ -268,6 +270,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn // run the plugin immediately, without looking for parameters endContext(); } + } else { + throwRestrictedModeException(firstToken); } } else if (inContext(PluginDslContext.class)) { @@ -289,6 +293,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn endContext(); } } + } else { + throwRestrictedModeException(firstToken); } } else if (inContext(ExternalScriptDslContext.class)) { @@ -437,6 +443,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn if (shouldStartContext(tokens)) { startContext(new ComponentFinderDslContext(this, getContext(ContainerDslContext.class).getContainer())); } + } else { + throwRestrictedModeException(firstToken); } } else if (COMPONENT_FINDER_CLASSES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { @@ -933,41 +941,57 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { if (!restricted) { new DocsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { if (!restricted) { new DocsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { if (!restricted) { new DocsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) { if (!restricted) { new DocsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(WorkspaceDslContext.class)) { if (!restricted) { new DecisionsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(SoftwareSystemDslContext.class)) { if (!restricted) { new DecisionsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ContainerDslContext.class)) { if (!restricted) { new DecisionsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ComponentDslContext.class)) { if (!restricted) { new DecisionsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); + } else { + throwRestrictedModeException(firstToken); } } else if (CONSTANT_TOKEN.equalsIgnoreCase(firstToken)) { @@ -1068,6 +1092,10 @@ private List preProcessLines(List lines) { return dslLines; } + private void throwRestrictedModeException(String firstToken) { + throw new RuntimeException(firstToken + " is not available when the parser is running in restricted mode"); + } + private String substituteStrings(String token) { Matcher m = STRING_SUBSTITUTION_PATTERN.matcher(token); while (m.find()) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 682753f62..7ff0184aa 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -419,13 +419,18 @@ void test_includeUrl() throws Exception { } @Test - void test_include_WhenRunningInRestrictedMode() throws Exception { - StructurizrDslParser parser = new StructurizrDslParser(); - parser.setRestricted(true); + void test_includeLocalFile_ThrowsAnException_WhenRunningInRestrictedMode() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); - // the model include will be ignored, so no software systems - parser.parse(new File("src/test/resources/dsl/include-file.dsl")); - assertEquals(0, model.getSoftwareSystems().size()); + // the model include will be ignored, so no software systems + parser.parse(new File("src/test/resources/dsl/include-file.dsl")); + fail(); + } catch (Exception e) { + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!include is not available when the parser is running in restricted mode")); + } } @ParameterizedTest @@ -663,6 +668,19 @@ void test_hierarchicalIdentifiersAndDeploymentNodes_WhenSoftwareContainerClashes parser.parse(new File("src/test/resources/dsl/hierarchical-identifiers-and-deployment-nodes-3.dsl")); } + @Test + void test_plugin_ThrowsAnException_WhenTheParserIsRunningInRestrictedMode() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + parser.parse(new File("src/test/resources/dsl/plugin-without-parameters.dsl")); + fail(); + } catch (Exception e) { + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!plugin is not available when the parser is running in restricted mode")); + } + } + @Test void test_pluginWithoutParameters() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); @@ -679,6 +697,18 @@ void test_pluginWithParameters() throws Exception { assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Java")); } + @Test + void test_script_ThrowsAnException_WhenTheParserIsInRestrictedMode() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + parser.parse(new File("src/test/resources/dsl/script-external.dsl")); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().startsWith("!script is not available when the parser is running in restricted mode")); + } + } + @Test void test_script() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); @@ -751,6 +781,18 @@ void test_docs() throws Exception { assertEquals(1, component.getDocumentation().getSections().size()); } + @Test + void test_docs_ThrowsAnException_WhenTheParserIsInRestrictedMode() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + parser.parse(new File("src/test/resources/dsl/docs/workspace.dsl")); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().startsWith("!docs is not available when the parser is running in restricted mode")); + } + } + @Test void test_decisions() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); @@ -771,6 +813,18 @@ void test_decisions() throws Exception { assertEquals(4, component.getDocumentation().getDecisions().size()); } + @Test + void test_decisions_ThrowsAnException_WhenTheParserIsInRestrictedMode() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + parser.parse(new File("src/test/resources/dsl/decisions/workspace.dsl")); + fail(); + } catch (Exception e) { + assertTrue(e.getMessage().startsWith("!adrs is not available when the parser is running in restricted mode")); + } + } + @Test void test_this() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); @@ -1192,6 +1246,17 @@ void springPetClinic() throws Exception { if (!StringUtils.isNullOrEmpty(springPetClinicHome)) { System.out.println("Running Spring PetClinic example..."); + try { + File workspaceFile = new File("src/test/resources/dsl/spring-petclinic/workspace.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setRestricted(true); + parser.parse(workspaceFile); + fail(); + } catch (Exception e) { + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!components is not available when the parser is running in restricted mode")); + } + File workspaceFile = new File("src/test/resources/dsl/spring-petclinic/workspace.dsl"); StructurizrDslParser parser = new StructurizrDslParser(); parser.parse(workspaceFile); From be69af2a78f0d3d1166b4bcec732db7ff16539ef Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 19 Sep 2024 10:56:05 +0100 Subject: [PATCH 285/418] Update docs. --- changelog.md | 2 +- structurizr-component/README.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 9052a7c63..1192cda71 100644 --- a/changelog.md +++ b/changelog.md @@ -15,7 +15,7 @@ - structurizr-dsl: Adds an `!elements` keyword that can be used to find a set of elements via an expression. - structurizr-dsl: Adds an `!relationship` keyword that can be used to find a single relationship by identifier (replaces `!ref` and `!extend`). - structurizr-dsl: Adds a `!relationships` keyword that can be used to find a set of relationships via an expression. -- structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder. +- structurizr-dsl: Adds a DSL wrapper around the `structurizr-component` component finder (`!components`). - structurizr-dsl: Adds support for local theme files to be specified via `theme` (https://github.com/structurizr/java/issues/331). - structurizr-dsl: An exception is now thrown when trying to use disallowed features in restricted mode (e.g. `!docs`, `!include `, etc). - structurizr-export: Adds support for icons to the Ilograph exporter (https://github.com/structurizr/java/issues/332). diff --git a/structurizr-component/README.md b/structurizr-component/README.md index 0e319527b..b9f556ae5 100644 --- a/structurizr-component/README.md +++ b/structurizr-component/README.md @@ -5,5 +5,9 @@ This library provides a facility to discover components in a Java codebase, via a combination of [Apache Commons BCEL](https://commons.apache.org/proper/commons-bcel/) and [JavaParser](https://javaparser.org), using a pluggable and customisable set of matching and filtering rules. +It is also available via the Structurizr DSL `!component` keyword. -__Unreleased, experimental, and potentially subject to change - see tests for an example.__ \ No newline at end of file +See the following tests for an example: + +- https://github.com/structurizr/java/blob/master/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java +- https://github.com/structurizr/java/blob/master/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl \ No newline at end of file From 6697b0882f42d124350814c1e330bcd35f02a3e3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 19 Sep 2024 11:26:47 +0100 Subject: [PATCH 286/418] Updated to reflect release date. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 1192cda71..3a510bf1e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 3.0.0 (unreleased) +## 3.0.0 (19th September 2024) - structurizr-client: Adds support to get/put workspace branches on the [cloud service](https://docs.structurizr.com/cloud/workspace-branches) and [on-premises installation](https://docs.structurizr.com/onpremises/workspace-branches). - structurizr-core: Adds name-value properties to dynamic view relationship views (https://github.com/structurizr/java/issues/316). From 75d3d131bda538b6e666298cecb3f8e8ecc97fe0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 20 Sep 2024 14:08:24 +0100 Subject: [PATCH 287/418] structurizr-client: Workspace archive file now includes the branch name in the filename. --- build.gradle | 2 +- changelog.md | 4 ++++ .../src/main/java/com/structurizr/api/WorkspaceApiClient.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 127bc7d21..bc0b040b4 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '3.0.0' + version = '3.0.1' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index 3a510bf1e..f3410decc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 3.0.1 (unreleased) + +- structurizr-client: Workspace archive file now includes the branch name in the filename. + ## 3.0.0 (19th September 2024) - structurizr-client: Adds support to get/put workspace branches on the [cloud service](https://docs.structurizr.com/cloud/workspace-branches) and [on-premises installation](https://docs.structurizr.com/onpremises/workspace-branches). diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java index 159a8fffd..1ab4a7158 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -415,7 +415,7 @@ private void debugArchivedWorkspaceLocation(File archiveFile) { private String createArchiveFileName(long workspaceId) { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); - return "structurizr-" + workspaceId + "-" + sdf.format(new Date()) + ".json"; + return "structurizr-" + workspaceId + "-" + (StringUtils.isNullOrEmpty(branch) ? "" : (branch + "-")) + sdf.format(new Date()) + ".json"; } public void setUser(String user) { From 4fe04a6e9a8c3024694c373dbe0069333a694018 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 25 Sep 2024 16:18:31 +0100 Subject: [PATCH 288/418] Typo. --- structurizr-component/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-component/README.md b/structurizr-component/README.md index b9f556ae5..06bb920af 100644 --- a/structurizr-component/README.md +++ b/structurizr-component/README.md @@ -5,7 +5,7 @@ This library provides a facility to discover components in a Java codebase, via a combination of [Apache Commons BCEL](https://commons.apache.org/proper/commons-bcel/) and [JavaParser](https://javaparser.org), using a pluggable and customisable set of matching and filtering rules. -It is also available via the Structurizr DSL `!component` keyword. +It is also available via the Structurizr DSL `!components` keyword. See the following tests for an example: From e6a78c705c042c86f0aa466725b1014cbf4eff23 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 26 Sep 2024 23:17:43 +0100 Subject: [PATCH 289/418] Bump dependencies. --- build.gradle | 2 +- changelog.md | 2 +- structurizr-autolayout/build.gradle | 2 +- structurizr-client/build.gradle | 6 +++--- structurizr-component/build.gradle | 6 +++--- structurizr-core/build.gradle | 8 ++++---- structurizr-dsl/build.gradle | 10 +++++----- structurizr-export/build.gradle | 2 +- structurizr-import/build.gradle | 2 +- structurizr-inspection/build.gradle | 2 +- structurizr-neo4j/build.gradle | 4 ++-- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index bc0b040b4..21ae1b226 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '3.0.1' + version = '3.1.0' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index f3410decc..568f80a34 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 3.0.1 (unreleased) +## 3.1.0 (unreleased) - structurizr-client: Workspace archive file now includes the branch name in the filename. diff --git a/structurizr-autolayout/build.gradle b/structurizr-autolayout/build.gradle index c40b607ba..19510a149 100644 --- a/structurizr-autolayout/build.gradle +++ b/structurizr-autolayout/build.gradle @@ -3,7 +3,7 @@ dependencies { api project(':structurizr-export') testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' } diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index f6a2c657e..32f0fb02b 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,11 +2,11 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.16.0' - api 'org.apache.httpcomponents.client5:httpclient5:5.2.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.17.2' + api 'org.apache.httpcomponents.client5:httpclient5:5.4' api 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' } diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle index 6c49d8797..538c74020 100644 --- a/structurizr-component/build.gradle +++ b/structurizr-component/build.gradle @@ -1,11 +1,11 @@ dependencies { api project(':structurizr-core') - implementation 'org.apache.bcel:bcel:6.8.1' - implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.1' + implementation 'org.apache.bcel:bcel:6.10.0' + implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.2' testImplementation project(':structurizr-annotation') - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' } diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index dae5405a4..80ccbf6dd 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,10 +1,10 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.16.0' + api 'com.fasterxml.jackson.core:jackson-annotations:2.17.2' api 'com.google.code.findbugs:jsr305:3.0.2' - api 'commons-logging:commons-logging:1.2' + api 'commons-logging:commons-logging:1.3.4' - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' - testImplementation 'org.assertj:assertj-core:3.24.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.assertj:assertj-core:3.26.3' } \ No newline at end of file diff --git a/structurizr-dsl/build.gradle b/structurizr-dsl/build.gradle index 2b6eaabbb..1a38536b6 100644 --- a/structurizr-dsl/build.gradle +++ b/structurizr-dsl/build.gradle @@ -4,12 +4,12 @@ dependencies { api project(':structurizr-import') api project(':structurizr-component') - testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.19' - testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.8.10' - testImplementation 'org.jruby:jruby-core:9.4.4.0' + testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.22' + testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.25' + testImplementation 'org.jruby:jruby-core:9.4.8.0' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.2' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.0' } description = 'Structurizr DSL' \ No newline at end of file diff --git a/structurizr-export/build.gradle b/structurizr-export/build.gradle index 0729fcb26..67a1a4e33 100644 --- a/structurizr-export/build.gradle +++ b/structurizr-export/build.gradle @@ -3,7 +3,7 @@ dependencies { api project(':structurizr-core') testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' } diff --git a/structurizr-import/build.gradle b/structurizr-import/build.gradle index bf562441f..a1dd4218c 100644 --- a/structurizr-import/build.gradle +++ b/structurizr-import/build.gradle @@ -2,7 +2,7 @@ dependencies { api project(':structurizr-core') - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' } diff --git a/structurizr-inspection/build.gradle b/structurizr-inspection/build.gradle index 1dfe8a378..25ca05b8e 100644 --- a/structurizr-inspection/build.gradle +++ b/structurizr-inspection/build.gradle @@ -3,6 +3,6 @@ dependencies { api project(':structurizr-core') testImplementation project(':structurizr-dsl') - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' } \ No newline at end of file diff --git a/structurizr-neo4j/build.gradle b/structurizr-neo4j/build.gradle index 2c06e792e..a8990a997 100644 --- a/structurizr-neo4j/build.gradle +++ b/structurizr-neo4j/build.gradle @@ -1,9 +1,9 @@ dependencies { api project(':structurizr-core') - implementation 'org.neo4j.driver:neo4j-java-driver:5.23.0' + implementation 'org.neo4j.driver:neo4j-java-driver:5.24.0' testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' } \ No newline at end of file From d5e0d201e5fc9e1d6e6d4f3ea6d694e2706afbb0 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 26 Sep 2024 23:19:17 +0100 Subject: [PATCH 290/418] Adds a couple more supporting types strategies. --- changelog.md | 4 ++ .../java/com/structurizr/component/Type.java | 20 ++++++++ ...tionWithPrefixSupportingTypesStrategy.java | 38 +++++++++++++++ ...tionWithSuffixSupportingTypesStrategy.java | 38 +++++++++++++++ ...ithPrefixSupportingTypesStrategyTests.java | 44 ++++++++++++++++++ ...ithSuffixSupportingTypesStrategyTests.java | 46 +++++++++++++++++++ .../implementation/ExampleRepository.java | 4 ++ .../implementation/ExampleRepositoryImpl.java | 4 ++ .../implementation/JdbcExampleRepository.java | 4 ++ .../dsl/ComponentFinderStrategyParser.java | 22 ++++++++- .../ComponentFinderStrategyParserTests.java | 34 +++++++++++++- 11 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategy.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepository.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/JdbcExampleRepository.java diff --git a/changelog.md b/changelog.md index 568f80a34..7db1bca08 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,10 @@ ## 3.1.0 (unreleased) - structurizr-client: Workspace archive file now includes the branch name in the filename. +- structurizr-component: Adds `ImplementationWithPrefixSupportingTypesStrategy`. +- structurizr-component: Adds `ImplementationWithSuffixSupportingTypesStrategy`. +- structurizr-dsl: Adds `supportingTypes implementation-prefix `. +- structurizr-dsl: Adds `supportingTypes implementation-suffix `. ## 3.0.0 (19th September 2024) diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index 3e3be19ec..dea55129f 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -2,6 +2,8 @@ import com.structurizr.util.StringUtils; import org.apache.bcel.classfile.*; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import java.util.*; @@ -10,6 +12,8 @@ */ public class Type { + private static final Log log = LogFactory.getLog(Type.class); + private static final String STRUCTURIZR_TAG_ANNOTATION = "Lcom/structurizr/annotation/Tag;"; private static final String STRUCTURIZR_TAGS_ANNOTATION = "Lcom/structurizr/annotation/Tags;"; @@ -92,6 +96,10 @@ public boolean isAbstractClass() { return javaClass.isAbstract() && javaClass.isClass(); } + public boolean isInterface() { + return javaClass.isInterface(); + } + public List getTags() { List tags = new ArrayList<>(); @@ -159,4 +167,16 @@ public String toString() { return this.fullyQualifiedName; } + public boolean implementsInterface(Type type) { + if (javaClass != null) { + try { + return javaClass.implementationOf(type.javaClass); + } catch (ClassNotFoundException e) { + log.warn(e); + } + } + + return false; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategy.java new file mode 100644 index 000000000..1d0b64fe5 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategy.java @@ -0,0 +1,38 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A strategy that, given an interface, finds the implementation class with the specified prefix. + */ +public class ImplementationWithPrefixSupportingTypesStrategy implements SupportingTypesStrategy { + + private final String prefix; + + public ImplementationWithPrefixSupportingTypesStrategy(String prefix) { + this.prefix = prefix; + } + + @Override + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { + if (!type.isInterface()) { + throw new IllegalArgumentException("The type " + type.getFullyQualifiedName() + " is not an interface"); + } + + return typeRepository.getTypes().stream() + .filter(dependency -> dependency.implementsInterface(type) && dependency.getName().equals(prefix + type.getName())) + .collect(Collectors.toSet()); + } + + @Override + public String toString() { + return "ImplementationWithPrefixSupportingTypesStrategy{" + + "prefix='" + prefix + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategy.java new file mode 100644 index 000000000..f14006cea --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategy.java @@ -0,0 +1,38 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A strategy that, given an interface, finds the implementation class with the specified suffix. + */ +public class ImplementationWithSuffixSupportingTypesStrategy implements SupportingTypesStrategy { + + private final String suffix; + + public ImplementationWithSuffixSupportingTypesStrategy(String suffix) { + this.suffix = suffix; + } + + @Override + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { + if (!type.isInterface()) { + throw new IllegalArgumentException("The type " + type.getFullyQualifiedName() + " is not an interface"); + } + + return typeRepository.getTypes().stream() + .filter(dependency -> dependency.implementsInterface(type) && dependency.getName().equals(type.getName() + suffix)) + .collect(Collectors.toSet()); + } + + @Override + public String toString() { + return "ImplementationWithSuffixSupportingTypesStrategy{" + + "suffix='" + suffix + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategyTests.java new file mode 100644 index 000000000..6afccd312 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategyTests.java @@ -0,0 +1,44 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; +import org.apache.bcel.classfile.ClassParser; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class ImplementationWithPrefixSupportingTypesStrategyTests { + + private final File classes = new File("build/classes/java/test"); + + @Test + void findSupportingTypes() throws Exception { + Type interfaceType = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepository.class").getAbsolutePath()).parse()); + Type implementationTypeWithPrefix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse()); + Type implementationTypeWithSuffix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.class").getAbsolutePath()).parse()); + + TypeRepository typeRepository = new TypeRepository(); + typeRepository.add(interfaceType); + typeRepository.add(implementationTypeWithPrefix); + typeRepository.add(implementationTypeWithSuffix); + + Set supportingTypes = new ImplementationWithPrefixSupportingTypesStrategy("Jdbc").findSupportingTypes(interfaceType, typeRepository); + assertEquals(1, supportingTypes.size()); + assertTrue(supportingTypes.contains(implementationTypeWithPrefix)); + } + + @Test + void findSupportingTypes_ThrowsAnException_WhenTheTypeIsNotAnInterface() throws Exception { + try { + Type type = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse()); + new ImplementationWithPrefixSupportingTypesStrategy("Impl").findSupportingTypes(type, null); + fail(); + } catch (Exception e) { + assertEquals("The type com.structurizr.component.supporting.implementation.JdbcExampleRepository is not an interface", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategyTests.java new file mode 100644 index 000000000..1929432ad --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategyTests.java @@ -0,0 +1,46 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; +import org.apache.bcel.classfile.ClassFormatException; +import org.apache.bcel.classfile.ClassParser; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class ImplementationWithSuffixSupportingTypesStrategyTests { + + private final File classes = new File("build/classes/java/test"); + + @Test + void findSupportingTypes() throws Exception { + Type interfaceType = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepository.class").getAbsolutePath()).parse()); + Type implementationTypeWithPrefix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse()); + Type implementationTypeWithSuffix = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.class").getAbsolutePath()).parse()); + + TypeRepository typeRepository = new TypeRepository(); + typeRepository.add(interfaceType); + typeRepository.add(implementationTypeWithPrefix); + typeRepository.add(implementationTypeWithSuffix); + + Set supportingTypes = new ImplementationWithSuffixSupportingTypesStrategy("Impl").findSupportingTypes(interfaceType, typeRepository); + assertEquals(1, supportingTypes.size()); + assertTrue(supportingTypes.contains(implementationTypeWithSuffix)); + } + + @Test + void findSupportingTypes_ThrowsAnException_WhenTheTypeIsNotAnInterface() throws Exception { + try { + Type type = new Type(new ClassParser(new File(classes, "com/structurizr/component/supporting/implementation/JdbcExampleRepository.class").getAbsolutePath()).parse()); + new ImplementationWithSuffixSupportingTypesStrategy("Impl").findSupportingTypes(type, null); + fail(); + } catch (Exception e) { + assertEquals("The type com.structurizr.component.supporting.implementation.JdbcExampleRepository is not an interface", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepository.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepository.java new file mode 100644 index 000000000..6765edae1 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.supporting.implementation; + +public interface ExampleRepository { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.java new file mode 100644 index 000000000..14447809b --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.java @@ -0,0 +1,4 @@ +package com.structurizr.component.supporting.implementation; + +public class ExampleRepositoryImpl implements ExampleRepository { +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/JdbcExampleRepository.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/JdbcExampleRepository.java new file mode 100644 index 000000000..3a565ab90 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/JdbcExampleRepository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.supporting.implementation; + +public class JdbcExampleRepository implements ExampleRepository { +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index 340f437e8..9c73a68b9 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -39,8 +39,12 @@ final class ComponentFinderStrategyParser extends AbstractParser { private static final String SUPPORTING_TYPES_REFERENCED_IN_PACKAGE = "referenced-in-package"; private static final String SUPPORTING_TYPES_IN_PACKAGE = "in-package"; private static final String SUPPORTING_TYPES_UNDER_PACKAGE = "under-package"; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX = "implementation-prefix"; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX = "implementation-suffix"; private static final String SUPPORTING_TYPES_NONE = "none"; - private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_NONE)) + "> [parameters]"; + private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX, SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX, SUPPORTING_TYPES_NONE)) + "> [parameters]"; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX_GRAMMAR = "supportingTypes implementation-prefix "; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX_GRAMMAR = "supportingTypes implementation-suffix "; private static final String NAME_TYPE_NAME = "type-name"; private static final String NAME_FQN = "fqn"; @@ -185,6 +189,22 @@ void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens toke case SUPPORTING_TYPES_UNDER_PACKAGE: context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesUnderPackageSupportingTypesStrategy()); break; + case SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX: + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX_GRAMMAR); + } + + String prefix = tokens.get(2); + context.getComponentFinderStrategyBuilder().supportedBy(new ImplementationWithPrefixSupportingTypesStrategy(prefix)); + break; + case SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX: + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX_GRAMMAR); + } + + String suffix = tokens.get(2); + context.getComponentFinderStrategyBuilder().supportedBy(new ImplementationWithSuffixSupportingTypesStrategy(suffix)); + break; case SUPPORTING_TYPES_NONE: context.getComponentFinderStrategyBuilder().supportedBy(new DefaultSupportingTypesStrategy()); break; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index 6ea5eb258..f953aaaee 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -201,10 +201,42 @@ void test_parseSupportingTypes_ThrowsAnException_WhenNoTypeIsSpecified() { parser.parseSupportingTypes(context, tokens("supportingTypes"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: supportingTypes [parameters]", e.getMessage()); + assertEquals("Too few tokens, expected: supportingTypes [parameters]", e.getMessage()); } } + @Test + void test_parseSupportingTypes_ThrowsAnException_WhenImplementationSuffixIsUsedWithoutASuffix() { + try { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-suffix"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: supportingTypes implementation-suffix ", e.getMessage()); + } + } + + @Test + void test_parseSupportingTypes_ImplementationSuffix() { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-suffix", "Impl"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=ImplementationWithSuffixSupportingTypesStrategy{suffix='Impl'}, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseSupportingTypes_ThrowsAnException_WhenImplementationPrefixIsUsedWithoutAPrefix() { + try { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-prefix"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: supportingTypes implementation-prefix ", e.getMessage()); + } + } + + @Test + void test_parseSupportingTypes_ImplementationPrefix() { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-prefix", "Jdbc"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=ImplementationWithPrefixSupportingTypesStrategy{prefix='Jdbc'}, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + @Test void test_parseName_ThrowsAnException_WhenNoTypeIsSpecified() { try { From ff37cc29fd4a9bfd9a79b669bae9406a95e1f750 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:11:11 +0100 Subject: [PATCH 291/418] See https://github.com/structurizr/java/issues/344 --- .../dsl/ExternalScriptDslContext.java | 8 +-- .../com/structurizr/dsl/ScriptDslContext.java | 67 ++++++++++++------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java index 772e36545..b8e8c4070 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java @@ -1,9 +1,6 @@ package com.structurizr.dsl; import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.List; class ExternalScriptDslContext extends ScriptDslContext { @@ -23,10 +20,7 @@ void end() { throw new RuntimeException("Script file " + scriptFile.getCanonicalPath() + " does not exist"); } - String fileExtension = filename.substring(filename.lastIndexOf('.') + 1); - List lines = Files.readAllLines(scriptFile.toPath(), StandardCharsets.UTF_8); - - run(this, fileExtension, lines); + run(this, scriptFile); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("Error running script at " + filename + ", caused by " + e.getClass().getName() + ": " + e.getMessage()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java index 6e11529f1..6463e8324 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java @@ -3,10 +3,9 @@ import com.structurizr.model.Element; import com.structurizr.model.Relationship; -import javax.script.Bindings; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; +import javax.script.*; import java.io.File; +import java.io.FileReader; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,32 +47,54 @@ void run(DslContext context, String extension, List lines) throws Except if (engine != null) { Bindings bindings = engine.createBindings(); - bindings.put(WORKSPACE_VARIABLE_NAME, context.getWorkspace()); - - if (parentContext instanceof ViewDslContext) { - bindings.put(VIEW_VARIABLE_NAME, ((ViewDslContext)parentContext).getView()); - } else if (parentContext instanceof ModelItemDslContext) { - ModelItemDslContext modelItemDslContext = (ModelItemDslContext)parentContext; - if (modelItemDslContext.getModelItem() instanceof Element) { - bindings.put(ELEMENT_VARIABLE_NAME, modelItemDslContext.getModelItem()); - } else if (modelItemDslContext.getModelItem() instanceof Relationship) { - bindings.put(RELATIONSHIP_VARIABLE_NAME, modelItemDslContext.getModelItem()); - } - } + populateBindings(bindings, context); - // bind a context object - StructurizrDslScriptContext scriptContext = new StructurizrDslScriptContext(dslParser, dslFile, getWorkspace(), parameters); - bindings.put(CONTEXT_VARIABLE_NAME, scriptContext); + engine.eval(script.toString(), bindings); + } else { + throw new RuntimeException("Could not load a scripting engine for extension \"" + extension + "\""); + } + } - // and any custom parameters - for (String name : parameters.keySet()) { - bindings.put(name, parameters.get(name)); - } + void run(DslContext context, File scriptFile) throws Exception { + String extension = scriptFile.getName().substring(scriptFile.getName().lastIndexOf('.') + 1); + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByExtension(extension); - engine.eval(script.toString(), bindings); + if (engine != null) { + Bindings bindings = engine.createBindings(); + populateBindings(bindings, context); + + ScriptContext scriptContext = new SimpleScriptContext(); + scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE); + scriptContext.setAttribute(ScriptEngine.FILENAME, scriptFile.getAbsolutePath(), ScriptContext.ENGINE_SCOPE); + engine.eval(new FileReader(scriptFile), scriptContext); } else { throw new RuntimeException("Could not load a scripting engine for extension \"" + extension + "\""); } } + private void populateBindings(Bindings bindings, DslContext context) { + bindings.put(WORKSPACE_VARIABLE_NAME, context.getWorkspace()); + + if (parentContext instanceof ViewDslContext) { + bindings.put(VIEW_VARIABLE_NAME, ((ViewDslContext)parentContext).getView()); + } else if (parentContext instanceof ModelItemDslContext) { + ModelItemDslContext modelItemDslContext = (ModelItemDslContext)parentContext; + if (modelItemDslContext.getModelItem() instanceof Element) { + bindings.put(ELEMENT_VARIABLE_NAME, modelItemDslContext.getModelItem()); + } else if (modelItemDslContext.getModelItem() instanceof Relationship) { + bindings.put(RELATIONSHIP_VARIABLE_NAME, modelItemDslContext.getModelItem()); + } + } + + // bind a context object + StructurizrDslScriptContext scriptContext = new StructurizrDslScriptContext(dslParser, dslFile, getWorkspace(), parameters); + bindings.put(CONTEXT_VARIABLE_NAME, scriptContext); + + // and any custom parameters + for (String name : parameters.keySet()) { + bindings.put(name, parameters.get(name)); + } + } + } \ No newline at end of file From ba5779566e9a8e4b0baf584ee39c4fd2261aa309 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 5 Oct 2024 12:35:57 +0100 Subject: [PATCH 292/418] Fixes #346. --- changelog.md | 1 + .../src/main/java/com/structurizr/dsl/StructurizrDslParser.java | 2 +- structurizr-dsl/src/test/resources/dsl/test.dsl | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 7db1bca08..b4d3c9760 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ - structurizr-component: Adds `ImplementationWithSuffixSupportingTypesStrategy`. - structurizr-dsl: Adds `supportingTypes implementation-prefix `. - structurizr-dsl: Adds `supportingTypes implementation-suffix `. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/346 (`// comment \` joins lines). ## 3.0.0 (19th September 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 1d1d0e759..a09b0cd13 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1069,7 +1069,7 @@ private List preProcessLines(List lines) { boolean lineComplete = true; for (String line : lines) { - if (line.endsWith(MULTI_LINE_SEPARATOR)) { + if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(MULTI_LINE_SEPARATOR)) { buf.append(line, 0, line.length()-1); lineComplete = false; } else { diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 994ea2d39..7deeaf866 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -27,6 +27,7 @@ workspace "Name" "Description" { !impliedRelationships "com.structurizr.model.CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy" !impliedRelationships true + // single line comment with long line split character \ properties { "Name" "Value" } From d57e6aeefe806212a795e8b5cba3c58e03fa5604 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 5 Oct 2024 12:47:35 +0100 Subject: [PATCH 293/418] Anonymous identifiers for relationships (i.e. relationships not assigned to an identifier) are excluded from the model, and therefore also excluded from the serialised JSON. --- .../com/structurizr/dsl/StructurizrDslParser.java | 5 ++++- .../test/java/com/structurizr/dsl/DslTests.java | 14 ++++++++++++++ .../dsl/relationship-without-identifier.dsl | 9 +++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/relationship-without-identifier.dsl diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index a09b0cd13..dc11d5217 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1181,7 +1181,10 @@ void registerIdentifier(String identifier, Element element) { void registerIdentifier(String identifier, Relationship relationship) { identifiersRegister.register(identifier, relationship); - relationship.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(relationship)); + + if (!StringUtils.isNullOrEmpty(identifier)) { + relationship.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(relationship)); + } } /** diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 7ff0184aa..41c50e880 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1007,6 +1007,20 @@ void test_identifiers() throws Exception { assertNull(impliedRelationship.getProperties().get("structurizr.dsl.identifier")); } + @Test + void test_relationshipWithoutIdentifier() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/relationship-without-identifier.dsl")); + + Workspace workspace = parser.getWorkspace(); + IdentifiersRegister register = parser.getIdentifiersRegister(); + assertEquals(1, workspace.getModel().getRelationships().size()); + Relationship relationship = workspace.getModel().getRelationships().iterator().next(); + + assertTrue(register.findIdentifier(relationship).matches("[\\w]{8}-[\\w]{4}-[\\w]{4}-[\\w]{4}-[\\w]{12}")); + assertNull(relationship.getProperties().get("structurizr.dsl.identifier")); // identifier is not included in model + } + @Test void test_imageViews_ViaFiles() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); diff --git a/structurizr-dsl/src/test/resources/dsl/relationship-without-identifier.dsl b/structurizr-dsl/src/test/resources/dsl/relationship-without-identifier.dsl new file mode 100644 index 000000000..b07a91125 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/relationship-without-identifier.dsl @@ -0,0 +1,9 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + a -> b + } + +} \ No newline at end of file From 3d223174e988197a5296639fda827c9fc3b58ac0 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 5 Oct 2024 12:48:21 +0100 Subject: [PATCH 294/418] . --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index b4d3c9760..8925cb7cb 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ - structurizr-dsl: Adds `supportingTypes implementation-prefix `. - structurizr-dsl: Adds `supportingTypes implementation-suffix `. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/346 (`// comment \` joins lines). +- structurizr-dsl: Anonymous identifiers for relationships (i.e. relationships not assigned to an identifier) are excluded from the model, and therefore also excluded from the serialised JSON. ## 3.0.0 (19th September 2024) From f103a7a876905b04712dbbd9779aa28568b0dbb6 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:59:19 +0100 Subject: [PATCH 295/418] Removes duplicate PlantUML/Mermaid encoders, adds support for Mermaid compression, makes SVG the default formats. --- .../java/com/structurizr/dsl/DslTests.java | 8 +- .../dsl/ImageViewContentParserTests.java | 8 +- .../dsl/image-views/workspace-via-file.dsl | 1 + .../dsl/image-views/workspace-via-url.dsl | 1 + .../export/mermaid/MermaidEncoder.java | 18 ----- .../export/plantuml/PlantUMLEncoder.java | 73 ------------------- .../diagrams/mermaid/MermaidEncoder.java | 32 +++++++- .../diagrams/mermaid/MermaidImporter.java | 14 +++- .../diagrams/plantuml/PlantUMLEncoder.java | 4 +- .../diagrams/plantuml/PlantUMLImporter.java | 6 +- .../diagrams/mermaid/MermaidEncoderTests.java | 13 +++- .../mermaid/MermaidImporterTests.java | 21 +++--- .../plantuml/PlantUMLImporterTests.java | 24 +++--- 13 files changed, 92 insertions(+), 131 deletions(-) delete mode 100644 structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java delete mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 41c50e880..62125ba96 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1031,13 +1031,13 @@ void test_imageViews_ViaFiles() throws Exception { ImageView plantumlView = (ImageView)workspace.getViews().getViewWithKey("plantuml"); assertEquals("diagram.puml", plantumlView.getTitle()); - assertEquals("http://localhost:7777/png/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", plantumlView.getContent()); - assertEquals("image/png", plantumlView.getContentType()); + assertEquals("http://localhost:7777/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", plantumlView.getContent()); + assertEquals("image/svg+xml", plantumlView.getContentType()); ImageView mermaidView = (ImageView)workspace.getViews().getViewWithKey("mermaid"); assertEquals("diagram.mmd", mermaidView.getTitle()); - assertEquals("http://localhost:8888/img/Zmxvd2NoYXJ0IFRECiAgICBTdGFydCAtLT4gU3RvcA==?type=png", mermaidView.getContent()); - assertEquals("image/png", mermaidView.getContentType()); + assertEquals("http://localhost:8888/svg/Zmxvd2NoYXJ0IFRECiAgICBTdGFydCAtLT4gU3RvcA==", mermaidView.getContent()); + assertEquals("image/svg+xml", mermaidView.getContentType()); ImageView krokiView = (ImageView)workspace.getViews().getViewWithKey("kroki"); assertEquals("diagram.dot", krokiView.getTitle()); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index 4e693973e..fe2eef5eb 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -20,8 +20,10 @@ void setUp() { @Test void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); parser = new ImageViewContentParser(true); - parser.parsePlantUML(new ImageViewDslContext(imageView), null, tokens("plantuml", "image.puml")); + parser.parsePlantUML(context, null, tokens("plantuml", "image.puml")); fail(); } catch (Exception e) { assertEquals("PlantUML source must be specified as a URL when running in restricted mode", e.getMessage()); @@ -31,8 +33,10 @@ void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { @Test void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); parser = new ImageViewContentParser(true); - parser.parseMermaid(new ImageViewDslContext(imageView), null, tokens("mermaid", "image.puml")); + parser.parseMermaid(context, null, tokens("mermaid", "image.puml")); fail(); } catch (Exception e) { assertEquals("Mermaid source must be specified as a URL when running in restricted mode", e.getMessage()); diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl index 93ec34d37..3cfeed7e9 100644 --- a/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl +++ b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl @@ -4,6 +4,7 @@ workspace { properties { "plantuml.url" "http://localhost:7777" "mermaid.url" "http://localhost:8888" + "mermaid.compress" "false" "kroki.url" "http://localhost:9999" } diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl index 99fe3859b..f42f657ac 100644 --- a/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl +++ b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl @@ -6,6 +6,7 @@ workspace { "plantuml.format" "svg" "mermaid.url" "http://localhost:8888" "mermaid.format" "svg" + "mermaid.compress" "false" "kroki.url" "http://localhost:9999" "kroki.format" "svg" } diff --git a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java deleted file mode 100644 index 3f1e9fe9d..000000000 --- a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidEncoder.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.structurizr.export.mermaid; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -/** - * Encodes a Mermaid diagram definition to base64 format, for use with image URLs, etc. - */ -public class MermaidEncoder { - - private static final String TEMPLATE = "{ \"code\":\"%s\", \"mermaid\":{\"theme\":\"default\", \"securityLevel\": \"loose\"}}"; - - public String encode(String mermaidDefinition) { - String s = String.format(TEMPLATE, mermaidDefinition.replaceAll("\n", "\\\\n").replaceAll("\"", "\\\\\"")); - return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)); - } - -} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java deleted file mode 100644 index 1b5d24da7..000000000 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLEncoder.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.structurizr.export.plantuml; - -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; - -/** - * A Java implementation of http://plantuml.com/code-javascript-synchronous - * that uses Java's built-in Deflate algorithm. - */ -public class PlantUMLEncoder { - - public String encode(String plantUMLDefinition) throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true); - - DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, true); - dos.write(plantUMLDefinition.getBytes(StandardCharsets.UTF_8)); - dos.finish(); - - return encode(baos.toByteArray()); - } - - private String encode(byte[] bytes) { - StringBuilder buf = new StringBuilder(); - for (int i = 0; i < bytes.length; i += 3) { - int b1 = (bytes[i]) & 0xFF; - int b2 = (i + 1 < bytes.length ? bytes[i + 1] : (byte)0) & 0xFF; - int b3 = (i + 2 < bytes.length ? bytes[i + 2] : (byte)0) & 0xFF; - - append3bytes(buf, b1, b2, b3); - } - - return buf.toString(); - } - - private char encode6bit(byte b) { - if (b < 10) { - return (char) ('0' + b); - } - b -= 10; - if (b < 26) { - return (char) ('A' + b); - } - b -= 26; - if (b < 26) { - return (char) ('a' + b); - } - b -= 26; - if (b == 0) { - return '-'; - } - if (b == 1) { - return '_'; - } - - return '?'; - } - - private void append3bytes(StringBuilder buf, int b1, int b2, int b3) { - int c1 = b1 >> 2; - int c2 = (b1 & 0x3) << 4 | b2 >> 4; - int c3 = (b2 & 0xF) << 2 | b3 >> 6; - int c4 = b3 & 0x3F; - - buf.append(encode6bit((byte)(c1 & 0x3F))); - buf.append(encode6bit((byte)(c2 & 0x3F))); - buf.append(encode6bit((byte)(c3 & 0x3F))); - buf.append(encode6bit((byte)(c4 & 0x3F))); - } - -} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java index 8bbc4bc91..553d33d78 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java @@ -1,15 +1,45 @@ package com.structurizr.importer.diagrams.mermaid; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; /** * Encodes a Mermaid diagram definition to base64 format, for use with image URLs, etc. */ -public class MermaidEncoder { +public final class MermaidEncoder { + + private static final String TEMPLATE = "{ \"code\":\"%s\", \"mermaid\":{\"theme\":\"default\"}}"; public String encode(String mermaidDefinition) { + return this.encode(mermaidDefinition, false); + } + + public String encode(String mermaidDefinition, boolean compress) { + if (compress) { + try { + String content = String.format(TEMPLATE, mermaidDefinition.replaceAll("\n", "\\\\n").replaceAll("\"", "\\\\\"")); + byte[] compressedDefinition = compress(content); + return "pako:" + Base64.getUrlEncoder().encodeToString(compressedDefinition); + } catch(Exception e) { + e.printStackTrace(); + } + } + return Base64.getUrlEncoder().encodeToString(mermaidDefinition.getBytes(StandardCharsets.UTF_8)); } + private byte[] compress(String content) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(); + + DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, true); + dos.write(content.getBytes(StandardCharsets.UTF_8)); + dos.finish(); + + return baos.toByteArray(); + } + } \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java index 9168fff71..c7fa23b44 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java @@ -10,8 +10,9 @@ public class MermaidImporter extends AbstractDiagramImporter { - private static final String MERMAID_URL_PROPERTY = "mermaid.url"; - private static final String MERMAID_FORMAT_PROPERTY = "mermaid.format"; + public static final String MERMAID_URL_PROPERTY = "mermaid.url"; + public static final String MERMAID_FORMAT_PROPERTY = "mermaid.format"; + public static final String MERMAID_COMPRESS_PROPERTY = "mermaid.compress"; public void importDiagram(ImageView view, File file) throws Exception { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); @@ -28,14 +29,19 @@ public void importDiagram(ImageView view, String content) { String format = getViewOrViewSetProperty(view, MERMAID_FORMAT_PROPERTY); if (StringUtils.isNullOrEmpty(format)) { - format = PNG_FORMAT; + format = SVG_FORMAT; } if (!format.equals(PNG_FORMAT) && !format.equals(SVG_FORMAT)) { throw new IllegalArgumentException(String.format("Expected a format of %s or %s", PNG_FORMAT, SVG_FORMAT)); } - String encodedMermaid = new MermaidEncoder().encode(content); + String compress = getViewOrViewSetProperty(view, MERMAID_COMPRESS_PROPERTY); + if (StringUtils.isNullOrEmpty(compress)) { + compress = "true"; + } + + String encodedMermaid = new MermaidEncoder().encode(content, compress.equalsIgnoreCase("true")); String url; if (format.equals(PNG_FORMAT)) { url = String.format("%s/img/%s?type=png", mermaidServer, encodedMermaid); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java index 20238926b..96088b150 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoder.java @@ -9,9 +9,9 @@ * A Java implementation of http://plantuml.com/code-javascript-synchronous * that uses Java's built-in Deflate algorithm. */ -class PlantUMLEncoder { +public final class PlantUMLEncoder { - String encode(String plantUMLDefinition) throws Exception { + public String encode(String plantUMLDefinition) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java index 56ed5a835..3ed7651e0 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -10,8 +10,8 @@ public class PlantUMLImporter extends AbstractDiagramImporter { - private static final String PLANTUML_URL_PROPERTY = "plantuml.url"; - private static final String PLANTUML_FORMAT_PROPERTY = "plantuml.format"; + public static final String PLANTUML_URL_PROPERTY = "plantuml.url"; + public static final String PLANTUML_FORMAT_PROPERTY = "plantuml.format"; private static final String TITLE_STRING = "title "; private static final String NEWLINE = "\n"; @@ -30,7 +30,7 @@ public void importDiagram(ImageView view, String content) throws Exception { String format = getViewOrViewSetProperty(view, PLANTUML_FORMAT_PROPERTY); if (StringUtils.isNullOrEmpty(format)) { - format = PNG_FORMAT; + format = SVG_FORMAT; } if (!format.equals(PNG_FORMAT) && !format.equals(SVG_FORMAT)) { diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java index b249e45ff..b4b69190b 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import java.io.File; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -14,7 +13,16 @@ public class MermaidEncoderTests { public void encode_flowchart() throws Exception { File file = new File("./src/test/resources/diagrams/mermaid/flowchart.mmd"); String mermaid = Files.readString(file.toPath()); - assertEquals("Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==", new MermaidEncoder().encode(mermaid)); + String encodedMermaid = new MermaidEncoder().encode(mermaid); + assertEquals("Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==", encodedMermaid); + } + + @Test + public void encode_flowchart_compressed() throws Exception { + File file = new File("./src/test/resources/diagrams/mermaid/flowchart.mmd"); + String mermaid = Files.readString(file.toPath()); + String encodedMermaid = new MermaidEncoder().encode(mermaid, true); + assertEquals("pako:eJxVj70OgjAUhV_lppMm8gIMJlKUhUQHtspwAxfbSH9SaoihvLsgi571-85JzgSssS2xlHW9HRuJPkCV3w0sOQkuvRqCxqGGJDnGggJoa-gdIdsVFgZpnVPmsd_8bJWAT-WqEQSpzHPeEP_2r4Yi5KJEF6yrf0k12ghnoW5ymf8n0tPSuogO0w6TBj1w9DU7ANPkNaqWpRMLkvR6oqUOX31g8_wBLY9E1w==", encodedMermaid); } @Test @@ -22,7 +30,6 @@ public void encode_class() throws Exception { File file = new File("./src/test/resources/diagrams/mermaid/class.mmd"); String mermaid = Files.readString(file.toPath()); assertEquals("Y2xhc3NEaWFncmFtCiAgICBBbmltYWwgPHwtLSBEdWNrCiAgICBBbmltYWwgPHwtLSBGaXNoCiAgICBBbmltYWwgPHwtLSBaZWJyYQogICAgQW5pbWFsIDogK2ludCBhZ2UKICAgIEFuaW1hbCA6ICtTdHJpbmcgZ2VuZGVyCiAgICBBbmltYWw6ICtpc01hbW1hbCgpCiAgICBBbmltYWw6ICttYXRlKCkKICAgIGNsYXNzIER1Y2t7CiAgICAgICtTdHJpbmcgYmVha0NvbG9yCiAgICAgICtzd2ltKCkKICAgICAgK3F1YWNrKCkKICAgIH0KICAgIGNsYXNzIEZpc2h7CiAgICAgIC1pbnQgc2l6ZUluRmVldAogICAgICAtY2FuRWF0KCkKICAgIH0KICAgIGNsYXNzIFplYnJhewogICAgICArYm9vbCBpc193aWxkCiAgICAgICtydW4oKQogICAgfQo=", new MermaidEncoder().encode(mermaid)); - } } \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java index 3cccf9ac8..5293f6c8f 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java @@ -13,7 +13,8 @@ public class MermaidImporterTests { @Test public void importDiagram() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_COMPRESS_PROPERTY, "false"); ImageView view = workspace.getViews().createImageView("key"); new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); @@ -21,15 +22,16 @@ public void importDiagram() throws Exception { assertNull(view.getElement()); assertNull(view.getElementId()); assertEquals("flowchart.mmd", view.getTitle()); - assertEquals("https://mermaid.ink/img/Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==?type=png", view.getContent()); - assertEquals("image/png", view.getContentType()); + assertEquals("https://mermaid.ink/svg/Zmxvd2NoYXJ0IFRECiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZykKICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfQogICAgQyAtLT58T25lfCBEW0xhcHRvcF0KICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdCiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXQ==", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); } @Test public void importDiagram_AsPNG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); - workspace.getViews().getConfiguration().addProperty("mermaid.format", "png"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_FORMAT_PROPERTY, "png"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_COMPRESS_PROPERTY, "false"); ImageView view = workspace.getViews().createImageView("key"); new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); @@ -44,8 +46,9 @@ public void importDiagram_AsPNG() throws Exception { @Test public void importDiagram_AsSVG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); - workspace.getViews().getConfiguration().addProperty("mermaid.format", "svg"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_FORMAT_PROPERTY, "svg"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_COMPRESS_PROPERTY, "false"); ImageView view = workspace.getViews().createImageView("key"); new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); @@ -73,8 +76,8 @@ public void importDiagram_WhenTheMermaidUrlIsNotDefined() throws Exception { @Test public void importDiagram_WhenAnInvalidFormatIsSpecified() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("mermaid.url", "https://mermaid.ink"); - workspace.getViews().getConfiguration().addProperty("mermaid.format", "jpg"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_FORMAT_PROPERTY, "jpg"); ImageView view = workspace.getViews().createImageView("key"); try { diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java index 7e51dcd50..ea21bf2aa 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java @@ -13,7 +13,7 @@ public class PlantUMLImporterTests { @Test public void importDiagram_WhenATitleIsDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); ImageView view = workspace.getViews().createImageView("key"); new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); @@ -21,14 +21,14 @@ public void importDiagram_WhenATitleIsDefined() throws Exception { assertNull(view.getElement()); assertNull(view.getElementId()); assertEquals("Sequence diagram example", view.getTitle()); - assertEquals("https://plantuml.com/plantuml/png/SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", view.getContent()); - assertEquals("image/png", view.getContentType()); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); } @Test public void importDiagram_WhenATitleIsNotDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); ImageView view = workspace.getViews().createImageView("key"); new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/without-title.puml")); @@ -36,15 +36,15 @@ public void importDiagram_WhenATitleIsNotDefined() throws Exception { assertNull(view.getElement()); assertNull(view.getElementId()); assertEquals("without-title.puml", view.getTitle()); - assertEquals("https://plantuml.com/plantuml/png/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", view.getContent()); - assertEquals("image/png", view.getContentType()); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); } @Test public void importDiagram_AsPNG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); - workspace.getViews().getConfiguration().addProperty("plantuml.format", "png"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_FORMAT_PROPERTY, "png"); ImageView view = workspace.getViews().createImageView("key"); new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); @@ -55,8 +55,8 @@ public void importDiagram_AsPNG() throws Exception { @Test public void importDiagram_AsSVG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); - workspace.getViews().getConfiguration().addProperty("plantuml.format", "svg"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_FORMAT_PROPERTY, "svg"); ImageView view = workspace.getViews().createImageView("key"); new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); @@ -80,8 +80,8 @@ public void importDiagram_WhenThePlantUMLURLIsNotSpecified() throws Exception { @Test public void importDiagram_WhenAnInvalidFormatIsSpecified() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("plantuml.url", "https://plantuml.com/plantuml"); - workspace.getViews().getConfiguration().addProperty("plantuml.format", "jpg"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_FORMAT_PROPERTY, "jpg"); ImageView view = workspace.getViews().createImageView("key"); try { From 34cd1491e0acb52a8e23df62769b9923aa0acaad Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sun, 6 Oct 2024 13:02:38 +0100 Subject: [PATCH 296/418] Adds a way to configure whether the DSL source is retained via a workspace property named `structurizr.dsl.source` - `true` (default) or `false`. --- changelog.md | 1 + .../java/com/structurizr/dsl/DslUtils.java | 15 +++----- .../structurizr/dsl/StructurizrDslParser.java | 5 ++- .../java/com/structurizr/dsl/DslTests.java | 38 +++++++++++++++++++ .../src/test/resources/dsl/source-child.dsl | 7 ++++ .../resources/dsl/source-not-retained.dsl | 11 ++++++ .../src/test/resources/dsl/source-parent.dsl | 7 ++++ 7 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/source-child.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/source-not-retained.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/source-parent.dsl diff --git a/changelog.md b/changelog.md index 8925cb7cb..e08f54ed5 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ - structurizr-dsl: Adds `supportingTypes implementation-suffix `. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/346 (`// comment \` joins lines). - structurizr-dsl: Anonymous identifiers for relationships (i.e. relationships not assigned to an identifier) are excluded from the model, and therefore also excluded from the serialised JSON. +- structurizr-dsl: Adds a way to configure whether the DSL source is retained via a workspace property named `structurizr.dsl.source` - `true` (default) or `false`. ## 3.0.0 (19th September 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java index 16e73789e..e20b936f2 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java @@ -11,7 +11,8 @@ */ public class DslUtils { - private static final String STRUCTURIZR_DSL_PROPERTY_NAME = "structurizr.dsl"; + static final String STRUCTURIZR_DSL_PROPERTY_NAME = "structurizr.dsl"; + static final String STRUCTURIZR_DSL_RETAIN_SOURCE_PROPERTY_NAME = "structurizr.dsl.source"; /** * Gets the DSL associated with a workspace. @@ -21,13 +22,11 @@ public class DslUtils { */ public static String getDsl(Workspace workspace) { String base64 = workspace.getProperties().get(STRUCTURIZR_DSL_PROPERTY_NAME); - String dsl = ""; - if (!StringUtils.isNullOrEmpty(base64)) { - dsl = new String(Base64.getDecoder().decode(base64)); + return new String(Base64.getDecoder().decode(base64)); } - return dsl; + return ""; } /** @@ -37,12 +36,10 @@ public static String getDsl(Workspace workspace) { * @param dsl the DSL string */ public static void setDsl(Workspace workspace, String dsl) { - String base64 = ""; if (!StringUtils.isNullOrEmpty(dsl)) { - base64 = Base64.getEncoder().encodeToString(dsl.getBytes(StandardCharsets.UTF_8)); + String base64 = Base64.getEncoder().encodeToString(dsl.getBytes(StandardCharsets.UTF_8)); + workspace.addProperty(STRUCTURIZR_DSL_PROPERTY_NAME, base64); } - - workspace.addProperty(STRUCTURIZR_DSL_PROPERTY_NAME, base64); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index dc11d5217..50f30b356 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -101,7 +101,10 @@ public void setRestricted(boolean restricted) { */ public Workspace getWorkspace() { if (workspace != null) { - DslUtils.setDsl(workspace, getParsedDsl()); + String value = workspace.getProperties().get(DslUtils.STRUCTURIZR_DSL_RETAIN_SOURCE_PROPERTY_NAME); + if (value == null || value.equalsIgnoreCase("true")) { + DslUtils.setDsl(workspace, getParsedDsl()); + } } return workspace; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 62125ba96..0c73de740 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1377,4 +1377,42 @@ void test_ImageView_WhenParserIsInRestrictedMode() { } } + @Test + void test_sourceIsRetained() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/source-parent.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + assertEquals(""" +workspace { + + model { + a = softwareSystem "A" + } + +}""", DslUtils.getDsl(workspace)); + + File childDslFile = new File("src/test/resources/dsl/source-child.dsl"); + parser = new StructurizrDslParser(); + parser.parse(childDslFile); + workspace = parser.getWorkspace(); + assertEquals(""" +workspace extends source-parent.dsl { + + model { + b = softwareSystem "B" + } + +}""", DslUtils.getDsl(workspace)); + } + + @Test + void test_sourceIsNotRetained() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/source-not-retained.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + assertNull(workspace.getProperties().get(DslUtils.STRUCTURIZR_DSL_PROPERTY_NAME)); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/source-child.dsl b/structurizr-dsl/src/test/resources/dsl/source-child.dsl new file mode 100644 index 000000000..54ca1a6de --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/source-child.dsl @@ -0,0 +1,7 @@ +workspace extends source-parent.dsl { + + model { + b = softwareSystem "B" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/source-not-retained.dsl b/structurizr-dsl/src/test/resources/dsl/source-not-retained.dsl new file mode 100644 index 000000000..a09c05b91 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/source-not-retained.dsl @@ -0,0 +1,11 @@ +workspace { + + properties { + structurizr.dsl.source false + } + + model { + a = softwareSystem "A" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/source-parent.dsl b/structurizr-dsl/src/test/resources/dsl/source-parent.dsl new file mode 100644 index 000000000..a19e355ac --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/source-parent.dsl @@ -0,0 +1,7 @@ +workspace { + + model { + a = softwareSystem "A" + } + +} \ No newline at end of file From b412261494684383c5ce34d54527bd7d7543e56a Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sun, 6 Oct 2024 13:53:24 +0100 Subject: [PATCH 297/418] structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view. --- changelog.md | 1 + structurizr-dsl/build.gradle | 1 + .../dsl/ImageViewContentParser.java | 61 ++++++++++++------- .../dsl/ImageViewContentParserTests.java | 54 ++++++++++++++++ .../export/AbstractDiagramExporter.java | 20 ++++++ .../plantuml/PlantUMLEncoderTests.java | 5 +- 6 files changed, 117 insertions(+), 25 deletions(-) diff --git a/changelog.md b/changelog.md index e08f54ed5..fee0fefc8 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/346 (`// comment \` joins lines). - structurizr-dsl: Anonymous identifiers for relationships (i.e. relationships not assigned to an identifier) are excluded from the model, and therefore also excluded from the serialised JSON. - structurizr-dsl: Adds a way to configure whether the DSL source is retained via a workspace property named `structurizr.dsl.source` - `true` (default) or `false`. +- structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view. ## 3.0.0 (19th September 2024) diff --git a/structurizr-dsl/build.gradle b/structurizr-dsl/build.gradle index 1a38536b6..b5d1a6a55 100644 --- a/structurizr-dsl/build.gradle +++ b/structurizr-dsl/build.gradle @@ -2,6 +2,7 @@ dependencies { api project(':structurizr-client') api project(':structurizr-import') + api project(':structurizr-export') api project(':structurizr-component') testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.22' diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index f59f5fcb5..2866f9b16 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -1,24 +1,25 @@ package com.structurizr.dsl; +import com.structurizr.export.mermaid.MermaidDiagramExporter; +import com.structurizr.export.plantuml.StructurizrPlantUMLExporter; import com.structurizr.importer.diagrams.kroki.KrokiImporter; import com.structurizr.importer.diagrams.mermaid.MermaidImporter; import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; import com.structurizr.util.ImageUtils; import com.structurizr.util.Url; import com.structurizr.view.ImageView; +import com.structurizr.view.ModelView; +import com.structurizr.view.View; import java.io.File; final class ImageViewContentParser extends AbstractParser { - private static final String PLANTUML_GRAMMAR = "plantuml "; - private static final String MERMAID_GRAMMAR = "mermaid "; + private static final String PLANTUML_GRAMMAR = "plantuml "; + private static final String MERMAID_GRAMMAR = "mermaid "; private static final String KROKI_GRAMMAR = "kroki "; private static final String IMAGE_GRAMMAR = "image "; - private static final int TITLE_INDEX = 1; - private static final int DESCRIPTION_INDEX = 1; - private static final int PLANTUML_SOURCE_INDEX = 1; private static final int MERMAID_SOURCE_INDEX = 1; private static final int KROKI_FORMAT_INDEX = 1; @@ -32,7 +33,7 @@ final class ImageViewContentParser extends AbstractParser { } void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { - // plantuml + // plantuml if (tokens.hasMoreThan(PLANTUML_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + PLANTUML_GRAMMAR); @@ -44,16 +45,23 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { String source = tokens.get(PLANTUML_SOURCE_INDEX); try { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new PlantUMLImporter().importDiagram(context.getView(), content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); + if (viewWithKey instanceof ModelView) { + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + String plantuml = exporter.export((ModelView)viewWithKey).getDefinition(); + new PlantUMLImporter().importDiagram(context.getView(), plantuml); } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - new PlantUMLImporter().importDiagram(context.getView(), file); + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new PlantUMLImporter().importDiagram(context.getView(), content.getContent()); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + new PlantUMLImporter().importDiagram(context.getView(), file); + } else { + throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); + } } } } catch (Exception e) { @@ -66,7 +74,7 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { } void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { - // mermaid + // mermaid if (tokens.hasMoreThan(MERMAID_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + MERMAID_GRAMMAR); @@ -78,16 +86,23 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { String source = tokens.get(MERMAID_SOURCE_INDEX); try { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new MermaidImporter().importDiagram(context.getView(), content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); + if (viewWithKey instanceof ModelView) { + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + String mermaid = exporter.export((ModelView)viewWithKey).getDefinition(); + new MermaidImporter().importDiagram(context.getView(), mermaid); } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - new MermaidImporter().importDiagram(context.getView(), file); + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new MermaidImporter().importDiagram(context.getView(), content.getContent()); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + new MermaidImporter().importDiagram(context.getView(), file); + } else { + throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); + } } } } catch (Exception e) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index fe2eef5eb..ca65bcadc 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -1,5 +1,7 @@ package com.structurizr.dsl; +import com.structurizr.importer.diagrams.mermaid.MermaidImporter; +import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; import com.structurizr.view.ImageView; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,6 +19,19 @@ void setUp() { imageView = workspace.getViews().createImageView("key"); } + @Test + void test_parsePlantUML_ThrowsAnException_WithTooFewTokens() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + parser = new ImageViewContentParser(true); + parser.parsePlantUML(context, null, tokens("plantuml")); + fail(); + } catch (Exception e) { + assertEquals("Expected: plantuml ", e.getMessage()); + } + } + @Test void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { @@ -30,6 +45,32 @@ void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { } } + @Test + void test_parsePlantUML_WithViewKey() { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + workspace.getModel().addSoftwareSystem("A"); + workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements(); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + + parser = new ImageViewContentParser(true); + parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape")); + assertEquals("https://plantuml.com/plantuml/svg/HP2nJiD038RtUmghF00f6oYD6f2OO2eI0p2Od9EUUh4Zdwkq8DwTkrB0bZpylsL_zZePgkt7w18P99fGqKI1XSbPi4YmEIQZ4HwGVUfm8kTC9Z21Tp6J4NnGwYm8EvTsWSk44JuT0AhAV2zic_11iAoovAd7VRGdEbWRmy0ZiK6N2sbsPyNfENZRmbLLkaSyF59AED1vGkM-dDi6Jv2HbCIE1UT_Qm517YBLTTiq9uXRx7Q3ofxzdSHys8K_HNOAsLchJb6wHJtfMRt6abbDM_Go1nwWnvYeGFnjWiLgrRvodJBXpR9gNZRIsupw-xUt-h9OpG9-c311wzoQsEUdVmC0", imageView.getContent()); + } + + @Test + void test_parseMermaid_ThrowsAnException_WithTooFewTokens() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + parser = new ImageViewContentParser(true); + parser.parseMermaid(context, null, tokens("plantuml")); + fail(); + } catch (Exception e) { + assertEquals("Expected: mermaid ", e.getMessage()); + } + } + @Test void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { @@ -43,6 +84,19 @@ void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { } } + @Test + void test_parseMermaid_WithViewKey() { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + workspace.getModel().addSoftwareSystem("A"); + workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements(); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + + parser = new ImageViewContentParser(true); + parser.parseMermaid(context, null, tokens("mermaid", "SystemLandscape")); + assertEquals("https://mermaid.ink/svg/pako:eJxlkMtuwjAQRX9lNAhlE9SwqupCpLLuLt0RFiYeJxZ-RLYppYh_bxJHVR93NrM4c3U0N8DGCUKGred9B2-72gJoZU9VvGoCQZKfdQSptGYLOaW2IxPOx3QiFB8WA_saq2uIZOCVWxEa3lONhxEd4FQ2kz_L8hC9O9HvboD10LYR6j1dbjPpbFxdSLVdZHB0WmTly-ZhAMp_VFCfxOCxWD6D4b5VdhVdz6DoP7JyXzkZL9wTJNVD6vjjuZ4NxZRvwyc-Tt447TxbFFOSL1mBOaAhb7gSyG4YOzLjV-f_4f3-BQMfekI=", imageView.getContent()); + } + @Test void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java index b06c62f5a..56cef1709 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -80,6 +80,26 @@ public final Collection export(Workspace workspace) { return diagrams; } + public Diagram export(ModelView view) { + if (view instanceof SystemLandscapeView) { + return export((SystemLandscapeView)view); + } else if (view instanceof SystemContextView) { + return export((SystemContextView)view); + } else if (view instanceof ContainerView) { + return export((ContainerView)view); + } else if (view instanceof ComponentView) { + return export((ComponentView)view); + } else if (view instanceof DynamicView) { + return export((DynamicView)view); + } else if (view instanceof DeploymentView) { + return export((DeploymentView)view); + } else if (view instanceof CustomView) { + return export((CustomView)view); + } else { + throw new RuntimeException(view.getClass().getName() + " is not supported"); + } + } + public Diagram export(CustomView view) { Diagram diagram = export(view, null); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java index 867cf0bb2..e13f848ee 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java @@ -13,8 +13,9 @@ public class PlantUMLEncoderTests { @Test public void encode() throws Exception { File file = new File("./src/test/resources/diagrams/plantuml/with-title.puml"); - String mermaid = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); - assertEquals("SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", new PlantUMLEncoder().encode(mermaid)); + String plantuml = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + String encodedPlantuml = new PlantUMLEncoder().encode(plantuml); + assertEquals("SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", encodedPlantuml); } } \ No newline at end of file From b507b5d7d38610a0a268d6326fe7c5f72b62646b Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sun, 6 Oct 2024 14:46:43 +0100 Subject: [PATCH 298/418] structurizr-dsl: Adds support for `url`, `properties`, and `perspectives` nested inside `!elements` and `!relationships`. --- changelog.md | 1 + .../structurizr/dsl/ElementsDslContext.java | 9 ++- .../com/structurizr/dsl/ModelItemParser.java | 26 ------- .../dsl/ModelItemPerspectivesDslContext.java | 22 ------ .../com/structurizr/dsl/ModelItemsParser.java | 17 +++++ .../structurizr/dsl/PerspectiveParser.java | 35 +++++++++ .../dsl/PerspectivesDslContext.java | 29 ++++++++ .../structurizr/dsl/PropertiesDslContext.java | 15 ++-- .../com/structurizr/dsl/PropertyParser.java | 6 +- .../dsl/RelationshipsDslContext.java | 8 ++- .../structurizr/dsl/StructurizrDslParser.java | 16 ++++- .../structurizr/dsl/ModelItemParserTests.java | 58 --------------- .../dsl/PerspectiveParserTests.java | 72 +++++++++++++++++++ .../src/test/resources/dsl/test.dsl | 22 +++++- 14 files changed, 219 insertions(+), 117 deletions(-) delete mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/PerspectiveParserTests.java diff --git a/changelog.md b/changelog.md index fee0fefc8..96cdedfee 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ - structurizr-dsl: Anonymous identifiers for relationships (i.e. relationships not assigned to an identifier) are excluded from the model, and therefore also excluded from the serialised JSON. - structurizr-dsl: Adds a way to configure whether the DSL source is retained via a workspace property named `structurizr.dsl.source` - `true` (default) or `false`. - structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view. +- structurizr-dsl: Adds support for `url`, `properties`, and `perspectives` nested inside `!elements` and `!relationships`. ## 3.0.0 (19th September 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java index d0aec535b..7e056c159 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java @@ -27,7 +27,14 @@ Set getModelItems() { @Override protected String[] getPermittedTokens() { - return new String[0]; + return new String[] { + StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java index f98cb241d..e4c48ac90 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java @@ -8,10 +8,6 @@ final class ModelItemParser extends AbstractParser { private final static int URL_INDEX = 1; - private final static int PERSPECTIVE_NAME_INDEX = 0; - private final static int PERSPECTIVE_DESCRIPTION_INDEX = 1; - private final static int PERSPECTIVE_VALUE_INDEX = 2; - void parseTags(ModelItemDslContext context, Tokens tokens) { // tags [tags] if (!tokens.includes(TAGS_INDEX)) { @@ -52,26 +48,4 @@ void parseUrl(ModelItemDslContext context, Tokens tokens) { context.getModelItem().setUrl(url); } - void parsePerspective(ModelItemPerspectivesDslContext context, Tokens tokens) { - // [value] - - if (tokens.hasMoreThan(PERSPECTIVE_VALUE_INDEX)) { - throw new RuntimeException("Too many tokens, expected: [value]"); - } - - if (!tokens.includes(PERSPECTIVE_DESCRIPTION_INDEX)) { - throw new RuntimeException("Expected: [value]"); - } - - String name = tokens.get(PERSPECTIVE_NAME_INDEX); - String description = tokens.get(PERSPECTIVE_DESCRIPTION_INDEX); - String value = ""; - - if (tokens.includes(PERSPECTIVE_VALUE_INDEX)) { - value = tokens.get(PERSPECTIVE_VALUE_INDEX); - } - - context.getModelItem().addPerspective(name, description, value); - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java deleted file mode 100644 index 37647a492..000000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.model.ModelItem; - -final class ModelItemPerspectivesDslContext extends DslContext { - - private ModelItem modelItem; - - public ModelItemPerspectivesDslContext(ModelItem modelItem) { - this.modelItem = modelItem; - } - - ModelItem getModelItem() { - return this.modelItem; - } - - @Override - protected String[] getPermittedTokens() { - return new String[0]; - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java index c34ceb33d..2c44be60b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java @@ -5,6 +5,7 @@ final class ModelItemsParser extends AbstractParser { private final static int TAGS_INDEX = 1; + private final static int URL_INDEX = 1; void parseTags(ModelItemsDslContext context, Tokens tokens) { // tags [tags] @@ -21,4 +22,20 @@ void parseTags(ModelItemsDslContext context, Tokens tokens) { } } + void parseUrl(ModelItemsDslContext context, Tokens tokens) { + // url + if (tokens.hasMoreThan(URL_INDEX)) { + throw new RuntimeException("Too many tokens, expected: url "); + } + + if (!tokens.includes(URL_INDEX)) { + throw new RuntimeException("Expected: url "); + } + + String url = tokens.get(URL_INDEX); + for (ModelItem modelItem : context.getModelItems()) { + modelItem.setUrl(url); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java new file mode 100644 index 000000000..3a38ad3ab --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java @@ -0,0 +1,35 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +final class PerspectiveParser extends AbstractParser { + + private final static int PERSPECTIVE_NAME_INDEX = 0; + private final static int PERSPECTIVE_DESCRIPTION_INDEX = 1; + private final static int PERSPECTIVE_VALUE_INDEX = 2; + + void parse(PerspectivesDslContext context, Tokens tokens) { + // [value] + + if (tokens.hasMoreThan(PERSPECTIVE_VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: [value]"); + } + + if (!tokens.includes(PERSPECTIVE_DESCRIPTION_INDEX)) { + throw new RuntimeException("Expected: [value]"); + } + + String name = tokens.get(PERSPECTIVE_NAME_INDEX); + String description = tokens.get(PERSPECTIVE_DESCRIPTION_INDEX); + String value = ""; + + if (tokens.includes(PERSPECTIVE_VALUE_INDEX)) { + value = tokens.get(PERSPECTIVE_VALUE_INDEX); + } + + for (ModelItem modelItem : context.getModelItems()) { + modelItem.addPerspective(name, description, value); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java new file mode 100644 index 000000000..50ff2e6d3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +import java.util.ArrayList; +import java.util.Collection; + +final class PerspectivesDslContext extends DslContext { + + private final Collection modelItems = new ArrayList<>(); + + PerspectivesDslContext(ModelItem modelItem) { + this.modelItems.add(modelItem); + } + + PerspectivesDslContext(Collection modelItems) { + this.modelItems.addAll(modelItems); + } + + Collection getModelItems() { + return this.modelItems; + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java index e1f8184fd..6b31503de 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java @@ -2,16 +2,23 @@ import com.structurizr.PropertyHolder; +import java.util.ArrayList; +import java.util.Collection; + final class PropertiesDslContext extends DslContext { - private PropertyHolder propertyHolder; + private final Collection propertyHolders = new ArrayList<>(); public PropertiesDslContext(PropertyHolder propertyHolder) { - this.propertyHolder = propertyHolder; + this.propertyHolders.add(propertyHolder); + } + + public PropertiesDslContext(Collection propertyHolders) { + this.propertyHolders.addAll(propertyHolders); } - PropertyHolder getPropertyHolder() { - return this.propertyHolder; + Collection getPropertyHolders() { + return this.propertyHolders; } @Override diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java index 2129a2ddd..0a1ba6fb5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java @@ -1,5 +1,7 @@ package com.structurizr.dsl; +import com.structurizr.PropertyHolder; + final class PropertyParser extends AbstractParser { private final static int PROPERTY_NAME_INDEX = 0; @@ -19,7 +21,9 @@ void parse(PropertiesDslContext context, Tokens tokens) { String name = tokens.get(PROPERTY_NAME_INDEX); String value = tokens.get(PROPERTY_VALUE_INDEX); - context.getPropertyHolder().addProperty(name, value); + for (PropertyHolder propertyHolder : context.getPropertyHolders()) { + propertyHolder.addProperty(name, value); + } } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java index 3797be712..9a830de7f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java @@ -26,7 +26,13 @@ Set getModelItems() { @Override protected String[] getPermittedTokens() { - return new String[0]; + return new String[] { + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 50f30b356..9581c6d5e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.PropertyHolder; import com.structurizr.Workspace; import com.structurizr.model.*; import com.structurizr.util.StringUtils; @@ -554,6 +555,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { new ModelItemParser().parseUrl(getContext(ModelItemDslContext.class), tokens); + } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemsDslContext.class)) { + new ModelItemsParser().parseUrl(getContext(ModelItemsDslContext.class), tokens); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { startContext(new PropertiesDslContext(workspace)); @@ -566,6 +570,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { startContext(new PropertiesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemsDslContext.class)) { + startContext(new PropertiesDslContext(getContext(ModelItemsDslContext.class).getModelItems().stream().map(mi -> (PropertyHolder)mi).toList())); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { startContext(new PropertiesDslContext(workspace.getViews().getConfiguration())); @@ -585,10 +592,13 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new PropertyParser().parse(getContext(PropertiesDslContext.class), tokens); } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { - startContext(new ModelItemPerspectivesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + startContext(new PerspectivesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + + } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemsDslContext.class)) { + startContext(new PerspectivesDslContext(getContext(ModelItemsDslContext.class).getModelItems())); - } else if (inContext(ModelItemPerspectivesDslContext.class)) { - new ModelItemParser().parsePerspective(getContext(ModelItemPerspectivesDslContext.class), tokens); + } else if (inContext(PerspectivesDslContext.class)) { + new PerspectiveParser().parse(getContext(PerspectivesDslContext.class), tokens); } else if (WORKSPACE_TOKEN.equalsIgnoreCase(firstToken) && contextStack.empty()) { if (parsedTokens.contains(WORKSPACE_TOKEN)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java index 011826924..fb1091782 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Perspective; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; @@ -105,61 +104,4 @@ void test_parseUrl_SetsTheUrl_WhenAUrlIsSpecified() { assertEquals("http://example.com", softwareSystem.getUrl()); } - @Test - void test_parsePerspective_ThrowsAnException_WhenThereAreTooManyTokens() { - try { - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(null); - parser.parsePerspective(context, tokens("name", "description", "value", "extra")); - fail(); - } catch (Exception e) { - assertEquals("Too many tokens, expected: [value]", e.getMessage()); - } - } - - @Test - void test_parsePerspective_ThrowsAnException_WhenNoNameIsSpecified() { - try { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens()); - fail(); - } catch (Exception e) { - assertEquals("Expected: [value]", e.getMessage()); - } - } - - @Test - void test_parsePerspective_ThrowsAnException_WhenNoDescriptionIsSpecified() { - try { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens("name")); - fail(); - } catch (Exception e) { - assertEquals("Expected: [value]", e.getMessage()); - } - } - - @Test - void test_parsePerspective_AddsThePerspective_WhenADescriptionIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens("Security", "Description")); - - Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); - assertEquals("Description", perspective.getDescription()); - assertEquals("", perspective.getValue()); - } - - @Test - void test_parsePerspective_AddsThePerspective_WhenADescriptionAndValueIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens("Security", "Description", "Value")); - - Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); - assertEquals("Description", perspective.getDescription()); - assertEquals("Value", perspective.getValue()); - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PerspectiveParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PerspectiveParserTests.java new file mode 100644 index 000000000..3f11603dc --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PerspectiveParserTests.java @@ -0,0 +1,72 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Perspective; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class PerspectiveParserTests extends AbstractTests { + + private final PerspectiveParser parser = new PerspectiveParser(); + + @Test + void test_parsePerspective_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + PerspectivesDslContext context = new PerspectivesDslContext((ModelItem)null); + parser.parse(context, tokens("name", "description", "value", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_ThrowsAnException_WhenNoNameIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens("name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_AddsThePerspective_WhenADescriptionIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens("Security", "Description")); + + Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); + assertEquals("Description", perspective.getDescription()); + assertEquals("", perspective.getValue()); + } + + @Test + void test_parsePerspective_AddsThePerspective_WhenADescriptionAndValueIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens("Security", "Description", "Value")); + + Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); + assertEquals("Description", perspective.getDescription()); + assertEquals("Value", perspective.getValue()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 7deeaf866..045aa311d 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -69,7 +69,15 @@ workspace "Name" "Description" { } !elements "element.parent==webApplication && element.technology==Spring MVC Controller" { - tags "Spring MVC Controller" + tag "Tag 1" + tags "Tag 2, Tag 3" + url "https://example.com" + properties { + "type" "Spring MVC Controller" + } + perspectives { + "Owner" "Team A" + } } } @@ -142,6 +150,18 @@ workspace "Name" "Description" { } } + + !relationships "*->*" { + tag "Tag 1" + tags "Tag 2, Tag 3" + url "https://example.com" + properties { + name value + } + perspectives { + name value + } + } } views { From 1e6c2a35fb8202da9cf323157bd3b2f2e485ee4f Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:36:34 +0100 Subject: [PATCH 299/418] Closes #347. --- changelog.md | 1 + .../dsl/DeploymentViewExpressionParser.java | 19 +++++++ .../com/structurizr/dsl/ExpressionParser.java | 2 +- .../DeploymentViewExpressionParserTests.java | 53 +++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 96cdedfee..2975057df 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ - structurizr-dsl: Adds a way to configure whether the DSL source is retained via a workspace property named `structurizr.dsl.source` - `true` (default) or `false`. - structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view. - structurizr-dsl: Adds support for `url`, `properties`, and `perspectives` nested inside `!elements` and `!relationships`. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/347 (`->container->` expression does not work as expected in deployment view). ## 3.0.0 (19th September 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java index 33e9a5b37..ea6266d86 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java @@ -4,6 +4,7 @@ import java.util.LinkedHashSet; import java.util.Set; +import java.util.stream.Collectors; import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; @@ -43,6 +44,24 @@ protected Set evaluateElementTypeExpression(String expr, DslContext con return elements; } + @Override + protected Set getElements(String identifier, DslContext context) { + Set elements = new LinkedHashSet<>(); + for (Element element : super.getElements(identifier, context)) { + if (element instanceof SoftwareSystem) { + Set elementInstances = context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).map(e -> (SoftwareSystemInstance) e).filter(ssi -> ssi.getSoftwareSystem().equals(element)).collect(Collectors.toSet()); + elements.addAll(elementInstances); + } else if (element instanceof Container) { + Set elementInstances = context.getWorkspace().getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getContainer().equals(element)).collect(Collectors.toSet()); + elements.addAll(elementInstances); + } else { + elements.add(element); + } + } + + return elements; + } + protected Set findAfferentCouplings(Element element) { Set elements = new LinkedHashSet<>(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java index 9599c79f1..8ee65bbcf 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java @@ -419,7 +419,7 @@ protected Set parseIdentifier(String identifier, DslContext context) } } - private Set getElements(String identifier, DslContext context) { + protected Set getElements(String identifier, DslContext context) { Set elements = new HashSet<>(); Element element = context.getElement(identifier); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java index bf4a417a0..95eef0d04 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java @@ -217,4 +217,57 @@ void test_parseExpression_ReturnsElements_WhenBooleanOrUsed() { assertTrue(elements.contains(containerInstance)); } + @Test + void test_parseExpression_ReturnsSoftwareSystemInstanceDependencies_WhenASoftwareSystemExpressionIsUsed() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + a.uses(b, ""); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstanceA = deploymentNode.add(a); + SoftwareSystemInstance softwareSystemInstanceB = deploymentNode.add(b); + infrastructureNode.uses(softwareSystemInstanceA, "", ""); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister identifiersRegister = new IdentifiersRegister(); + identifiersRegister.register("a", a); + context.setIdentifierRegister(identifiersRegister); + + Set elements = parser.parseExpression("->a->", context); + assertEquals(3, elements.size()); + assertTrue(elements.contains(infrastructureNode)); + assertTrue(elements.contains(softwareSystemInstanceA)); + assertTrue(elements.contains(softwareSystemInstanceB)); + } + + @Test + void test_parseExpression_ReturnsContainerInstanceDependencies_WhenAContainerExpressionIsUsed() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container1 = softwareSystem.addContainer("Container 1"); + Container container2 = softwareSystem.addContainer("Container 2"); + container1.uses(container2, ""); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + ContainerInstance containerInstance1 = deploymentNode.add(container1); + ContainerInstance containerInstance2 = deploymentNode.add(container2); + infrastructureNode.uses(containerInstance1, "", ""); + + DeploymentViewDslContext context = new DeploymentViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister identifiersRegister = new IdentifiersRegister(); + identifiersRegister.register("c1", container1); + context.setIdentifierRegister(identifiersRegister); + + Set elements = parser.parseExpression("->c1->", context); + assertEquals(3, elements.size()); + assertTrue(elements.contains(infrastructureNode)); + assertTrue(elements.contains(containerInstance1)); + assertTrue(elements.contains(containerInstance2)); + } + } \ No newline at end of file From cb8a407ba82626c4dc4fe6152b07161e02a3d2dc Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:13:50 +0200 Subject: [PATCH 300/418] WorkspaceAPIClient: Allows "main" to be used to refer to the main/default branch. --- .../src/main/java/com/structurizr/api/WorkspaceApiClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java index 1ab4a7158..3b7ce5c95 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -39,6 +39,7 @@ public class WorkspaceApiClient extends AbstractApiClient { private static final Log log = LogFactory.getLog(WorkspaceApiClient.class); + private static final String MAIN_BRANCH = "main"; private String user; @@ -234,7 +235,7 @@ public Workspace getWorkspace(long workspaceId) throws StructurizrClientExceptio log.info("Getting workspace with ID " + workspaceId); HttpGet httpGet; - if (StringUtils.isNullOrEmpty(branch)) { + if (StringUtils.isNullOrEmpty(branch) || branch.equalsIgnoreCase(MAIN_BRANCH)) { httpGet = new HttpGet(url + WORKSPACE_PATH + "/" + workspaceId); } else { httpGet = new HttpGet(url + WORKSPACE_PATH + "/" + workspaceId + "/branch/" + branch); From f3638363d7eba76f3784691d74e8e3d744743020 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:26:09 +0100 Subject: [PATCH 301/418] Adds support for `!elements group` (https://github.com/structurizr/java/issues/351). --- changelog.md | 1 + .../com/structurizr/dsl/ExpressionParser.java | 6 ++++ .../java/com/structurizr/dsl/DslTests.java | 29 +++++++++++++++- .../dsl/find-elements-in-flat-group.dsl | 17 ++++++++++ .../dsl/find-elements-in-nested-group.dsl | 34 +++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/find-elements-in-flat-group.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/find-elements-in-nested-group.dsl diff --git a/changelog.md b/changelog.md index 2975057df..29cc8c172 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ - structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view. - structurizr-dsl: Adds support for `url`, `properties`, and `perspectives` nested inside `!elements` and `!relationships`. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/347 (`->container->` expression does not work as expected in deployment view). +- structurizr-dsl: Adds support for `!elements group` (https://github.com/structurizr/java/issues/351). ## 3.0.0 (19th September 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java index 8ee65bbcf..cac370945 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java @@ -274,6 +274,12 @@ private Set evaluateExpression(String expr, DslContext context) { modelItems.add(relationship); } }); + } else { + // fallback that the expression is an identifier + Set elements = getElements(expr, context); + if (!elements.isEmpty()) { + modelItems.addAll(elements); + } } return modelItems; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 0c73de740..ec37f2afd 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -531,7 +531,7 @@ void test_findElement() throws Exception { } @Test - void test_findElement_Hierachical() throws Exception { + void test_findElement_Hierarchical() throws Exception { File dslFile = new File("src/test/resources/dsl/find-element-hierarchical.dsl"); StructurizrDslParser parser = new StructurizrDslParser(); @@ -543,6 +543,33 @@ void test_findElement_Hierachical() throws Exception { assertEquals("Value3", component.getProperties().get("Name3")); } + @Test + void test_findElements_InFlatGroup() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/find-elements-in-flat-group.dsl")); + + Person user = parser.getWorkspace().getModel().getPersonWithName("User"); + assertTrue(user.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("A"), "Uses")); + assertTrue(user.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("B"), "Uses")); + assertTrue(user.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("C"), "Uses")); + } + + @Test + void test_findElements_InNestedGroup() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/find-elements-in-nested-group.dsl")); + + Person user1 = parser.getWorkspace().getModel().getPersonWithName("User 1"); + assertTrue(user1.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("A"), "Uses")); + assertTrue(user1.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("B"), "Uses")); + assertTrue(user1.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("C"), "Uses")); + + Person user2 = parser.getWorkspace().getModel().getPersonWithName("User 2"); + assertTrue(user2.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("A"), "Uses")); + assertFalse(user2.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("B"), "Uses")); + assertFalse(user2.hasEfferentRelationshipWith(parser.getWorkspace().getModel().getSoftwareSystemWithName("C"), "Uses")); + } + @Test void test_parallel1() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); diff --git a/structurizr-dsl/src/test/resources/dsl/find-elements-in-flat-group.dsl b/structurizr-dsl/src/test/resources/dsl/find-elements-in-flat-group.dsl new file mode 100644 index 000000000..a64eccbc5 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/find-elements-in-flat-group.dsl @@ -0,0 +1,17 @@ +workspace { + + model { + user = person "User" + + group = group "Group" { + softwareSystem "A" + softwareSystem "B" + softwareSystem "C" + } + + !elements group { + user -> this "Uses" + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/find-elements-in-nested-group.dsl b/structurizr-dsl/src/test/resources/dsl/find-elements-in-nested-group.dsl new file mode 100644 index 000000000..daae8b271 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/find-elements-in-nested-group.dsl @@ -0,0 +1,34 @@ +workspace { + + model { + properties { + "structurizr.groupSeparator" "/" + } + + user1 = person "User 1" + user2 = person "User 2" + + department1 = group "Department 1" { + team1 = group "Team 1" { + softwareSystem "A" + } + + team2 = group "Team 2" { + softwareSystem "B" + } + + team3 = group "Team 3" { + softwareSystem "C" + } + } + + !elements department1 { + user1 -> this "Uses" + } + + !elements team1 { + user2 -> this "Uses" + } + } + +} \ No newline at end of file From a3aa43439f0ce68fdc350ff9ea1e514ab2d66a2d Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:32:34 +0000 Subject: [PATCH 302/418] Updated for release. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 29cc8c172..62e8b3181 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 3.1.0 (unreleased) +## 3.1.0 (4th November 2024) - structurizr-client: Workspace archive file now includes the branch name in the filename. - structurizr-component: Adds `ImplementationWithPrefixSupportingTypesStrategy`. From 0351c2dce30dd1d98d2ef4dc438db747bf646dea Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:41:14 +0000 Subject: [PATCH 303/418] Bumps dependencies. --- structurizr-autolayout/build.gradle | 2 +- structurizr-client/build.gradle | 6 +++--- structurizr-component/build.gradle | 2 +- structurizr-core/build.gradle | 4 ++-- structurizr-dsl/build.gradle | 4 ++-- structurizr-export/build.gradle | 2 +- structurizr-import/build.gradle | 2 +- structurizr-inspection/build.gradle | 2 +- structurizr-neo4j/build.gradle | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/structurizr-autolayout/build.gradle b/structurizr-autolayout/build.gradle index 19510a149..631c38fcc 100644 --- a/structurizr-autolayout/build.gradle +++ b/structurizr-autolayout/build.gradle @@ -3,7 +3,7 @@ dependencies { api project(':structurizr-export') testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index 32f0fb02b..a67e1adf7 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,11 +2,11 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.17.2' - api 'org.apache.httpcomponents.client5:httpclient5:5.4' + api 'com.fasterxml.jackson.core:jackson-databind:2.18.1' + api 'org.apache.httpcomponents.client5:httpclient5:5.4.1' api 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle index 538c74020..3c365fbb5 100644 --- a/structurizr-component/build.gradle +++ b/structurizr-component/build.gradle @@ -5,7 +5,7 @@ dependencies { implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.2' testImplementation project(':structurizr-annotation') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 80ccbf6dd..d188aa2be 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,10 +1,10 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.17.2' + api 'com.fasterxml.jackson.core:jackson-annotations:2.18.1' api 'com.google.code.findbugs:jsr305:3.0.2' api 'commons-logging:commons-logging:1.3.4' - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' testImplementation 'org.assertj:assertj-core:3.26.3' } \ No newline at end of file diff --git a/structurizr-dsl/build.gradle b/structurizr-dsl/build.gradle index b5d1a6a55..b452a6366 100644 --- a/structurizr-dsl/build.gradle +++ b/structurizr-dsl/build.gradle @@ -9,8 +9,8 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.25' testImplementation 'org.jruby:jruby-core:9.4.8.0' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.0' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.3' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.3' } description = 'Structurizr DSL' \ No newline at end of file diff --git a/structurizr-export/build.gradle b/structurizr-export/build.gradle index 67a1a4e33..ddef771b2 100644 --- a/structurizr-export/build.gradle +++ b/structurizr-export/build.gradle @@ -3,7 +3,7 @@ dependencies { api project(':structurizr-core') testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-import/build.gradle b/structurizr-import/build.gradle index a1dd4218c..eeb4cdc43 100644 --- a/structurizr-import/build.gradle +++ b/structurizr-import/build.gradle @@ -2,7 +2,7 @@ dependencies { api project(':structurizr-core') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-inspection/build.gradle b/structurizr-inspection/build.gradle index 25ca05b8e..bf3f60229 100644 --- a/structurizr-inspection/build.gradle +++ b/structurizr-inspection/build.gradle @@ -3,6 +3,6 @@ dependencies { api project(':structurizr-core') testImplementation project(':structurizr-dsl') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } \ No newline at end of file diff --git a/structurizr-neo4j/build.gradle b/structurizr-neo4j/build.gradle index a8990a997..f6518e3c2 100644 --- a/structurizr-neo4j/build.gradle +++ b/structurizr-neo4j/build.gradle @@ -1,9 +1,9 @@ dependencies { api project(':structurizr-core') - implementation 'org.neo4j.driver:neo4j-java-driver:5.24.0' + implementation 'org.neo4j.driver:neo4j-java-driver:5.26.1' testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } \ No newline at end of file From b26d5b1abf02c44f6a7622848c7f09454301bb36 Mon Sep 17 00:00:00 2001 From: Maksim Beliaev Date: Mon, 18 Nov 2024 17:25:31 +0100 Subject: [PATCH 304/418] Update README.md for plantuml exporter Removed the referenced repository that is archived and deprecated. --- .../src/main/java/com/structurizr/export/plantuml/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md b/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md index 616e11ca9..8474c6296 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md @@ -2,6 +2,4 @@ There are two PlantUML exporters in this package - [StructurizrPlantUMLExporter](StructurizrPlantUMLExporter.java) and [C4PlantUMLExporter](C4PlantUMLExporter.java). -If neither of these provide the features you are looking for, an alternative PlantUML exporter can be found at [https://github.com/cloudflightio/structurizr-export-c4plantuml](https://github.com/cloudflightio/structurizr-export-c4plantuml). - -See https://docs.structurizr.com/export for more. \ No newline at end of file +See https://docs.structurizr.com/export for more. From 69b7d399ae8d3c57bcc88a948baeef535cd062db Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 23 Nov 2024 09:35:29 +0000 Subject: [PATCH 305/418] structurizr-dsl: Adds support for `element!=` expressions. --- build.gradle | 2 +- changelog.md | 4 ++ .../com/structurizr/dsl/ExpressionParser.java | 47 +++++++++++++------ .../dsl/StructurizrDslExpressions.java | 1 + .../dsl/ExpressionParserTests.java | 43 +++++++++++++++++ 5 files changed, 82 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 21ae1b226..96d8f743e 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '3.1.0' + version = '3.2.0' repositories { mavenCentral() diff --git a/changelog.md b/changelog.md index 62e8b3181..3ac74faf8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 3.2.0 (unreleased) + +- structurizr-dsl: Adds support for `element!=` expressions. + ## 3.1.0 (4th November 2024) - structurizr-client: Workspace archive file now includes the branch name in the filename. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java index cac370945..31c39eb98 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java @@ -18,21 +18,22 @@ static boolean isExpression(String token) { token = token.toLowerCase(); return + token.startsWith(ELEMENT_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_NOT_EQUALS_EXPRESSION.toLowerCase()) || token.startsWith(ELEMENT_TYPE_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(ELEMENT_TAG_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(ELEMENT_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(ELEMENT_TECHNOLOGY_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(ELEMENT_TECHNOLOGY_NOT_EQUALS_EXPRESSION.toLowerCase()) || - token.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION) || - token.startsWith(ELEMENT_PARENT_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(RELATIONSHIP) || token.endsWith(RELATIONSHIP) || token.contains(RELATIONSHIP) || - token.startsWith(ELEMENT_EQUALS_EXPRESSION) || - token.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || - token.matches(RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION) || - token.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION.toLowerCase()) || - token.startsWith(RELATIONSHIP_EQUALS_EXPRESSION); + token.startsWith(ELEMENT_TAG_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_TECHNOLOGY_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(ELEMENT_TECHNOLOGY_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.matches(ELEMENT_PROPERTY_EQUALS_EXPRESSION) || + token.startsWith(ELEMENT_PARENT_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP) || token.endsWith(RELATIONSHIP) || token.contains(RELATIONSHIP) || + token.startsWith(RELATIONSHIP_TAG_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP_TAG_NOT_EQUALS_EXPRESSION.toLowerCase()) || + token.matches(RELATIONSHIP_PROPERTY_EQUALS_EXPRESSION) || + token.startsWith(RELATIONSHIP_SOURCE_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP_DESTINATION_EQUALS_EXPRESSION.toLowerCase()) || + token.startsWith(RELATIONSHIP_EQUALS_EXPRESSION); } @@ -71,6 +72,24 @@ private Set evaluateExpression(String expr, DslContext context) { } else { modelItems.addAll(parseIdentifier(expr, context)); } + } else if (expr.startsWith(ELEMENT_NOT_EQUALS_EXPRESSION)) { + expr = expr.substring(ELEMENT_NOT_EQUALS_EXPRESSION.length()); + + if (isExpression(expr)) { + Set mi = evaluateExpression(expr, context); + context.getWorkspace().getModel().getElements().forEach(element -> { + if (!mi.contains(element)) { + modelItems.add(element); + } + }); + } else { + Set mi = parseIdentifier(expr, context); + context.getWorkspace().getModel().getElements().forEach(element -> { + if (!mi.contains(element)) { + modelItems.add(element); + } + }); + } } else if (expr.startsWith(RELATIONSHIP_EQUALS_EXPRESSION)) { expr = expr.substring(RELATIONSHIP_EQUALS_EXPRESSION.length()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java index a10aa0924..ba9ae26fb 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java @@ -10,6 +10,7 @@ class StructurizrDslExpressions { static final String ELEMENT_PROPERTY_EQUALS_EXPRESSION = "element\\.properties\\[.*]==.*"; static final String ELEMENT_EQUALS_EXPRESSION = "element=="; + static final String ELEMENT_NOT_EQUALS_EXPRESSION = "element!="; static final String ELEMENT_PARENT_EQUALS_EXPRESSION = "element.parent=="; static final String RELATIONSHIP_TAG_EQUALS_EXPRESSION = "relationship.tag=="; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java index fcf91a830..83bb4a3ab 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java @@ -541,4 +541,47 @@ void test_parseExpression_ReturnsRelationshipsAndImpliedRelationships_WhenUsingA assertTrue(relationships.contains(impliedRelationship)); } + @Test + void test_parseExpression_ReturnsElements_WhenUsingElementNotEqualsIdentifier() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("a", a); + map.register("b", b); + map.register("c", c); + context.setIdentifierRegister(map); + + Set elements = parser.parseExpression("element!=c", context); + assertEquals(2, elements.size()); + assertTrue(elements.contains(a)); + assertTrue(elements.contains(b)); + } + + @Test + void test_parseExpression_ReturnsElements_WhenUsingElementNotEqualsExpression() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Relationship aToB = a.uses(b, "Uses"); + Relationship bToC = b.uses(c, "Uses"); + + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(null); + context.setWorkspace(workspace); + + IdentifiersRegister map = new IdentifiersRegister(); + map.register("a", a); + map.register("b", b); + map.register("c", c); + context.setIdentifierRegister(map); + + Set elements = parser.parseExpression("element!=->b", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(c)); + } + } \ No newline at end of file From 5cc29d00bb8aa1801afb0b77efbd011efc26d378 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 23 Nov 2024 10:01:53 +0000 Subject: [PATCH 306/418] structurizr-dsl: `!elements` and `!relationships` now work inside deployment environment blocks. --- changelog.md | 1 + .../main/java/com/structurizr/dsl/StructurizrDslParser.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 3ac74faf8..03139f0aa 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## 3.2.0 (unreleased) - structurizr-dsl: Adds support for `element!=` expressions. +- structurizr-dsl: `!elements` and `!relationships` now work inside deployment environment blocks. ## 3.1.0 (4th November 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 9581c6d5e..9f6d4af23 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -383,14 +383,14 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } } - } else if (FIND_ELEMENTS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { + } else if (FIND_ELEMENTS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) { Set elements = new FindElementsParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { startContext(new ElementsDslContext(getContext(), elements)); } - } else if (FIND_RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(ElementDslContext.class))) { + } else if (FIND_RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) { Set relationships = new FindRelationshipsParser().parse(getContext(), tokens.withoutContextStartToken()); if (shouldStartContext(tokens)) { From d95879f292b42a858717b73f94a2804fd2ea4349 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sun, 24 Nov 2024 13:02:01 +0000 Subject: [PATCH 307/418] Make version overridable from command line. --- build.gradle | 1 - gradle.properties | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 96d8f743e..536ce612a 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,6 @@ subprojects { proj -> description = 'Structurizr' group = 'com.structurizr' - version = '3.2.0' repositories { mavenCentral() diff --git a/gradle.properties b/gradle.properties index fe55ae08c..8af3d8f3b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,3 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password +version=3.2.0 \ No newline at end of file From b6fd8cdb3e2dabddb2206409c90c5f63dfc647ab Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 30 Nov 2024 15:21:17 +0000 Subject: [PATCH 308/418] structurizr-dsl: `description` and `technology` now work inside `!elements` blocks. --- changelog.md | 1 + .../structurizr/dsl/ElementsDslContext.java | 2 + .../com/structurizr/dsl/ElementsParser.java | 50 +++++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 6 ++ .../structurizr/dsl/ElementsParserTests.java | 70 +++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java diff --git a/changelog.md b/changelog.md index 03139f0aa..cd6faebbf 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - structurizr-dsl: Adds support for `element!=` expressions. - structurizr-dsl: `!elements` and `!relationships` now work inside deployment environment blocks. +- structurizr-dsl: `description` and `technology` now work inside `!elements` blocks. ## 3.1.0 (4th November 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java index 7e056c159..3ef8706e3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java @@ -29,6 +29,8 @@ Set getModelItems() { protected String[] getPermittedTokens() { return new String[] { StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, StructurizrDslTokens.URL_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java new file mode 100644 index 000000000..b2862307e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsParser.java @@ -0,0 +1,50 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +final class ElementsParser extends AbstractParser { + + private final static int DESCRIPTION_INDEX = 1; + private final static int TECHNOLOGY_INDEX = 1; + + void parseDescription(ElementsDslContext context, Tokens tokens) { + // description + if (tokens.hasMoreThan(DESCRIPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: description "); + } + + if (!tokens.includes(DESCRIPTION_INDEX)) { + throw new RuntimeException("Expected: description "); + } + + String description = tokens.get(DESCRIPTION_INDEX); + for (Element element : context.getElements()) { + element.setDescription(description); + } + } + + void parseTechnology(ElementsDslContext context, Tokens tokens) { + // technology + if (tokens.hasMoreThan(TECHNOLOGY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: technology "); + } + + if (!tokens.includes(TECHNOLOGY_INDEX)) { + throw new RuntimeException("Expected: technology "); + } + + String technology = tokens.get(TECHNOLOGY_INDEX); + for (Element element : context.getElements()) { + if (element instanceof Container) { + ((Container)element).setTechnology(technology); + } else if (element instanceof Component) { + ((Component)element).setTechnology(technology); + } else if (element instanceof DeploymentNode) { + ((DeploymentNode)element).setTechnology(technology); + } else if (element instanceof InfrastructureNode) { + ((InfrastructureNode)element).setTechnology(technology); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 9f6d4af23..f0faf6f4f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -537,6 +537,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementDslContext.class) && !isGroup(getContext())) { new ModelItemParser().parseDescription(getContext(ElementDslContext.class), tokens); + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementsDslContext.class)) { + new ElementsParser().parseDescription(getContext(ElementsDslContext.class), tokens); + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class) && !getContext(ContainerDslContext.class).hasGroup()) { new ContainerParser().parseTechnology(getContext(ContainerDslContext.class), tokens); @@ -549,6 +552,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(InfrastructureNodeDslContext.class)) { new InfrastructureNodeParser().parseTechnology(getContext(InfrastructureNodeDslContext.class), tokens); + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementsDslContext.class)) { + new ElementsParser().parseTechnology(getContext(ElementsDslContext.class), tokens); + } else if (INSTANCES_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { new DeploymentNodeParser().parseInstances(getContext(DeploymentNodeDslContext.class), tokens); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java new file mode 100644 index 000000000..1c1836204 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementsParserTests.java @@ -0,0 +1,70 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ElementsParserTests extends AbstractTests { + + private final ElementsParser parser = new ElementsParser(); + + @Test + void test_parseTechnology_ThrowsAnException_WhenNoTechnologyIsSpecified() { + try { + parser.parseTechnology(null, tokens("technology")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology ", e.getMessage()); + } + } + + @Test + void test_parseTechnology() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + + ElementsDslContext context = new ElementsDslContext(null, Set.of(softwareSystem, container, component, deploymentNode, infrastructureNode)); + + parser.parseTechnology(context, tokens("technology", "Technology")); + assertEquals("Technology", container.getTechnology()); + assertEquals("Technology", component.getTechnology()); + assertEquals("Technology", deploymentNode.getTechnology()); + assertEquals("Technology", infrastructureNode.getTechnology()); + } + + @Test + void test_parseDescription_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + parser.parseDescription(null, tokens("description")); + fail(); + } catch (Exception e) { + assertEquals("Expected: description ", e.getMessage()); + } + } + + @Test + void test_parseDescription() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + + ElementsDslContext context = new ElementsDslContext(null, Set.of(softwareSystem, container, component, deploymentNode, infrastructureNode)); + + parser.parseDescription(context, tokens("description", "Description")); + assertEquals("Description", softwareSystem.getDescription()); + assertEquals("Description", container.getDescription()); + assertEquals("Description", component.getDescription()); + assertEquals("Description", deploymentNode.getDescription()); + assertEquals("Description", infrastructureNode.getDescription()); + } + +} \ No newline at end of file From a734e984457478277358d7ce80f3f534cbb54a22 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:36:22 +0000 Subject: [PATCH 309/418] Ensures restricted mode setting is propagated to spawned parsers. --- .../src/main/java/com/structurizr/dsl/DslParserContext.java | 4 ++-- .../src/main/java/com/structurizr/dsl/WorkspaceParser.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java index f7f7491d8..7fd4eea7b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java @@ -4,8 +4,8 @@ final class DslParserContext extends DslContext { - private boolean restricted; - private File file; + private final boolean restricted; + private final File file; DslParserContext(File file, boolean restricted) { this.file = file; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java index 7778007a2..44f11b2c7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java @@ -46,6 +46,7 @@ Workspace parse(DslParserContext context, Tokens tokens) { } else { String dsl = content.getContent(); StructurizrDslParser structurizrDslParser = new StructurizrDslParser(); + structurizrDslParser.setRestricted(context.isRestricted()); structurizrDslParser.parse(context, dsl); workspace = structurizrDslParser.getWorkspace(); } From b51c9a7c3f3709de14bcd45e795945601cb99281 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:45:14 +0000 Subject: [PATCH 310/418] Updated to reflect release. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index cd6faebbf..a949f37e1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 3.2.0 (unreleased) +## 3.2.0 (6th December 2024) - structurizr-dsl: Adds support for `element!=` expressions. - structurizr-dsl: `!elements` and `!relationships` now work inside deployment environment blocks. From 0ef815a404a9ff3f3a87750709bdedce17fc9989 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:53:43 +0000 Subject: [PATCH 311/418] Bump dependency. --- structurizr-neo4j/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-neo4j/build.gradle b/structurizr-neo4j/build.gradle index f6518e3c2..aa2bcf209 100644 --- a/structurizr-neo4j/build.gradle +++ b/structurizr-neo4j/build.gradle @@ -1,7 +1,7 @@ dependencies { api project(':structurizr-core') - implementation 'org.neo4j.driver:neo4j-java-driver:5.26.1' + implementation 'org.neo4j.driver:neo4j-java-driver:5.27.0' testImplementation project(':structurizr-client') testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' From 1d6cda2334252d2f6b0aaf0fa5b9d9564c443b10 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:24:57 +0000 Subject: [PATCH 312/418] Fixes #362. --- changelog.md | 4 ++++ gradle.properties | 2 +- .../src/main/java/com/structurizr/model/Model.java | 7 +++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index a949f37e1..e3a3806d7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 3.2.1 (10th December 2024) + +- structurizr-core: Fixes https://github.com/structurizr/java/issues/362 (Ordering of replicated relationships in deployment environment is non-deterministic). + ## 3.2.0 (6th December 2024) - structurizr-dsl: Adds support for `element!=` expressions. diff --git a/gradle.properties b/gradle.properties index 8af3d8f3b..c1670d1fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=3.2.0 \ No newline at end of file +version=3.2.1 \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 111d124c1..57d947b2d 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -902,12 +902,11 @@ private void replicateElementRelationships(StaticStructureElementInstance elemen StaticStructureElement element = elementInstance.getElement(); // find all StaticStructureElementInstance objects in the same deployment environment and deployment group - Set elementInstances = getElements().stream() + TreeSet elementInstances = getElements().stream() .filter(e -> e instanceof StaticStructureElementInstance) - .map(e -> (StaticStructureElementInstance)e) + .map(e -> (StaticStructureElementInstance) e) .filter(ssei -> ssei.getEnvironment().equals(elementInstance.getEnvironment())) - .filter(ssei -> ssei.inSameDeploymentGroup(elementInstance)) - .collect(Collectors.toSet()); + .filter(ssei -> ssei.inSameDeploymentGroup(elementInstance)).collect(Collectors.toCollection(TreeSet::new)); // and replicate the relationships to/from the element instance for (StaticStructureElementInstance ssei : elementInstances) { From 98371df012175b8670b586e5c72c04b35d3bda1b Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:28:24 +0000 Subject: [PATCH 313/418] Initial POC re: archetypes. --- gradle.properties | 2 +- .../java/com/structurizr/dsl/Archetype.java | 66 ++++++ .../structurizr/dsl/ArchetypeDslContext.java | 15 ++ .../com/structurizr/dsl/ArchetypeParser.java | 62 +++++ .../structurizr/dsl/ArchetypesDslContext.java | 14 ++ .../dsl/ComponentArchetypeDslContext.java | 19 ++ .../dsl/ContainerArchetypeDslContext.java | 19 ++ .../DeploymentNodeArchetypeDslContext.java | 19 ++ ...InfrastructureNodeArchetypeDslContext.java | 19 ++ .../dsl/PersonArchetypeDslContext.java | 18 ++ .../SoftwareSystemArchetypeDslContext.java | 18 ++ .../structurizr/dsl/StructurizrDslParser.java | 216 ++++++++++++++++-- .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../java/com/structurizr/dsl/DslTests.java | 12 + .../src/test/resources/dsl/archetypes.dsl | 39 ++++ 15 files changed, 525 insertions(+), 14 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypesDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java create mode 100644 structurizr-dsl/src/test/resources/dsl/archetypes.dsl diff --git a/gradle.properties b/gradle.properties index c1670d1fb..3b854bb7b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=3.2.1 \ No newline at end of file +version=4.0.0 \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java new file mode 100644 index 000000000..18ead6aa2 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java @@ -0,0 +1,66 @@ +package com.structurizr.dsl; + +import com.structurizr.util.StringUtils; +import com.structurizr.util.TagUtils; + +import java.util.LinkedHashSet; +import java.util.Set; + +final class Archetype { + + private final String name; + private final String type; + private String description; + private String technology; + private final Set tags = new LinkedHashSet<>(); + + Archetype(String name, String type) { + if (StringUtils.isNullOrEmpty(name)) { + name = type; + } + + this.name = name.toLowerCase(); + this.type = type; + } + + String getName() { + return name; + } + + String getType() { + return type; + } + + String getDescription() { + return description; + } + + void setDescription(String description) { + this.description = description; + } + + String getTechnology() { + return technology; + } + + void setTechnology(String technology) { + this.technology = technology; + } + + void addTags(String... tags) { + if (tags == null) { + return; + } + + for (String tag : tags) { + if (tag != null) { + this.tags.add(tag.trim()); + } + } + } + + Set getTags() { + return new LinkedHashSet<>(tags); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeDslContext.java new file mode 100644 index 000000000..0f1dbb71f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeDslContext.java @@ -0,0 +1,15 @@ +package com.structurizr.dsl; + +abstract class ArchetypeDslContext extends DslContext { + + private final Archetype archetype; + + ArchetypeDslContext(Archetype archetype) { + this.archetype = archetype; + } + + Archetype getArchetype() { + return archetype; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java new file mode 100644 index 000000000..3c78a1ba6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java @@ -0,0 +1,62 @@ +package com.structurizr.dsl; + +final class ArchetypeParser extends AbstractParser { + + private final static int NAME_INDEX = 1; + private final static int VALUE_INDEX = 1; + + void parseTag(ArchetypeDslContext context, Tokens tokens) { + // tag + if (tokens.hasMoreThan(VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: tag "); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: tag "); + } + + String tag = tokens.get(VALUE_INDEX); + context.getArchetype().addTags(tag); + } + + void parseTags(ArchetypeDslContext context, Tokens tokens) { + // tags [tags] + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: tags [tags]"); + } + + for (int i = NAME_INDEX; i < tokens.size(); i++) { + String tags = tokens.get(i); + context.getArchetype().addTags(tags.split(",")); + } + } + + void parseDescription(ArchetypeDslContext context, Tokens tokens) { + // description + if (tokens.hasMoreThan(VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: description "); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: description "); + } + + String description = tokens.get(VALUE_INDEX); + context.getArchetype().setDescription(description); + } + + void parseTechnology(ArchetypeDslContext context, Tokens tokens) { + // technology + if (tokens.hasMoreThan(VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: technology "); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: technology "); + } + + String technology = tokens.get(VALUE_INDEX); + context.getArchetype().setTechnology(technology); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypesDslContext.java new file mode 100644 index 000000000..d457913f7 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypesDslContext.java @@ -0,0 +1,14 @@ +package com.structurizr.dsl; + +final class ArchetypesDslContext extends DslContext { + + ArchetypesDslContext() { + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java new file mode 100644 index 000000000..d361f92e7 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java @@ -0,0 +1,19 @@ +package com.structurizr.dsl; + +final class ComponentArchetypeDslContext extends ArchetypeDslContext { + + ComponentArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java new file mode 100644 index 000000000..3bd97646a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java @@ -0,0 +1,19 @@ +package com.structurizr.dsl; + +final class ContainerArchetypeDslContext extends ArchetypeDslContext { + + ContainerArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java new file mode 100644 index 000000000..895cb9a3d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java @@ -0,0 +1,19 @@ +package com.structurizr.dsl; + +final class DeploymentNodeArchetypeDslContext extends ArchetypeDslContext { + + DeploymentNodeArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java new file mode 100644 index 000000000..b5023744b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java @@ -0,0 +1,19 @@ +package com.structurizr.dsl; + +final class InfrastructureNodeArchetypeDslContext extends ArchetypeDslContext { + + InfrastructureNodeArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java new file mode 100644 index 000000000..a095ff456 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java @@ -0,0 +1,18 @@ +package com.structurizr.dsl; + +final class PersonArchetypeDslContext extends ArchetypeDslContext { + + PersonArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java new file mode 100644 index 000000000..8df3f4f60 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java @@ -0,0 +1,18 @@ +package com.structurizr.dsl; + +final class SoftwareSystemArchetypeDslContext extends ArchetypeDslContext { + + SoftwareSystemArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index f0faf6f4f..32b75a186 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -4,6 +4,7 @@ import com.structurizr.Workspace; import com.structurizr.model.*; import com.structurizr.util.StringUtils; +import com.structurizr.util.TagUtils; import com.structurizr.view.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -45,6 +46,17 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private final IdentifiersRegister identifiersRegister; private final Map constantsAndVariables; + private boolean archetypesEnabled = false; + private final Map> archetypes = Map.of( + StructurizrDslTokens.GROUP_TOKEN, new HashMap<>(), + StructurizrDslTokens.PERSON_TOKEN, new HashMap<>(), + StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, new HashMap<>(), + StructurizrDslTokens.CONTAINER_TOKEN, new HashMap<>(), + StructurizrDslTokens.COMPONENT_TOKEN, new HashMap<>(), + StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, new HashMap<>(), + StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, new HashMap<>() + ); + private final List dslSourceLines = new ArrayList<>(); private Workspace workspace; private boolean extendingWorkspace = false; @@ -218,7 +230,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn Tokens tokens = new Tokens(listOfTokens); String identifier = null; - if (tokens.size() > 3 && ASSIGNMENT_OPERATOR_TOKEN.equals(tokens.get(1))) { + if (tokens.size() >= 3 && ASSIGNMENT_OPERATOR_TOKEN.equals(tokens.get(1))) { identifier = tokens.get(0); identifiersRegister.validateIdentifierName(identifier); @@ -406,8 +418,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, customElement); - } else if (PERSON_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { + } else if (isElementKeywordOrArchetype(firstToken, PERSON_TOKEN) && (inContext(ModelDslContext.class))) { Person person = new PersonParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); + if (archetypesEnabled) { + applyArchetype(firstToken, person); + } if (shouldStartContext(tokens)) { startContext(new PersonDslContext(person)); @@ -415,8 +430,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, person); - } else if (SOFTWARE_SYSTEM_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { + } else if (isElementKeywordOrArchetype(firstToken, SOFTWARE_SYSTEM_TOKEN) && (inContext(ModelDslContext.class))) { SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); + if (archetypesEnabled) { + applyArchetype(firstToken, softwareSystem); + } if (shouldStartContext(tokens)) { startContext(new SoftwareSystemDslContext(softwareSystem)); @@ -424,8 +442,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, softwareSystem); - } else if (CONTAINER_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, CONTAINER_TOKEN) && inContext(SoftwareSystemDslContext.class)) { Container container = new ContainerParser().parse(getContext(SoftwareSystemDslContext.class), tokens.withoutContextStartToken()); + if (archetypesEnabled) { + applyArchetype(firstToken, container); + } if (shouldStartContext(tokens)) { startContext(new ContainerDslContext(container)); @@ -433,8 +454,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, container); - } else if (COMPONENT_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, COMPONENT_TOKEN) && inContext(ContainerDslContext.class)) { Component component = new ComponentParser().parse(getContext(ContainerDslContext.class), tokens.withoutContextStartToken()); + if (archetypesEnabled) { + applyArchetype(firstToken, component); + } if (shouldStartContext(tokens)) { startContext(new ComponentDslContext(component)); @@ -497,32 +521,32 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (ENTERPRISE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { throw new RuntimeException("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group)"); - } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ModelDslContext.class)) { ElementGroup group = new GroupParser().parse(getContext(ModelDslContext.class), tokens); startContext(new ModelDslContext(group)); registerIdentifier(identifier, group); - } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(SoftwareSystemDslContext.class)) { ElementGroup group = new GroupParser().parse(getContext(SoftwareSystemDslContext.class), tokens); SoftwareSystem softwareSystem = getContext(SoftwareSystemDslContext.class).getSoftwareSystem(); group.setParent(softwareSystem); startContext(new SoftwareSystemDslContext(softwareSystem, group)); registerIdentifier(identifier, group); - } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ContainerDslContext.class)) { ElementGroup group = new GroupParser().parse(getContext(ContainerDslContext.class), tokens); Container container = getContext(ContainerDslContext.class).getContainer(); group.setParent(container); startContext(new ContainerDslContext(container, group)); registerIdentifier(identifier, group); - } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentEnvironmentDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { ElementGroup group = new GroupParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens); String environment = getContext(DeploymentEnvironmentDslContext.class).getEnvironment(); startContext(new DeploymentEnvironmentDslContext(environment, group)); registerIdentifier(identifier, group); - } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentNodeDslContext.class)) { ElementGroup group = new GroupParser().parse(getContext(DeploymentNodeDslContext.class), tokens); DeploymentNode deploymentNode = getContext(DeploymentNodeDslContext.class).getDeploymentNode(); @@ -634,6 +658,81 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new ModelDslContext()); parsedTokens.add(MODEL_TOKEN); + } else if (ARCHETYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class) && archetypesEnabled) { + startContext(new ArchetypesDslContext()); + + } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, GROUP_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + } else if (isElementKeywordOrArchetype(firstToken, PERSON_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, PERSON_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + + if (shouldStartContext(tokens)) { + startContext(new PersonArchetypeDslContext(archetype)); + } + + } else if (isElementKeywordOrArchetype(firstToken, SOFTWARE_SYSTEM_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, SOFTWARE_SYSTEM_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + if (shouldStartContext(tokens)) { + startContext(new SoftwareSystemArchetypeDslContext(archetype)); + } + + } else if (isElementKeywordOrArchetype(firstToken, CONTAINER_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, CONTAINER_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + if (shouldStartContext(tokens)) { + startContext(new ContainerArchetypeDslContext(archetype)); + } + + } else if (isElementKeywordOrArchetype(firstToken, COMPONENT_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, COMPONENT_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + if (shouldStartContext(tokens)) { + startContext(new ComponentArchetypeDslContext(archetype)); + } + + } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, DEPLOYMENT_NODE_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + if (shouldStartContext(tokens)) { + startContext(new DeploymentNodeArchetypeDslContext(archetype)); + } + + } else if (isElementKeywordOrArchetype(firstToken, INFRASTRUCTURE_NODE_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, INFRASTRUCTURE_NODE_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + if (shouldStartContext(tokens)) { + startContext(new InfrastructureNodeArchetypeDslContext(archetype)); + } + + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { + new ArchetypeParser().parseDescription(getContext(ArchetypeDslContext.class), tokens); + + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ContainerArchetypeDslContext.class) || inContext(ComponentArchetypeDslContext.class) || inContext(DeploymentNodeArchetypeDslContext.class) || inContext(InfrastructureNodeArchetypeDslContext.class))) { + new ArchetypeParser().parseTechnology(getContext(ArchetypeDslContext.class), tokens); + + } else if (TAG_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { + new ArchetypeParser().parseTag(getContext(ArchetypeDslContext.class), tokens); + + } else if (TAGS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { + new ArchetypeParser().parseTags(getContext(ArchetypeDslContext.class), tokens); + } else if (VIEWS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { if (parsedTokens.contains(VIEWS_TOKEN)) { throw new RuntimeException("Multiple view sets are not permitted in a DSL definition"); @@ -742,24 +841,33 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, new DeploymentGroup(group)); - } else if (DEPLOYMENT_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentEnvironmentDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens.withoutContextStartToken()); + if (archetypesEnabled) { + applyArchetype(firstToken, deploymentNode); + } if (shouldStartContext(tokens)) { startContext(new DeploymentNodeDslContext(deploymentNode)); } registerIdentifier(identifier, deploymentNode); - } else if (DEPLOYMENT_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); + if (archetypesEnabled) { + applyArchetype(firstToken, deploymentNode); + } if (shouldStartContext(tokens)) { startContext(new DeploymentNodeDslContext(deploymentNode)); } registerIdentifier(identifier, deploymentNode); - } else if (INFRASTRUCTURE_NODE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + } else if (isElementKeywordOrArchetype(firstToken, INFRASTRUCTURE_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { InfrastructureNode infrastructureNode = new InfrastructureNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); + if (archetypesEnabled) { + applyArchetype(firstToken, infrastructureNode); + } if (shouldStartContext(tokens)) { startContext(new InfrastructureNodeDslContext(infrastructureNode)); @@ -1184,6 +1292,88 @@ private boolean isGroup(DslContext context) { return false; } + public void setArchetypesEnabled(boolean archetypesEnabled) { + this.archetypesEnabled = archetypesEnabled; + } + + private boolean isElementKeywordOrArchetype(String token, String keyword) { + if (archetypesEnabled) { + if (token.equalsIgnoreCase(keyword)) { + return true; + } else { + return (archetypes.get(keyword).containsKey(token.toLowerCase())); + } + } else { + return token.equalsIgnoreCase(keyword); + } + } + + private void addArchetype(Archetype archetype) { + archetypes.get(archetype.getType()).put(archetype.getName(), archetype); + } + + private void extendArchetype(Archetype archetype, String archetypeName) { + archetypeName = archetypeName.toLowerCase(); + Archetype parentArchetype = archetypes.get(archetype.getType()).get(archetypeName); + if (parentArchetype != null) { + archetype.setDescription(parentArchetype.getDescription()); + archetype.setTechnology(parentArchetype.getTechnology()); + archetype.addTags(parentArchetype.getTags().toArray(new String[0])); + } + } + + private void applyArchetype(String archetypeName, Person person) { + Archetype archetype = archetypes.get(StructurizrDslTokens.PERSON_TOKEN).get(archetypeName); + if (archetype != null) { + person.setDescription(archetype.getDescription()); + person.addTags(archetype.getTags().toArray(new String[0])); + } + } + + private void applyArchetype(String archetypeName, SoftwareSystem softwareSystem) { + Archetype archetype = archetypes.get(StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN).get(archetypeName); + if (archetype != null) { + softwareSystem.setDescription(archetype.getDescription()); + softwareSystem.addTags(archetype.getTags().toArray(new String[0])); + } + } + + private void applyArchetype(String archetypeName, Container container) { + Archetype archetype = archetypes.get(StructurizrDslTokens.CONTAINER_TOKEN).get(archetypeName); + if (archetype != null) { + container.setTechnology(archetype.getTechnology()); + container.setDescription(archetype.getDescription()); + container.addTags(archetype.getTags().toArray(new String[0])); + } + } + + private void applyArchetype(String archetypeName, Component component) { + Archetype archetype = archetypes.get(StructurizrDslTokens.COMPONENT_TOKEN).get(archetypeName); + if (archetype != null) { + component.setTechnology(archetype.getTechnology()); + component.setDescription(archetype.getDescription()); + component.addTags(archetype.getTags().toArray(new String[0])); + } + } + + private void applyArchetype(String archetypeName, DeploymentNode deploymentNode) { + Archetype archetype = archetypes.get(StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN).get(archetypeName); + if (archetype != null) { + deploymentNode.setTechnology(archetype.getTechnology()); + deploymentNode.setDescription(archetype.getDescription()); + deploymentNode.addTags(archetype.getTags().toArray(new String[0])); + } + } + + private void applyArchetype(String archetypeName, InfrastructureNode infrastructureNode) { + Archetype archetype = archetypes.get(StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN).get(archetypeName); + if (archetype != null) { + infrastructureNode.setTechnology(archetype.getTechnology()); + infrastructureNode.setDescription(archetype.getDescription()); + infrastructureNode.addTags(archetype.getTags().toArray(new String[0])); + } + } + /** * Gets the identifier register in use (this is the mapping of DSL identifiers to elements/relationships). * diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index e17ad379a..f44de980b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -27,6 +27,7 @@ class StructurizrDslTokens { static final String EXTENDS_TOKEN = "extends"; static final String SCOPE_TOKEN = "scope"; static final String MODEL_TOKEN = "model"; + static final String ARCHETYPES_TOKEN = "archetypes"; static final String VIEWS_TOKEN = "views"; static final String ENTERPRISE_TOKEN = "enterprise"; static final String DEPLOYMENT_ENVIRONMENT_TOKEN = "deploymentEnvironment"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index ec37f2afd..c9548254c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -4,6 +4,7 @@ import com.structurizr.documentation.Section; import com.structurizr.model.*; import com.structurizr.util.StringUtils; +import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -1442,4 +1443,15 @@ void test_sourceIsNotRetained() throws Exception { assertNull(workspace.getProperties().get(DslUtils.STRUCTURIZR_DSL_PROPERTY_NAME)); } + @Test + void test_archetypes() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/archetypes.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setArchetypesEnabled(true); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + + WorkspaceUtils.printWorkspaceAsJson(workspace); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes.dsl new file mode 100644 index 000000000..1ad2d6089 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes.dsl @@ -0,0 +1,39 @@ +workspace { + + model { + archetypes { + application = container + datastore = container + + microservice = group + + springBootApplication = application { + technology "Spring Boot" + tags "Spring Boot" + } + + restController = component { + technology "Spring MVC REST Controller" + tag "Spring MVC REST Controller" + } + repository = component { + technology "Spring Data Repository" + tag "Spring Data Repository" + } + } + + x = softwareSystem "X" { + customerService = microservice "Customer Service" { + db = datastore "Customer database" + api = springBootApplication "Customer API" { + customerController = restController "Customer Controller" + customerRepository = repository "Customer Repository" { + customerController -> this + this -> db + } + } + } + } + } + +} \ No newline at end of file From 11ff1e804ba23e69f89e3e6854a94bfe37a2d003 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:27:27 +0000 Subject: [PATCH 314/418] Adds a way to more easily enable/disable features; fixes tests. --- .../java/com/structurizr/util/Features.java | 26 ++++++++++++++ .../java/com/structurizr/dsl/Features.java | 7 ++++ .../structurizr/dsl/StructurizrDslParser.java | 36 +++++++++---------- .../java/com/structurizr/dsl/DslTests.java | 22 ++++++++++-- .../src/test/resources/dsl/archetypes.dsl | 8 +++-- 5 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 structurizr-core/src/main/java/com/structurizr/util/Features.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java diff --git a/structurizr-core/src/main/java/com/structurizr/util/Features.java b/structurizr-core/src/main/java/com/structurizr/util/Features.java new file mode 100644 index 000000000..44c615cfd --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/util/Features.java @@ -0,0 +1,26 @@ +package com.structurizr.util; + +import java.util.HashMap; +import java.util.Map; + +public class Features { + + private final Map features = new HashMap<>(); + + public void enable(String feature) { + features.put(feature, true); + } + + public void disable(String feature) { + features.put(feature, false); + } + + public void configure(String feature, boolean enabled) { + features.put(feature, enabled); + } + + public boolean isEnabled(String feature) { + return features.getOrDefault(feature, false); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java new file mode 100644 index 000000000..03a99eb4f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java @@ -0,0 +1,7 @@ +package com.structurizr.dsl; + +public final class Features extends com.structurizr.util.Features { + + public static final String ARCHETYPES = "structurizr.feature.dsl.archetypes"; + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 32b75a186..cbd43f361 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -45,8 +45,8 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private final Set parsedTokens = new HashSet<>(); private final IdentifiersRegister identifiersRegister; private final Map constantsAndVariables; + private final Features features = new Features(); - private boolean archetypesEnabled = false; private final Map> archetypes = Map.of( StructurizrDslTokens.GROUP_TOKEN, new HashMap<>(), StructurizrDslTokens.PERSON_TOKEN, new HashMap<>(), @@ -420,7 +420,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (isElementKeywordOrArchetype(firstToken, PERSON_TOKEN) && (inContext(ModelDslContext.class))) { Person person = new PersonParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { applyArchetype(firstToken, person); } @@ -432,7 +432,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (isElementKeywordOrArchetype(firstToken, SOFTWARE_SYSTEM_TOKEN) && (inContext(ModelDslContext.class))) { SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { applyArchetype(firstToken, softwareSystem); } @@ -444,7 +444,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (isElementKeywordOrArchetype(firstToken, CONTAINER_TOKEN) && inContext(SoftwareSystemDslContext.class)) { Container container = new ContainerParser().parse(getContext(SoftwareSystemDslContext.class), tokens.withoutContextStartToken()); - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { applyArchetype(firstToken, container); } @@ -456,7 +456,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (isElementKeywordOrArchetype(firstToken, COMPONENT_TOKEN) && inContext(ContainerDslContext.class)) { Component component = new ComponentParser().parse(getContext(ContainerDslContext.class), tokens.withoutContextStartToken()); - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { applyArchetype(firstToken, component); } @@ -658,7 +658,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new ModelDslContext()); parsedTokens.add(MODEL_TOKEN); - } else if (ARCHETYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class) && archetypesEnabled) { + } else if (ARCHETYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class) && features.isEnabled(Features.ARCHETYPES)) { startContext(new ArchetypesDslContext()); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ArchetypesDslContext.class)) { @@ -843,7 +843,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens.withoutContextStartToken()); - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { applyArchetype(firstToken, deploymentNode); } @@ -854,7 +854,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, deploymentNode); } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { applyArchetype(firstToken, deploymentNode); } @@ -865,7 +865,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, deploymentNode); } else if (isElementKeywordOrArchetype(firstToken, INFRASTRUCTURE_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { InfrastructureNode infrastructureNode = new InfrastructureNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { applyArchetype(firstToken, infrastructureNode); } @@ -1292,12 +1292,12 @@ private boolean isGroup(DslContext context) { return false; } - public void setArchetypesEnabled(boolean archetypesEnabled) { - this.archetypesEnabled = archetypesEnabled; + public Features getFeatures() { + return features; } private boolean isElementKeywordOrArchetype(String token, String keyword) { - if (archetypesEnabled) { + if (features.isEnabled(Features.ARCHETYPES)) { if (token.equalsIgnoreCase(keyword)) { return true; } else { @@ -1323,7 +1323,7 @@ private void extendArchetype(Archetype archetype, String archetypeName) { } private void applyArchetype(String archetypeName, Person person) { - Archetype archetype = archetypes.get(StructurizrDslTokens.PERSON_TOKEN).get(archetypeName); + Archetype archetype = archetypes.get(StructurizrDslTokens.PERSON_TOKEN).get(archetypeName.toLowerCase()); if (archetype != null) { person.setDescription(archetype.getDescription()); person.addTags(archetype.getTags().toArray(new String[0])); @@ -1331,7 +1331,7 @@ private void applyArchetype(String archetypeName, Person person) { } private void applyArchetype(String archetypeName, SoftwareSystem softwareSystem) { - Archetype archetype = archetypes.get(StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN).get(archetypeName); + Archetype archetype = archetypes.get(StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN).get(archetypeName.toLowerCase()); if (archetype != null) { softwareSystem.setDescription(archetype.getDescription()); softwareSystem.addTags(archetype.getTags().toArray(new String[0])); @@ -1339,7 +1339,7 @@ private void applyArchetype(String archetypeName, SoftwareSystem softwareSystem) } private void applyArchetype(String archetypeName, Container container) { - Archetype archetype = archetypes.get(StructurizrDslTokens.CONTAINER_TOKEN).get(archetypeName); + Archetype archetype = archetypes.get(StructurizrDslTokens.CONTAINER_TOKEN).get(archetypeName.toLowerCase()); if (archetype != null) { container.setTechnology(archetype.getTechnology()); container.setDescription(archetype.getDescription()); @@ -1348,7 +1348,7 @@ private void applyArchetype(String archetypeName, Container container) { } private void applyArchetype(String archetypeName, Component component) { - Archetype archetype = archetypes.get(StructurizrDslTokens.COMPONENT_TOKEN).get(archetypeName); + Archetype archetype = archetypes.get(StructurizrDslTokens.COMPONENT_TOKEN).get(archetypeName.toLowerCase()); if (archetype != null) { component.setTechnology(archetype.getTechnology()); component.setDescription(archetype.getDescription()); @@ -1357,7 +1357,7 @@ private void applyArchetype(String archetypeName, Component component) { } private void applyArchetype(String archetypeName, DeploymentNode deploymentNode) { - Archetype archetype = archetypes.get(StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN).get(archetypeName); + Archetype archetype = archetypes.get(StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN).get(archetypeName.toLowerCase()); if (archetype != null) { deploymentNode.setTechnology(archetype.getTechnology()); deploymentNode.setDescription(archetype.getDescription()); @@ -1366,7 +1366,7 @@ private void applyArchetype(String archetypeName, DeploymentNode deploymentNode) } private void applyArchetype(String archetypeName, InfrastructureNode infrastructureNode) { - Archetype archetype = archetypes.get(StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN).get(archetypeName); + Archetype archetype = archetypes.get(StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN).get(archetypeName.toLowerCase()); if (archetype != null) { infrastructureNode.setTechnology(archetype.getTechnology()); infrastructureNode.setDescription(archetype.getDescription()); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index c9548254c..278a48d9b 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1444,14 +1444,30 @@ void test_sourceIsNotRetained() throws Exception { } @Test - void test_archetypes() throws Exception { + void test_archetypes_WhenDisabled() throws Exception { + try { + File parentDslFile = new File("src/test/resources/dsl/archetypes.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(parentDslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertTrue(e.getMessage().startsWith("Unexpected tokens (expected: !identifiers, group, person, softwareSystem, deploymentEnvironment, element, ->) at line 4")); + assertTrue(e.getMessage().endsWith("archetypes {")); + } + } + + @Test + void test_archetypes_WhenEnabled() throws Exception { File parentDslFile = new File("src/test/resources/dsl/archetypes.dsl"); StructurizrDslParser parser = new StructurizrDslParser(); - parser.setArchetypesEnabled(true); + parser.getFeatures().enable(Features.ARCHETYPES); parser.parse(parentDslFile); Workspace workspace = parser.getWorkspace(); - WorkspaceUtils.printWorkspaceAsJson(workspace); + Container customerApi = workspace.getModel().getSoftwareSystemWithName("X").getContainerWithName("Customer API"); + assertTrue(customerApi.getTagsAsSet().contains("Application")); + assertTrue(customerApi.getTagsAsSet().contains("Spring Boot")); + assertEquals("Spring Boot", customerApi.getTechnology()); } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes.dsl index 1ad2d6089..d7524e06e 100644 --- a/structurizr-dsl/src/test/resources/dsl/archetypes.dsl +++ b/structurizr-dsl/src/test/resources/dsl/archetypes.dsl @@ -2,8 +2,12 @@ workspace { model { archetypes { - application = container - datastore = container + application = container { + tag "Application" + } + datastore = container { + tag "Datastore" + } microservice = group From f00c975dffdf247c5d7cbdc9055a8a20d3dc952e Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:43:37 +0000 Subject: [PATCH 315/418] Improves request/response logging of WorkspaceApiClient. --- .../structurizr/api/WorkspaceApiClient.java | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java index 3b7ce5c95..8524f753d 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -22,6 +22,7 @@ import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; @@ -201,11 +202,10 @@ private boolean manageLockForWorkspace(long workspaceId, boolean lock) throws St debugRequest(httpRequest, null); try (CloseableHttpResponse response = httpClient.execute(httpRequest)) { - debugResponse(response); + String json = EntityUtils.toString(response.getEntity()); + debugResponse(response, json); - String responseText = EntityUtils.toString(response.getEntity()); - ApiResponse apiResponse = ApiResponse.parse(responseText); - log.info(responseText); + ApiResponse apiResponse = ApiResponse.parse(json); if (response.getCode() == HttpStatus.SC_OK) { return apiResponse.isSuccess(); @@ -245,9 +245,9 @@ public Workspace getWorkspace(long workspaceId) throws StructurizrClientExceptio debugRequest(httpGet, null); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { - debugResponse(response); - String json = EntityUtils.toString(response.getEntity()); + debugResponse(response, json); + if (response.getCode() == HttpStatus.SC_OK) { archiveWorkspace(workspaceId, json); @@ -339,10 +339,9 @@ public void putWorkspace(long workspaceId, Workspace workspace) throws Structuri log.info("Putting workspace with ID " + workspaceId); try (CloseableHttpResponse response = httpClient.execute(httpPut)) { String json = EntityUtils.toString(response.getEntity()); - if (response.getCode() == HttpStatus.SC_OK) { - debugResponse(response); - log.info(json); - } else { + debugResponse(response, json); + + if (response.getCode() != HttpStatus.SC_OK) { ApiResponse apiResponse = ApiResponse.parse(json); throw new StructurizrClientException(apiResponse.getMessage()); } @@ -355,19 +354,29 @@ public void putWorkspace(long workspaceId, Workspace workspace) throws Structuri private void debugRequest(HttpUriRequestBase httpRequest, String content) { if (log.isDebugEnabled()) { - log.debug(httpRequest.getMethod() + " " + httpRequest.getPath()); + log.debug("Request"); + log.debug("HTTP method: " + httpRequest.getMethod()); + log.debug("Path: " + httpRequest.getPath()); Header[] headers = httpRequest.getHeaders(); for (Header header : headers) { - log.debug(header.getName() + ": " + header.getValue()); + log.debug("Header: " + header.getName() + "=" + header.getValue()); } if (content != null) { + log.debug("---Start content---"); log.debug(content); + log.debug("---End content---"); } } } - private void debugResponse(CloseableHttpResponse response) { - log.debug(response.getCode()); + private void debugResponse(CloseableHttpResponse response, String content) { + log.debug("Response"); + log.debug("HTTP status code: " + response.getCode()); + if (content != null) { + log.debug("---Start content---"); + log.debug(content); + log.debug("---End content---"); + } } private void addHeaders(HttpUriRequestBase httpRequest, String content, String contentType) throws Exception { From 461595b719a45c67b81886f18a74f3123ee5bba3 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Tue, 11 Feb 2025 09:40:47 +0000 Subject: [PATCH 316/418] Fixes tests. --- .../src/test/java/com/structurizr/view/ThemeUtilsTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java index ae1f66800..5f4882881 100644 --- a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java @@ -39,7 +39,7 @@ void loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { assertNotNull(style); assertEquals("#d6242d", style.getStroke()); assertEquals("#d6242d", style.getColor()); - assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Alexa-For-Business_light-bg@4x.png", style.getIcon()); + assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png", style.getIcon()); } @Test @@ -141,7 +141,7 @@ void loadThemes_ReplacesRelativeIconReferences() throws Exception { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); softwareSystem.addTags("Amazon Web Services - Alexa For Business"); - workspace.getViews().getConfiguration().setThemes("https://raw.githubusercontent.com/structurizr/themes/master/amazon-web-services-2020.04.30/theme.json"); + workspace.getViews().getConfiguration().setThemes("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json"); ThemeUtils.loadThemes(workspace); @@ -153,7 +153,7 @@ void loadThemes_ReplacesRelativeIconReferences() throws Exception { assertNotNull(style); assertEquals("#d6242d", style.getStroke()); assertEquals("#d6242d", style.getColor()); - assertEquals("https://raw.githubusercontent.com/structurizr/themes/master/amazon-web-services-2020.04.30/Alexa-For-Business_light-bg@4x.png", style.getIcon()); + assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png", style.getIcon()); } } \ No newline at end of file From a998bfaacbe3cb2500ec559f21728347000ee8d0 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Tue, 11 Feb 2025 09:44:13 +0000 Subject: [PATCH 317/418] Fixes tests. --- .../structurizr/export/ilograph/54915.ilograph | 14 +++++++------- ...915-AmazonWebServicesDeployment-WithTags.puml | 16 ++++++++-------- ...54915-AmazonWebServicesDeployment-Legend.puml | 16 ++++++++-------- .../54915-AmazonWebServicesDeployment.puml | 16 ++++++++-------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph index 411ac3c5e..5b37222b4 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph @@ -26,7 +26,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#232f3e" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png" children: - id: "6" @@ -34,7 +34,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#147eba" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png" children: - id: "12" @@ -42,7 +42,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#3b48cc" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png" children: - id: "13" @@ -50,7 +50,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#3b48cc" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png" children: - id: "14" @@ -65,7 +65,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#cc2264" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png" children: - id: "10" @@ -73,7 +73,7 @@ resources: subtitle: "[Deployment Node]" backgroundColor: "#ffffff" color: "#d86613" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png" children: - id: "11" @@ -97,7 +97,7 @@ resources: description: "Automatically distributes incoming application traffic." backgroundColor: "#ffffff" color: "#693cc5" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png" perspectives: - name: Static Structure diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml index 420931723..da23c6199 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml @@ -10,15 +10,15 @@ left to right direction !include AddElementTag("Container,Application", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Auto-Scaling_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-Route-53_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-EC2_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Region_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Elastic-Load-Balancing_light-bg@4x.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/Amazon-RDS_MySQL_instance_light-bg@4x.png{scale=0.15}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-route-53.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png{scale=0.1}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png{scale=0.15}", $shadowing="", $borderStyle="solid") AddElementTag("Container,Database", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/AWS-Cloud_light-bg@4x.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") +AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml index 3532dad2d..12bb6a1ce 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml @@ -22,7 +22,7 @@ skinparam rectangle<<1>> { BorderColor #cc2264 roundCorner 20 } -rectangle "==Amazon Web Services - Auto Scaling\n\n" <<1>> +rectangle "==Amazon Web Services - Auto Scaling\n\n" <<1>> skinparam rectangle<<2>> { BackgroundColor #ffffff @@ -30,7 +30,7 @@ skinparam rectangle<<2>> { BorderColor #232f3e roundCorner 20 } -rectangle "==Amazon Web Services - Cloud\n\n" <<2>> +rectangle "==Amazon Web Services - Cloud\n\n" <<2>> skinparam rectangle<<3>> { BackgroundColor #ffffff @@ -38,7 +38,7 @@ skinparam rectangle<<3>> { BorderColor #d86613 roundCorner 20 } -rectangle "==Amazon Web Services - EC2\n\n" <<3>> +rectangle "==Amazon Web Services - EC2\n\n" <<3>> skinparam rectangle<<4>> { BackgroundColor #ffffff @@ -46,7 +46,7 @@ skinparam rectangle<<4>> { BorderColor #693cc5 roundCorner 20 } -rectangle "==Amazon Web Services - Elastic Load Balancing\n\n" <<4>> +rectangle "==Amazon Web Services - Elastic Load Balancing\n\n" <<4>> skinparam rectangle<<5>> { BackgroundColor #ffffff @@ -54,7 +54,7 @@ skinparam rectangle<<5>> { BorderColor #3b48cc roundCorner 20 } -rectangle "==Amazon Web Services - RDS\n\n" <<5>> +rectangle "==Amazon Web Services - RDS\n\n" <<5>> skinparam rectangle<<6>> { BackgroundColor #ffffff @@ -62,7 +62,7 @@ skinparam rectangle<<6>> { BorderColor #3b48cc roundCorner 20 } -rectangle "==Amazon Web Services - RDS MySQL instance\n\n" <<6>> +rectangle "==Amazon Web Services - RDS MySQL instance\n\n" <<6>> skinparam rectangle<<7>> { BackgroundColor #ffffff @@ -70,7 +70,7 @@ skinparam rectangle<<7>> { BorderColor #147eba roundCorner 20 } -rectangle "==Amazon Web Services - Region\n\n" <<7>> +rectangle "==Amazon Web Services - Region\n\n" <<7>> skinparam rectangle<<8>> { BackgroundColor #ffffff @@ -78,7 +78,7 @@ skinparam rectangle<<8>> { BorderColor #693cc5 roundCorner 20 } -rectangle "==Amazon Web Services - Route 53\n\n" <<8>> +rectangle "==Amazon Web Services - Route 53\n\n" <<8>> skinparam rectangle<<9>> { BackgroundColor #ffffff diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml index b0799c732..a32f9c329 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml @@ -83,19 +83,19 @@ skinparam rectangle<[Deployment Node]\n\n" <> as Live.AmazonWebServices { - rectangle "US-East-1\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1 { - rectangle "Amazon RDS\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { - rectangle "MySQL\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { +rectangle "Amazon Web Services\n[Deployment Node]\n\n" <> as Live.AmazonWebServices { + rectangle "US-East-1\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1 { + rectangle "Amazon RDS\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { database "==Database\n[Container: Relational database schema]\n\nStores information regarding the veterinarians, the clients, and their pets." <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1 } } - rectangle "==Route 53\n[Infrastructure Node]\n\nHighly available and scalable cloud DNS service.\n\n" <> as Live.AmazonWebServices.USEast1.Route53 - rectangle "==Elastic Load Balancer\n[Infrastructure Node]\n\nAutomatically distributes incoming application traffic.\n\n" <> as Live.AmazonWebServices.USEast1.ElasticLoadBalancer - rectangle "Autoscaling group\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { - rectangle "Amazon EC2\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2 { + rectangle "==Route 53\n[Infrastructure Node]\n\nHighly available and scalable cloud DNS service.\n\n" <> as Live.AmazonWebServices.USEast1.Route53 + rectangle "==Elastic Load Balancer\n[Infrastructure Node]\n\nAutomatically distributes incoming application traffic.\n\n" <> as Live.AmazonWebServices.USEast1.ElasticLoadBalancer + rectangle "Autoscaling group\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2 { rectangle "==Web Application\n[Container: Java and Spring Boot]\n\nAllows employees to view and manage information regarding the veterinarians, the clients, and their pets." <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1 } From 95ac0ab589fea127f14066ea57060653bbd972f5 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:09:20 +0000 Subject: [PATCH 318/418] Fixes #374. --- changelog.md | 4 ++++ .../java/com/structurizr/dsl/DslParserContext.java | 10 ++++++++-- .../java/com/structurizr/dsl/StructurizrDslParser.java | 2 +- .../main/java/com/structurizr/dsl/WorkspaceParser.java | 2 ++ .../src/test/java/com/structurizr/dsl/DslTests.java | 2 ++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index e3a3806d7..db6a5505b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## (unreleased) + +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/374 (!identifiers hierarchical isn't propagated when extending a workspace). + ## 3.2.1 (10th December 2024) - structurizr-core: Fixes https://github.com/structurizr/java/issues/362 (Ordering of replicated relationships in deployment environment is non-deterministic). diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java index 7fd4eea7b..ed964b481 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java @@ -4,14 +4,20 @@ final class DslParserContext extends DslContext { - private final boolean restricted; + private final StructurizrDslParser parser; private final File file; + private final boolean restricted; - DslParserContext(File file, boolean restricted) { + DslParserContext(StructurizrDslParser parser, File file, boolean restricted) { + this.parser = parser; this.file = file; this.restricted = restricted; } + StructurizrDslParser getParser() { + return parser; + } + File getFile() { return file; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index cbd43f361..99e5f88bf 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -634,7 +634,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn if (parsedTokens.contains(WORKSPACE_TOKEN)) { throw new RuntimeException("Multiple workspaces are not permitted in a DSL definition"); } - DslParserContext dslParserContext = new DslParserContext(dslFile, restricted); + DslParserContext dslParserContext = new DslParserContext(this, dslFile, restricted); dslParserContext.setIdentifierRegister(identifiersRegister); workspace = new WorkspaceParser().parse(dslParserContext, tokens.withoutContextStartToken()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java index 44f11b2c7..7f70fde3a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java @@ -49,6 +49,7 @@ Workspace parse(DslParserContext context, Tokens tokens) { structurizrDslParser.setRestricted(context.isRestricted()); structurizrDslParser.parse(context, dsl); workspace = structurizrDslParser.getWorkspace(); + context.getParser().setIdentifierScope(structurizrDslParser.getIdentifierScope()); } } else { if (context.isRestricted()) { @@ -72,6 +73,7 @@ Workspace parse(DslParserContext context, Tokens tokens) { StructurizrDslParser structurizrDslParser = new StructurizrDslParser(); structurizrDslParser.parse(context, file); workspace = structurizrDslParser.getWorkspace(); + context.getParser().setIdentifierScope(structurizrDslParser.getIdentifierScope()); } } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 278a48d9b..8ca22ff83 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -479,6 +479,8 @@ void test_extendWorkspaceFromDsl(String dslFile) throws Exception { parser.parse(new File(dslFile)); Workspace workspace = parser.getWorkspace(); + assertEquals(IdentifierScope.Hierarchical, parser.getIdentifierScope()); + Model model = workspace.getModel(); assertEquals(CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.class, model.getImpliedRelationshipsStrategy().getClass()); From 4e27be67449376c020591d0509542cdd3c1e44a3 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:40:06 +0000 Subject: [PATCH 319/418] Spring PetClinic 3.3.0 -> 3.4.0. --- .../java/com/structurizr/component/SpringPetClinicTests.java | 5 +++-- .../src/test/resources/dsl/spring-petclinic/workspace.dsl | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java index a4158f36f..4df29513a 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java @@ -6,6 +6,7 @@ import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.AnnotationTypeMatcher; import com.structurizr.component.matcher.ImplementsTypeMatcher; +import com.structurizr.component.matcher.NameSuffixTypeMatcher; import com.structurizr.component.url.PrefixSourceUrlStrategy; import com.structurizr.model.Component; import com.structurizr.model.Container; @@ -35,7 +36,7 @@ void springPetClinic() { ComponentFinder componentFinder = new ComponentFinderBuilder() .forContainer(webApplication) - .fromClasses(new File(springPetClinicHome, "target/spring-petclinic-3.3.0-SNAPSHOT.jar")) + .fromClasses(new File(springPetClinicHome, "target/spring-petclinic-3.4.0-SNAPSHOT.jar")) .fromSource(new File(springPetClinicHome, "src/main/java")) .filteredBy(new IncludeFullyQualifiedNameRegexFilter("org\\.springframework\\.samples\\.petclinic\\..*")) .withStrategy( @@ -52,7 +53,7 @@ void springPetClinic() { ) .withStrategy( new ComponentFinderStrategyBuilder() - .matchedBy(new ImplementsTypeMatcher("org.springframework.data.repository.Repository")) + .matchedBy(new NameSuffixTypeMatcher("Repository")) .withDescription(new FirstSentenceDescriptionStrategy()) .withTechnology("Spring Data Repository") .withUrl(new PrefixSourceUrlStrategy("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java")) diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index b40a2d4bb..00202026f 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -21,7 +21,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt technology "Java and Spring" !components { - classes "${SPRING_PETCLINIC_HOME}/target/spring-petclinic-3.3.0-SNAPSHOT.jar" + classes "${SPRING_PETCLINIC_HOME}/target/spring-petclinic-3.4.0-SNAPSHOT.jar" source "${SPRING_PETCLINIC_HOME}/src/main/java" filter include fqn-regex "org.springframework.samples.petclinic..*" strategy { @@ -36,7 +36,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt } strategy { technology "Spring Data Repository" - matcher implements "org.springframework.data.repository.Repository" + matcher name-suffix "Repository" description first-sentence url prefix-src "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" forEach { From 5b64d91be0fca0b21960ceae01d0b7899bea59b5 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:24:29 +0000 Subject: [PATCH 320/418] structurizr-dsl: Adds the ability to use the `group` keyword inside a component definition, to set the group name of that component. --- changelog.md | 3 +- .../structurizr/dsl/ComponentDslContext.java | 1 + .../java/com/structurizr/dsl/GroupParser.java | 41 +++++++-- .../structurizr/dsl/StructurizrDslParser.java | 13 +-- .../java/com/structurizr/dsl/DslTests.java | 6 ++ .../com/structurizr/dsl/GroupParserTests.java | 84 ++++++++++++++++--- .../src/test/resources/dsl/groups-nested.dsl | 11 ++- 7 files changed, 134 insertions(+), 25 deletions(-) diff --git a/changelog.md b/changelog.md index db6a5505b..4251bb653 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,9 @@ # Changelog -## (unreleased) +## v4.0.0 (unreleased) - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/374 (!identifiers hierarchical isn't propagated when extending a workspace). +- structurizr-dsl: Adds the ability to use the `group` keyword inside a component definition, to set the group name of that component. ## 3.2.1 (10th December 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java index d6e5da57f..f207e3ebe 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java @@ -37,6 +37,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.URL_TOKEN, StructurizrDslTokens.PROPERTIES_TOKEN, StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.GROUP_TOKEN, StructurizrDslTokens.RELATIONSHIP_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java index 255f3db9d..251500fb6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java @@ -1,29 +1,32 @@ package com.structurizr.dsl; +import com.structurizr.model.Component; import com.structurizr.util.StringUtils; class GroupParser { private static final String STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator"; - private static final String GRAMMAR = "group {"; + private static final String GRAMMAR_AS_CONTEXT = "group {"; + private static final String GRAMMAR_AS_PROPERTY = "group "; private final static int NAME_INDEX = 1; private final static int BRACE_INDEX = 2; - ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) { + + ElementGroup parseContext(GroupableDslContext dslContext, Tokens tokens) { // group { if (tokens.hasMoreThan(BRACE_INDEX)) { - throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_AS_CONTEXT); } if (!tokens.includes(BRACE_INDEX)) { - throw new RuntimeException("Expected: " + GRAMMAR); + throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT); } if (!DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(tokens.get(BRACE_INDEX))) { - throw new RuntimeException("Expected: " + GRAMMAR); + throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT); } ElementGroup group; @@ -42,4 +45,32 @@ ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) { return group; } + void parseProperty(ComponentDslContext dslContext, Tokens tokens) { + // group + + if (tokens.includes(BRACE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_AS_PROPERTY); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR_AS_PROPERTY); + } + + String group = tokens.get(NAME_INDEX); + + Component component = dslContext.getComponent(); + String existingGroup = component.getGroup(); + + if (!StringUtils.isNullOrEmpty(existingGroup)) { + String groupSeparator = dslContext.getWorkspace().getModel().getProperties().getOrDefault(STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME, ""); + if (StringUtils.isNullOrEmpty(groupSeparator)) { + throw new RuntimeException("To use nested groups, please define a model property named " + STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME); + } + + group = existingGroup + groupSeparator + group; + } + + component.setGroup(group); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 99e5f88bf..db2d0e034 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -522,32 +522,32 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn throw new RuntimeException("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group)"); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ModelDslContext.class)) { - ElementGroup group = new GroupParser().parse(getContext(ModelDslContext.class), tokens); + ElementGroup group = new GroupParser().parseContext(getContext(ModelDslContext.class), tokens); startContext(new ModelDslContext(group)); registerIdentifier(identifier, group); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(SoftwareSystemDslContext.class)) { - ElementGroup group = new GroupParser().parse(getContext(SoftwareSystemDslContext.class), tokens); + ElementGroup group = new GroupParser().parseContext(getContext(SoftwareSystemDslContext.class), tokens); SoftwareSystem softwareSystem = getContext(SoftwareSystemDslContext.class).getSoftwareSystem(); group.setParent(softwareSystem); startContext(new SoftwareSystemDslContext(softwareSystem, group)); registerIdentifier(identifier, group); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ContainerDslContext.class)) { - ElementGroup group = new GroupParser().parse(getContext(ContainerDslContext.class), tokens); + ElementGroup group = new GroupParser().parseContext(getContext(ContainerDslContext.class), tokens); Container container = getContext(ContainerDslContext.class).getContainer(); group.setParent(container); startContext(new ContainerDslContext(container, group)); registerIdentifier(identifier, group); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { - ElementGroup group = new GroupParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens); + ElementGroup group = new GroupParser().parseContext(getContext(DeploymentEnvironmentDslContext.class), tokens); String environment = getContext(DeploymentEnvironmentDslContext.class).getEnvironment(); startContext(new DeploymentEnvironmentDslContext(environment, group)); registerIdentifier(identifier, group); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentNodeDslContext.class)) { - ElementGroup group = new GroupParser().parse(getContext(DeploymentNodeDslContext.class), tokens); + ElementGroup group = new GroupParser().parseContext(getContext(DeploymentNodeDslContext.class), tokens); DeploymentNode deploymentNode = getContext(DeploymentNodeDslContext.class).getDeploymentNode(); startContext(new DeploymentNodeDslContext(deploymentNode, group)); @@ -630,6 +630,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (inContext(PerspectivesDslContext.class)) { new PerspectiveParser().parse(getContext(PerspectivesDslContext.class), tokens); + } else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) { + new GroupParser().parseProperty(getContext(ComponentDslContext.class), tokens); + } else if (WORKSPACE_TOKEN.equalsIgnoreCase(firstToken) && contextStack.empty()) { if (parsedTokens.contains(WORKSPACE_TOKEN)) { throw new RuntimeException("Multiple workspaces are not permitted in a DSL definition"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 8ca22ff83..920373cf9 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -641,6 +641,12 @@ void test_nested_groups() throws Exception { Container aApi = a.getContainerWithName("A API"); assertEquals("Capability 1/Service A", aApi.getGroup()); + Component aApiEndpoint = aApi.getComponentWithName("API Endpoint"); + assertEquals("a-api.jar/API Layer", aApiEndpoint.getGroup()); + + Component aApiRepository = aApi.getComponentWithName("Repository"); + assertEquals("a-api.jar/Data Layer", aApiRepository.getGroup()); + Container aDatabase = a.getContainerWithName("A Database"); assertEquals("Capability 1/Service A", aDatabase.getGroup()); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java index ef1af486a..b4415f1d1 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java @@ -1,17 +1,18 @@ package com.structurizr.dsl; +import com.structurizr.model.Component; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class GroupParserTests extends AbstractTests { - private GroupParser parser = new GroupParser(); + private final GroupParser parser = new GroupParser(); @Test - void parse_ThrowsAnException_WhenThereAreTooManyTokens() { + void parseContext_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(null, tokens("group", "name", "{", "extra")); + parser.parseContext(null, tokens("group", "name", "{", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: group {", e.getMessage()); @@ -19,9 +20,9 @@ void parse_ThrowsAnException_WhenThereAreTooManyTokens() { } @Test - void parse_ThrowsAnException_WhenTheNameIsMissing() { + void parseContext_ThrowsAnException_WhenTheNameIsMissing() { try { - parser.parse(null, tokens("group")); + parser.parseContext(null, tokens("group")); fail(); } catch (Exception e) { assertEquals("Expected: group {", e.getMessage()); @@ -29,9 +30,9 @@ void parse_ThrowsAnException_WhenTheNameIsMissing() { } @Test - void parse_ThrowsAnException_WhenTheBraceIsMissing() { + void parseContext_ThrowsAnException_WhenTheBraceIsMissing() { try { - parser.parse(null, tokens("group", "Name", "foo")); + parser.parseContext(null, tokens("group", "Name", "foo")); fail(); } catch (Exception e) { assertEquals("Expected: group {", e.getMessage()); @@ -39,19 +40,19 @@ void parse_ThrowsAnException_WhenTheBraceIsMissing() { } @Test - void parse() { - ElementGroup group = parser.parse(context(), tokens("group", "Group 1", "{")); + void parseContext() { + ElementGroup group = parser.parseContext(context(), tokens("group", "Group 1", "{")); assertEquals("Group 1", group.getName()); assertTrue(group.getElements().isEmpty()); } @Test - void parse_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { + void parseContext_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1")); context.setWorkspace(workspace); try { - parser.parse(context, tokens("group", "Group 2", "{")); + parser.parseContext(context, tokens("group", "Group 2", "{")); fail(); } catch (Exception e) { assertEquals("To use nested groups, please define a model property named structurizr.groupSeparator", e.getMessage()); @@ -59,14 +60,71 @@ void parse_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { } @Test - void parse_NestedGroup() { + void parseContext_NestedGroup() { workspace.getModel().addProperty("structurizr.groupSeparator", "/"); ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1")); context.setWorkspace(workspace); - ElementGroup group = parser.parse(context, tokens("group", "Group 2", "{")); + ElementGroup group = parser.parseContext(context, tokens("group", "Group 2", "{")); assertEquals("Group 1/Group 2", group.getName()); assertTrue(group.getElements().isEmpty()); } + @Test + void parseProperty_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseProperty(null, tokens("group", "name", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: group ", e.getMessage()); + } + } + + @Test + void parseProperty_ThrowsAnException_WhenTheNameIsMissing() { + try { + parser.parseProperty(null, tokens("group")); + fail(); + } catch (Exception e) { + assertEquals("Expected: group ", e.getMessage()); + } + } + + @Test + void parseProperty() { + Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name"); + ComponentDslContext context = new ComponentDslContext(component); + context.setWorkspace(workspace); + + parser.parseProperty(context, tokens("group", "Group 1")); + assertEquals("Group 1", component.getGroup()); + } + + @Test + void parseProperty_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { + Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name"); + component.setGroup("Group 1"); + ComponentDslContext context = new ComponentDslContext(component); + context.setWorkspace(workspace); + + try { + parser.parseProperty(context, tokens("group", "Group 2")); + fail(); + } catch (Exception e) { + assertEquals("To use nested groups, please define a model property named structurizr.groupSeparator", e.getMessage()); + } + } + + @Test + void parseProperty_NestedGroup() { + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name"); + component.setGroup("Group 1"); + ComponentDslContext context = new ComponentDslContext(component); + context.setWorkspace(workspace); + + parser.parseProperty(context, tokens("group", "Group 2")); + assertEquals("Group 1/Group 2", component.getGroup()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl b/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl index 47ac91617..6a355bb33 100644 --- a/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl +++ b/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl @@ -10,7 +10,16 @@ workspace { a = softwareSystem "A" { group "Capability 1" { group "Service A" { - container "A API" + container "A API" { + group "a-api.jar" { + component "API Endpoint" { + group "API Layer" + } + component "Repository" { + group "Data Layer" + } + } + } container "A Database" } group "Service B" { From 677d5e14d30ee427809e4c2367ede0b3ed203209 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:31:29 +0000 Subject: [PATCH 321/418] structurizr-dsl: Adds the ability to use the `group` keyword inside the component finder strategy `forEach` block. --- changelog.md | 1 + .../structurizr/dsl/ComponentFinderDslContext.java | 11 ++++++++--- .../dsl/ComponentFinderStrategyDslContext.java | 13 ++++++++----- .../ComponentFinderStrategyForEachDslContext.java | 8 +++++++- .../com/structurizr/dsl/StructurizrDslParser.java | 5 ++--- .../structurizr/dsl/ComponentFinderParserTests.java | 2 +- .../dsl/ComponentFinderStrategyParserTests.java | 4 +--- .../src/test/java/com/structurizr/dsl/DslTests.java | 7 +++++++ .../resources/dsl/spring-petclinic/workspace.dsl | 2 ++ 9 files changed, 37 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index 4251bb653..e703945db 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/374 (!identifiers hierarchical isn't propagated when extending a workspace). - structurizr-dsl: Adds the ability to use the `group` keyword inside a component definition, to set the group name of that component. +- structurizr-dsl: Adds the ability to use the `group` keyword inside the component finder strategy `forEach` block. ## 3.2.1 (10th December 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java index 89f030ff8..96dc8c077 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java @@ -2,7 +2,6 @@ import com.structurizr.component.ComponentFinderBuilder; import com.structurizr.model.Component; -import com.structurizr.model.Container; import java.util.Set; @@ -11,10 +10,12 @@ final class ComponentFinderDslContext extends DslContext { private final ComponentFinderBuilder componentFinderBuilder = new ComponentFinderBuilder(); private final StructurizrDslParser dslParser; + private final ContainerDslContext containerDslContext; - ComponentFinderDslContext(StructurizrDslParser dslParser, Container container) { + ComponentFinderDslContext(StructurizrDslParser dslParser, ContainerDslContext containerDslContext) { this.dslParser = dslParser; - componentFinderBuilder.forContainer(container); + this.containerDslContext = containerDslContext; + componentFinderBuilder.forContainer(containerDslContext.getContainer()); } @Override @@ -26,6 +27,10 @@ protected String[] getPermittedTokens() { }; } + ContainerDslContext getContainerDslContext() { + return containerDslContext; + } + ComponentFinderBuilder getComponentFinderBuilder() { return this.componentFinderBuilder; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java index 2336fa006..805de90b3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java @@ -1,15 +1,14 @@ package com.structurizr.dsl; -import com.structurizr.component.ComponentFinderBuilder; import com.structurizr.component.ComponentFinderStrategyBuilder; final class ComponentFinderStrategyDslContext extends DslContext { - private final ComponentFinderBuilder componentFinderBuilder; + private final ComponentFinderDslContext componentFinderDslContext; private final ComponentFinderStrategyBuilder componentFinderStrategyBuilder = new ComponentFinderStrategyBuilder(); - ComponentFinderStrategyDslContext(ComponentFinderBuilder componentFinderBuilder) { - this.componentFinderBuilder = componentFinderBuilder; + ComponentFinderStrategyDslContext(ComponentFinderDslContext componentFinderDslContext) { + this.componentFinderDslContext = componentFinderDslContext; } @Override @@ -28,9 +27,13 @@ ComponentFinderStrategyBuilder getComponentFinderStrategyBuilder() { return this.componentFinderStrategyBuilder; } + ComponentFinderDslContext getComponentFinderDslContext() { + return this.componentFinderDslContext; + } + @Override void end() { - componentFinderBuilder.withStrategy(componentFinderStrategyBuilder.build()); + componentFinderDslContext.getComponentFinderBuilder().withStrategy(componentFinderStrategyBuilder.build()); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java index 2e600b353..fe765bf44 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java @@ -10,6 +10,12 @@ final class ComponentFinderStrategyForEachDslContext extends DslContext { ComponentFinderStrategyForEachDslContext(ComponentFinderStrategyDslContext dslContext, StructurizrDslParser dslParser) { dslContext.getComponentFinderStrategyBuilder().forEach(component -> { try { + ContainerDslContext containerDslContext = dslContext.getComponentFinderDslContext().getContainerDslContext(); + if (containerDslContext.hasGroup()) { + component.setGroup(containerDslContext.getGroup().getName()); + containerDslContext.getGroup().addElement(component); + } + dslParser.parse(dslLines, new ComponentDslContext(component)); } catch (StructurizrDslParserException e) { throw new RuntimeException(e); @@ -23,7 +29,7 @@ void addLine(String line) { @Override protected String[] getPermittedTokens() { - return new String[] {}; + return new ComponentDslContext(null).getPermittedTokens(); } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index db2d0e034..e8906be8b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -4,7 +4,6 @@ import com.structurizr.Workspace; import com.structurizr.model.*; import com.structurizr.util.StringUtils; -import com.structurizr.util.TagUtils; import com.structurizr.view.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -469,7 +468,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (COMPONENT_FINDER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { if (!restricted) { if (shouldStartContext(tokens)) { - startContext(new ComponentFinderDslContext(this, getContext(ContainerDslContext.class).getContainer())); + startContext(new ComponentFinderDslContext(this, getContext(ContainerDslContext.class))); } } else { throwRestrictedModeException(firstToken); @@ -486,7 +485,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (COMPONENT_FINDER_STRATEGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { if (shouldStartContext(tokens)) { - startContext(new ComponentFinderStrategyDslContext(getContext(ComponentFinderDslContext.class).getComponentFinderBuilder())); + startContext(new ComponentFinderStrategyDslContext(getContext(ComponentFinderDslContext.class))); } } else if (COMPONENT_FINDER_STRATEGY_TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java index 414de072a..ec43b6f57 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java @@ -12,7 +12,7 @@ class ComponentFinderParserTests extends AbstractTests { private final ComponentFinderParser parser = new ComponentFinderParser(); - private final ComponentFinderDslContext context = new ComponentFinderDslContext(null, null); + private final ComponentFinderDslContext context = new ComponentFinderDslContext(null, new ContainerDslContext(null)); @Test void test_parseFilter_ThrowsAnException_WhenNoModeAndTypeAreSpecified() { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index f953aaaee..a136790ee 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.component.ComponentFinderBuilder; import com.structurizr.component.matcher.NameSuffixTypeMatcher; import org.junit.jupiter.api.Test; @@ -12,8 +11,7 @@ class ComponentFinderStrategyParserTests extends AbstractTests { private final ComponentFinderStrategyParser parser = new ComponentFinderStrategyParser(); - private final ComponentFinderBuilder componentFinderBuilder = new ComponentFinderBuilder(); - private final ComponentFinderStrategyDslContext context = new ComponentFinderStrategyDslContext(componentFinderBuilder); + private final ComponentFinderStrategyDslContext context = new ComponentFinderStrategyDslContext(new ComponentFinderDslContext(null, new ContainerDslContext(null))); @Test void test_parseTechnology_ThrowsAnException_WhenThereAreTooFewTokens() { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 920373cf9..d85175225 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1324,6 +1324,7 @@ void springPetClinic() throws Exception { assertEquals("org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java", welcomeController.getUrl()); assertSame(welcomeController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.welcomecontroller")); + assertEquals("Web Controllers", welcomeController.getGroup()); assertTrue(clinicEmployee.hasEfferentRelationshipWith(welcomeController)); Component ownerController = webApplication.getComponentWithName("Owner Controller"); @@ -1332,6 +1333,7 @@ void springPetClinic() throws Exception { assertEquals("org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java", ownerController.getUrl()); assertSame(ownerController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerController")); + assertEquals("Web Controllers", ownerController.getGroup()); assertTrue(clinicEmployee.hasEfferentRelationshipWith(ownerController)); Component petController = webApplication.getComponentWithName("Pet Controller"); @@ -1340,6 +1342,7 @@ void springPetClinic() throws Exception { assertEquals("org/springframework/samples/petclinic/owner/PetController.java", petController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetController.java", petController.getUrl()); assertSame(petController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.petcontroller")); + assertEquals("Web Controllers", petController.getGroup()); assertTrue(clinicEmployee.hasEfferentRelationshipWith(petController)); Component vetController = webApplication.getComponentWithName("Vet Controller"); @@ -1348,6 +1351,7 @@ void springPetClinic() throws Exception { assertEquals("org/springframework/samples/petclinic/vet/VetController.java", vetController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetController.java", vetController.getUrl()); assertSame(vetController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetcontroller")); + assertEquals("Web Controllers", vetController.getGroup()); assertTrue(clinicEmployee.hasEfferentRelationshipWith(vetController)); Component visitController = webApplication.getComponentWithName("Visit Controller"); @@ -1356,6 +1360,7 @@ void springPetClinic() throws Exception { assertEquals("org/springframework/samples/petclinic/owner/VisitController.java", visitController.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java", visitController.getUrl()); assertSame(visitController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.visitcontroller")); + assertEquals("Web Controllers", visitController.getGroup()); assertTrue(clinicEmployee.hasEfferentRelationshipWith(visitController)); Component ownerRepository = webApplication.getComponentWithName("Owner Repository"); @@ -1365,6 +1370,7 @@ void springPetClinic() throws Exception { assertEquals("org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java", ownerRepository.getUrl()); assertSame(ownerRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerrepository")); + assertEquals("Data Repositories", ownerRepository.getGroup()); assertTrue(ownerRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); Component vetRepository = webApplication.getComponentWithName("Vet Repository"); @@ -1374,6 +1380,7 @@ void springPetClinic() throws Exception { assertEquals("org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getProperties().get("component.src")); assertEquals("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java", vetRepository.getUrl()); assertSame(vetRepository, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetrepository")); + assertEquals("Data Repositories", vetRepository.getGroup()); assertTrue(vetRepository.hasEfferentRelationshipWith(relationalDatabaseSchema, "Reads from and writes to")); assertTrue(welcomeController.getRelationships().isEmpty()); diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index 00202026f..8809a0700 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -32,6 +32,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt forEach { clinicEmployee -> this "Uses" tag "Spring MVC Controller" + group "Web Controllers" } } strategy { @@ -42,6 +43,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt forEach { -> relationalDatabaseSchema "Reads from and writes to" tag "Spring Data Repository" + group "Data Repositories" } } } From c6fb6453f8cf1401364703c47222f4132d209238 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:54:27 +0000 Subject: [PATCH 322/418] Improves archetypes support - defaults can be overridden, adds support for relationships. --- .../com/structurizr/model/Relationship.java | 2 +- .../java/com/structurizr/dsl/Archetype.java | 5 +- .../dsl/ComponentArchetypeDslContext.java | 2 +- .../com/structurizr/dsl/ComponentParser.java | 17 +- .../dsl/ContainerArchetypeDslContext.java | 2 +- .../com/structurizr/dsl/ContainerParser.java | 17 +- .../DeploymentNodeArchetypeDslContext.java | 2 +- .../structurizr/dsl/DeploymentNodeParser.java | 24 +-- .../dsl/ElementArchetypeDslContext.java | 9 + .../dsl/ExplicitRelationshipParser.java | 16 +- .../dsl/ImplicitRelationshipParser.java | 16 +- ...InfrastructureNodeArchetypeDslContext.java | 2 +- .../dsl/InfrastructureNodeParser.java | 11 +- .../dsl/PersonArchetypeDslContext.java | 2 +- .../com/structurizr/dsl/PersonParser.java | 11 +- .../dsl/RelationshipArchetypeDslContext.java | 19 ++ .../SoftwareSystemArchetypeDslContext.java | 2 +- .../structurizr/dsl/SoftwareSystemParser.java | 11 +- .../structurizr/dsl/StructurizrDslParser.java | 162 ++++++++---------- .../structurizr/dsl/StructurizrDslTokens.java | 3 + .../structurizr/dsl/ComponentParserTests.java | 17 +- .../structurizr/dsl/ContainerParserTests.java | 13 +- .../dsl/DeploymentNodeParserTests.java | 19 +- .../java/com/structurizr/dsl/DslTests.java | 49 ++++++ .../dsl/ExplicitRelationshipParserTests.java | 23 +-- .../dsl/ImplicitRelationshipParserTests.java | 17 +- .../dsl/InfrastructureNodeParserTests.java | 13 +- .../structurizr/dsl/PersonParserTests.java | 11 +- .../dsl/SoftwareSystemParserTests.java | 11 +- .../resources/dsl/archetypes-for-defaults.dsl | 22 +++ .../dsl/archetypes-for-extension.dsl | 29 ++++ .../src/test/resources/dsl/archetypes.dsl | 10 +- 32 files changed, 354 insertions(+), 215 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/ElementArchetypeDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java create mode 100644 structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl diff --git a/structurizr-core/src/main/java/com/structurizr/model/Relationship.java b/structurizr-core/src/main/java/com/structurizr/model/Relationship.java index 5c3dc2b2e..e8e0fee0f 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Relationship.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Relationship.java @@ -113,7 +113,7 @@ void setDescription(String description) { } /** - * Gets the technology associated with this relationship (e.g. HTTPS, JDBC, etc). + * Gets the technology associated with this relationship (e.g. HTTPS, etc). * * @return the technology as a String, * or null if a technology is not specified diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java index 18ead6aa2..a6781b426 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java @@ -1,7 +1,6 @@ package com.structurizr.dsl; import com.structurizr.util.StringUtils; -import com.structurizr.util.TagUtils; import java.util.LinkedHashSet; import java.util.Set; @@ -10,8 +9,8 @@ final class Archetype { private final String name; private final String type; - private String description; - private String technology; + private String description = ""; + private String technology = ""; private final Set tags = new LinkedHashSet<>(); Archetype(String name, String type) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java index d361f92e7..a204af35c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -final class ComponentArchetypeDslContext extends ArchetypeDslContext { +final class ComponentArchetypeDslContext extends ElementArchetypeDslContext { ComponentArchetypeDslContext(Archetype archetype) { super(archetype); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java index 9940d837a..6600845f1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java @@ -12,7 +12,7 @@ final class ComponentParser extends AbstractParser { private final static int TECHNOLOGY_INDEX = 3; private final static int TAGS_INDEX = 4; - Component parse(ContainerDslContext context, Tokens tokens) { + Component parse(ContainerDslContext context, Tokens tokens, Archetype archetype) { // component [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -35,20 +35,23 @@ Component parse(ContainerDslContext context, Tokens tokens) { component = container.addComponent(name); } + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { - String description = tokens.get(DESCRIPTION_INDEX); - component.setDescription(description); + description = tokens.get(DESCRIPTION_INDEX); } + component.setDescription(description); + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { - String technology = tokens.get(TECHNOLOGY_INDEX); - component.setTechnology(technology); + technology = tokens.get(TECHNOLOGY_INDEX); } + component.setTechnology(technology); + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - String tags = tokens.get(TAGS_INDEX); - component.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + component.addTags(tags); if (context.hasGroup()) { component.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java index 3bd97646a..df4c4d015 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -final class ContainerArchetypeDslContext extends ArchetypeDslContext { +final class ContainerArchetypeDslContext extends ElementArchetypeDslContext { ContainerArchetypeDslContext(Archetype archetype) { super(archetype); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java index eb7f38372..c937849de 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java @@ -12,7 +12,7 @@ final class ContainerParser extends AbstractParser { private final static int TECHNOLOGY_INDEX = 3; private final static int TAGS_INDEX = 4; - Container parse(SoftwareSystemDslContext context, Tokens tokens) { + Container parse(SoftwareSystemDslContext context, Tokens tokens, Archetype archetype) { // container [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -35,20 +35,23 @@ Container parse(SoftwareSystemDslContext context, Tokens tokens) { container = softwareSystem.addContainer(name); } + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { - String description = tokens.get(DESCRIPTION_INDEX); - container.setDescription(description); + description = tokens.get(DESCRIPTION_INDEX); } + container.setDescription(description); + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { - String technology = tokens.get(TECHNOLOGY_INDEX); - container.setTechnology(technology); + technology = tokens.get(TECHNOLOGY_INDEX); } + container.setTechnology(technology); + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - String tags = tokens.get(TAGS_INDEX); - container.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + container.addTags(tags); if (context.hasGroup()) { container.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java index 895cb9a3d..65b411b5c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -final class DeploymentNodeArchetypeDslContext extends ArchetypeDslContext { +final class DeploymentNodeArchetypeDslContext extends ElementArchetypeDslContext { DeploymentNodeArchetypeDslContext(Archetype archetype) { super(archetype); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java index ba88d73af..7e1bb3569 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java @@ -12,7 +12,7 @@ final class DeploymentNodeParser extends AbstractParser { private static final int TAGS_INDEX = 4; private static final int INSTANCES_INDEX = 5; - DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens) { + DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens, Archetype archetype) { // deploymentNode [description] [technology] [tags] [instances] if (tokens.hasMoreThan(INSTANCES_INDEX)) { @@ -26,23 +26,23 @@ DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens) { DeploymentNode deploymentNode = null; String name = tokens.get(NAME_INDEX); - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } - String technology = ""; + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); } deploymentNode = context.getWorkspace().getModel().addDeploymentNode(context.getEnvironment(), name, description, technology); - String tags = ""; + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX); - deploymentNode.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + deploymentNode.addTags(tags); String instances = "1"; if (tokens.includes(INSTANCES_INDEX)) { @@ -58,7 +58,7 @@ DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens) { return deploymentNode; } - DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens) { + DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens, Archetype archetype) { // deploymentNode [description] [technology] [tags] [instances] if (tokens.hasMoreThan(INSTANCES_INDEX)) { @@ -72,12 +72,12 @@ DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens) { DeploymentNode deploymentNode = null; String name = tokens.get(NAME_INDEX); - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } - String technology = ""; + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); } @@ -85,11 +85,11 @@ DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens) { DeploymentNode parent = context.getDeploymentNode(); deploymentNode = parent.addDeploymentNode(name, description, technology); - String tags = ""; + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX); - deploymentNode.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + deploymentNode.addTags(tags); String instances = "1"; if (tokens.includes(INSTANCES_INDEX)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementArchetypeDslContext.java new file mode 100644 index 000000000..40a53783c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementArchetypeDslContext.java @@ -0,0 +1,9 @@ +package com.structurizr.dsl; + +abstract class ElementArchetypeDslContext extends ArchetypeDslContext { + + ElementArchetypeDslContext(Archetype archetype) { + super(archetype); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index 489958f19..f67820aee 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -17,7 +17,7 @@ final class ExplicitRelationshipParser extends AbstractRelationshipParser { private final static int TECHNOLOGY_INDEX = 4; private final static int TAGS_INDEX = 5; - Relationship parse(DslContext context, Tokens tokens) { + Relationship parse(DslContext context, Tokens tokens, Archetype archetype) { // -> [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -40,17 +40,17 @@ Relationship parse(DslContext context, Tokens tokens) { throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); } - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } - String technology = ""; + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); } - String[] tags = new String[0]; + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { tags = tokens.get(TAGS_INDEX).split(","); } @@ -58,7 +58,7 @@ Relationship parse(DslContext context, Tokens tokens) { return createRelationship(sourceElement, description, technology, tags, destinationElement); } - Set parse(ElementsDslContext context, Tokens tokens) { + Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { // -> [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -81,17 +81,17 @@ Set parse(ElementsDslContext context, Tokens tokens) { throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); } - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } - String technology = ""; + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); } - String[] tags = new String[0]; + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { tags = tokens.get(TAGS_INDEX).split(","); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index 19448e25d..e5dd02246 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -15,7 +15,7 @@ final class ImplicitRelationshipParser extends AbstractRelationshipParser { private final static int TECHNOLOGY_INDEX = 3; private final static int TAGS_INDEX = 4; - Relationship parse(ElementDslContext context, Tokens tokens) { + Relationship parse(ElementDslContext context, Tokens tokens, Archetype archetype) { // -> [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -35,17 +35,17 @@ Relationship parse(ElementDslContext context, Tokens tokens) { throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); } - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } - String technology = ""; + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); } - String[] tags = new String[0]; + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { tags = tokens.get(TAGS_INDEX).split(","); } @@ -53,7 +53,7 @@ Relationship parse(ElementDslContext context, Tokens tokens) { return createRelationship(sourceElement, description, technology, tags, destinationElement); } - Set parse(ElementsDslContext context, Tokens tokens) { + Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { // -> [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -72,17 +72,17 @@ Set parse(ElementsDslContext context, Tokens tokens) { throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); } - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } - String technology = ""; + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); } - String[] tags = new String[0]; + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { tags = tokens.get(TAGS_INDEX).split(","); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java index b5023744b..197da828c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -final class InfrastructureNodeArchetypeDslContext extends ArchetypeDslContext { +final class InfrastructureNodeArchetypeDslContext extends ElementArchetypeDslContext { InfrastructureNodeArchetypeDslContext(Archetype archetype) { super(archetype); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java index 9f20ba9a5..126c90776 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java @@ -12,7 +12,7 @@ final class InfrastructureNodeParser extends AbstractParser { private static final int TECHNOLOGY_INDEX = 3; private static final int TAGS_INDEX = 4; - InfrastructureNode parse(DeploymentNodeDslContext context, Tokens tokens) { + InfrastructureNode parse(DeploymentNodeDslContext context, Tokens tokens, Archetype archetype) { // infrastructureNode [description] [technology] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -27,22 +27,23 @@ InfrastructureNode parse(DeploymentNodeDslContext context, Tokens tokens) { InfrastructureNode infrastructureNode; String name = tokens.get(NAME_INDEX); - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } - String technology = ""; + String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); } infrastructureNode = deploymentNode.addInfrastructureNode(name, description, technology); + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - String tags = tokens.get(TAGS_INDEX); - infrastructureNode.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + infrastructureNode.addTags(tags); if (context.hasGroup()) { infrastructureNode.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java index a095ff456..0ecd4b4d1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -final class PersonArchetypeDslContext extends ArchetypeDslContext { +final class PersonArchetypeDslContext extends ElementArchetypeDslContext { PersonArchetypeDslContext(Archetype archetype) { super(archetype); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java index aecc819eb..f16209982 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -10,7 +10,7 @@ final class PersonParser extends AbstractParser { private final static int DESCRIPTION_INDEX = 2; private final static int TAGS_INDEX = 3; - Person parse(ModelDslContext context, Tokens tokens) { + Person parse(ModelDslContext context, Tokens tokens, Archetype archetype) { // person [description] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -32,16 +32,17 @@ Person parse(ModelDslContext context, Tokens tokens) { person = context.getWorkspace().getModel().addPerson(name); } - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); - person.setDescription(description); } + person.setDescription(description); + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - String tags = tokens.get(TAGS_INDEX); - person.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + person.addTags(tags); if (context.hasGroup()) { person.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java new file mode 100644 index 000000000..4bd231e68 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java @@ -0,0 +1,19 @@ +package com.structurizr.dsl; + +final class RelationshipArchetypeDslContext extends ArchetypeDslContext { + + RelationshipArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java index 8df3f4f60..5170b064a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -final class SoftwareSystemArchetypeDslContext extends ArchetypeDslContext { +final class SoftwareSystemArchetypeDslContext extends ElementArchetypeDslContext { SoftwareSystemArchetypeDslContext(Archetype archetype) { super(archetype); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java index 243cd6d6c..cdbef98a5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -10,7 +10,7 @@ final class SoftwareSystemParser extends AbstractParser { private final static int DESCRIPTION_INDEX = 2; private final static int TAGS_INDEX = 3; - SoftwareSystem parse(ModelDslContext context, Tokens tokens) { + SoftwareSystem parse(ModelDslContext context, Tokens tokens, Archetype archetype) { // softwareSystem [description] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -32,16 +32,17 @@ SoftwareSystem parse(ModelDslContext context, Tokens tokens) { softwareSystem = context.getWorkspace().getModel().addSoftwareSystem(name); } - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); - softwareSystem.setDescription(description); } + softwareSystem.setDescription(description); + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - String tags = tokens.get(TAGS_INDEX); - softwareSystem.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + softwareSystem.addTags(tags); if (context.hasGroup()) { softwareSystem.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index e8906be8b..8423e6ac6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -53,7 +53,8 @@ public final class StructurizrDslParser extends StructurizrDslTokens { StructurizrDslTokens.CONTAINER_TOKEN, new HashMap<>(), StructurizrDslTokens.COMPONENT_TOKEN, new HashMap<>(), StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, new HashMap<>(), - StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, new HashMap<>() + StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, new HashMap<>(), + StructurizrDslTokens.RELATIONSHIP_TOKEN, new HashMap<>() ); private final List dslSourceLines = new ArrayList<>(); @@ -315,8 +316,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (inContext(ExternalScriptDslContext.class)) { new ScriptParser().parseParameter(getContext(ExternalScriptDslContext.class), tokens); - } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) { - Relationship relationship = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken()); + } else if (tokens.size() > 2 && isRelationshipKeywordOrArchetype(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) { + // explicit without archetype: a -> b + // explicit with archetype: a --https-> b + Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); + Relationship relationship = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new RelationshipDslContext(relationship)); @@ -324,8 +328,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, relationship); - } else if (tokens.size() >= 2 && RELATIONSHIP_TOKEN.equals(tokens.get(0)) && inContext(ElementDslContext.class)) { - Relationship relationship = new ImplicitRelationshipParser().parse(getContext(ElementDslContext.class), tokens.withoutContextStartToken()); + } else if (tokens.size() >= 2 && isRelationshipKeywordOrArchetype(tokens.get(0)) && inContext(ElementDslContext.class)) { + // implicit without archetype: -> this + // implicit with archetype: --https-> this + Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); + Relationship relationship = new ImplicitRelationshipParser().parse(getContext(ElementDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new RelationshipDslContext(relationship)); @@ -333,15 +340,17 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, relationship); - } else if (tokens.size() > 2 && RELATIONSHIP_TOKEN.equals(tokens.get(1)) && inContext(ElementsDslContext.class)) { - Set relationships = new ExplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + } else if (tokens.size() > 2 && isRelationshipKeywordOrArchetype(tokens.get(1)) && inContext(ElementsDslContext.class)) { + Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); + Set relationships = new ExplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new RelationshipsDslContext(getContext(), relationships)); } - } else if (tokens.size() >= 2 && RELATIONSHIP_TOKEN.equals(tokens.get(0)) && inContext(ElementsDslContext.class)) { - Set relationships = new ImplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken()); + } else if (tokens.size() >= 2 && isRelationshipKeywordOrArchetype(tokens.get(0)) && inContext(ElementsDslContext.class)) { + Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); + Set relationships = new ImplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new RelationshipsDslContext(getContext(), relationships)); @@ -418,10 +427,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, customElement); } else if (isElementKeywordOrArchetype(firstToken, PERSON_TOKEN) && (inContext(ModelDslContext.class))) { - Person person = new PersonParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); - if (features.isEnabled(Features.ARCHETYPES)) { - applyArchetype(firstToken, person); - } + Archetype archetype = getArchetype(PERSON_TOKEN, firstToken); + Person person = new PersonParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new PersonDslContext(person)); @@ -430,10 +437,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, person); } else if (isElementKeywordOrArchetype(firstToken, SOFTWARE_SYSTEM_TOKEN) && (inContext(ModelDslContext.class))) { - SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); - if (features.isEnabled(Features.ARCHETYPES)) { - applyArchetype(firstToken, softwareSystem); - } + Archetype archetype = getArchetype(SOFTWARE_SYSTEM_TOKEN, firstToken); + SoftwareSystem softwareSystem = new SoftwareSystemParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new SoftwareSystemDslContext(softwareSystem)); @@ -442,10 +447,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, softwareSystem); } else if (isElementKeywordOrArchetype(firstToken, CONTAINER_TOKEN) && inContext(SoftwareSystemDslContext.class)) { - Container container = new ContainerParser().parse(getContext(SoftwareSystemDslContext.class), tokens.withoutContextStartToken()); - if (features.isEnabled(Features.ARCHETYPES)) { - applyArchetype(firstToken, container); - } + Archetype archetype = getArchetype(CONTAINER_TOKEN, firstToken); + Container container = new ContainerParser().parse(getContext(SoftwareSystemDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new ContainerDslContext(container)); @@ -454,10 +457,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, container); } else if (isElementKeywordOrArchetype(firstToken, COMPONENT_TOKEN) && inContext(ContainerDslContext.class)) { - Component component = new ComponentParser().parse(getContext(ContainerDslContext.class), tokens.withoutContextStartToken()); - if (features.isEnabled(Features.ARCHETYPES)) { - applyArchetype(firstToken, component); - } + Archetype archetype = getArchetype(COMPONENT_TOKEN, firstToken); + Component component = new ComponentParser().parse(getContext(ContainerDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new ComponentDslContext(component)); @@ -723,10 +724,19 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new InfrastructureNodeArchetypeDslContext(archetype)); } + } else if (isRelationshipKeywordOrArchetype(firstToken) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, RELATIONSHIP_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipArchetypeDslContext(archetype)); + } + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { new ArchetypeParser().parseDescription(getContext(ArchetypeDslContext.class), tokens); - } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ContainerArchetypeDslContext.class) || inContext(ComponentArchetypeDslContext.class) || inContext(DeploymentNodeArchetypeDslContext.class) || inContext(InfrastructureNodeArchetypeDslContext.class))) { + } else if (TECHNOLOGY_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ContainerArchetypeDslContext.class) || inContext(ComponentArchetypeDslContext.class) || inContext(DeploymentNodeArchetypeDslContext.class) || inContext(InfrastructureNodeArchetypeDslContext.class) || inContext(RelationshipArchetypeDslContext.class))) { new ArchetypeParser().parseTechnology(getContext(ArchetypeDslContext.class), tokens); } else if (TAG_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { @@ -844,10 +854,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, new DeploymentGroup(group)); } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { - DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens.withoutContextStartToken()); - if (features.isEnabled(Features.ARCHETYPES)) { - applyArchetype(firstToken, deploymentNode); - } + Archetype archetype = getArchetype(DEPLOYMENT_NODE_TOKEN, firstToken); + DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new DeploymentNodeDslContext(deploymentNode)); @@ -855,10 +863,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, deploymentNode); } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { - DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); - if (features.isEnabled(Features.ARCHETYPES)) { - applyArchetype(firstToken, deploymentNode); - } + Archetype archetype = getArchetype(DEPLOYMENT_NODE_TOKEN, firstToken); + DeploymentNode deploymentNode = new DeploymentNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new DeploymentNodeDslContext(deploymentNode)); @@ -866,10 +872,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, deploymentNode); } else if (isElementKeywordOrArchetype(firstToken, INFRASTRUCTURE_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { - InfrastructureNode infrastructureNode = new InfrastructureNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); - if (features.isEnabled(Features.ARCHETYPES)) { - applyArchetype(firstToken, infrastructureNode); - } + Archetype archetype = getArchetype(INFRASTRUCTURE_NODE_TOKEN, firstToken); + InfrastructureNode infrastructureNode = new InfrastructureNodeParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new InfrastructureNodeDslContext(infrastructureNode)); @@ -1310,69 +1314,49 @@ private boolean isElementKeywordOrArchetype(String token, String keyword) { } } - private void addArchetype(Archetype archetype) { - archetypes.get(archetype.getType()).put(archetype.getName(), archetype); - } - - private void extendArchetype(Archetype archetype, String archetypeName) { - archetypeName = archetypeName.toLowerCase(); - Archetype parentArchetype = archetypes.get(archetype.getType()).get(archetypeName); - if (parentArchetype != null) { - archetype.setDescription(parentArchetype.getDescription()); - archetype.setTechnology(parentArchetype.getTechnology()); - archetype.addTags(parentArchetype.getTags().toArray(new String[0])); + private boolean isRelationshipKeywordOrArchetype(String token) { + if (features.isEnabled(Features.ARCHETYPES)) { + if (token.equalsIgnoreCase(RELATIONSHIP_TOKEN)) { + return true; + } else if (token.startsWith(RELATIONSHIP_ARCHETYPE_PREFIX) && token.endsWith(RELATIONSHIP_ARCHETYPE_SUFFIX)) { + token = token.substring(RELATIONSHIP_ARCHETYPE_PREFIX.length(), token.length()-RELATIONSHIP_ARCHETYPE_SUFFIX.length()); + return (archetypes.get(RELATIONSHIP_TOKEN).containsKey(token.toLowerCase())); + } } - } - private void applyArchetype(String archetypeName, Person person) { - Archetype archetype = archetypes.get(StructurizrDslTokens.PERSON_TOKEN).get(archetypeName.toLowerCase()); - if (archetype != null) { - person.setDescription(archetype.getDescription()); - person.addTags(archetype.getTags().toArray(new String[0])); - } + return token.equalsIgnoreCase(RELATIONSHIP_TOKEN); } - private void applyArchetype(String archetypeName, SoftwareSystem softwareSystem) { - Archetype archetype = archetypes.get(StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN).get(archetypeName.toLowerCase()); - if (archetype != null) { - softwareSystem.setDescription(archetype.getDescription()); - softwareSystem.addTags(archetype.getTags().toArray(new String[0])); - } + private void addArchetype(Archetype archetype) { + archetypes.get(archetype.getType()).put(archetype.getName(), archetype); } - private void applyArchetype(String archetypeName, Container container) { - Archetype archetype = archetypes.get(StructurizrDslTokens.CONTAINER_TOKEN).get(archetypeName.toLowerCase()); - if (archetype != null) { - container.setTechnology(archetype.getTechnology()); - container.setDescription(archetype.getDescription()); - container.addTags(archetype.getTags().toArray(new String[0])); - } - } + private Archetype getArchetype(String archetypeType, String archetypeName) { + Archetype archetype = null; - private void applyArchetype(String archetypeName, Component component) { - Archetype archetype = archetypes.get(StructurizrDslTokens.COMPONENT_TOKEN).get(archetypeName.toLowerCase()); - if (archetype != null) { - component.setTechnology(archetype.getTechnology()); - component.setDescription(archetype.getDescription()); - component.addTags(archetype.getTags().toArray(new String[0])); + if (features.isEnabled(Features.ARCHETYPES)) { + if (RELATIONSHIP_TOKEN.equals(archetypeType)) { + if (archetypeName.startsWith(RELATIONSHIP_ARCHETYPE_PREFIX) && archetypeName.endsWith(RELATIONSHIP_ARCHETYPE_SUFFIX)) { + archetypeName = archetypeName.substring(RELATIONSHIP_ARCHETYPE_PREFIX.length(), archetypeName.length() - RELATIONSHIP_ARCHETYPE_SUFFIX.length()); + } + } + archetype = archetypes.get(archetypeType).get(archetypeName.toLowerCase()); } - } - private void applyArchetype(String archetypeName, DeploymentNode deploymentNode) { - Archetype archetype = archetypes.get(StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN).get(archetypeName.toLowerCase()); - if (archetype != null) { - deploymentNode.setTechnology(archetype.getTechnology()); - deploymentNode.setDescription(archetype.getDescription()); - deploymentNode.addTags(archetype.getTags().toArray(new String[0])); + if (archetype == null) { + archetype = new Archetype(archetypeName, archetypeType); } + + return archetype; } - private void applyArchetype(String archetypeName, InfrastructureNode infrastructureNode) { - Archetype archetype = archetypes.get(StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN).get(archetypeName.toLowerCase()); - if (archetype != null) { - infrastructureNode.setTechnology(archetype.getTechnology()); - infrastructureNode.setDescription(archetype.getDescription()); - infrastructureNode.addTags(archetype.getTags().toArray(new String[0])); + private void extendArchetype(Archetype archetype, String archetypeName) { + archetypeName = archetypeName.toLowerCase(); + Archetype parentArchetype = archetypes.get(archetype.getType()).get(archetypeName); + if (parentArchetype != null) { + archetype.setDescription(parentArchetype.getDescription()); + archetype.setTechnology(parentArchetype.getTechnology()); + archetype.addTags(parentArchetype.getTags().toArray(new String[0])); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index f44de980b..fef341aad 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -132,4 +132,7 @@ class StructurizrDslTokens { static final String COMPONENT_FINDER_STRATEGY_URL_TOKEN = "url"; static final String COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN = "forEach"; + static final String RELATIONSHIP_ARCHETYPE_PREFIX = "--"; + static final String RELATIONSHIP_ARCHETYPE_SUFFIX = "->"; + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java index 77e4993f2..77ddc23fa 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java @@ -10,11 +10,12 @@ class ComponentParserTests extends AbstractTests { private ComponentParser parser = new ComponentParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(new ContainerDslContext(null), tokens("container", "name", "description", "technology", "tags", "extra")); + parser.parse(new ContainerDslContext(null), tokens("container", "name", "description", "technology", "tags", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: component [description] [technology] [tags]", e.getMessage()); @@ -24,7 +25,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { try { - parser.parse(new ContainerDslContext(null), tokens("container")); + parser.parse(new ContainerDslContext(null), tokens("container"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: component [description] [technology] [tags]", e.getMessage()); @@ -36,13 +37,13 @@ void test_parse_CreatesAComponent() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); ContainerDslContext context = new ContainerDslContext(container); - parser.parse(context, tokens("component", "Name")); + parser.parse(context, tokens("component", "Name"), archetype); assertEquals(3, model.getElements().size()); Component component = container.getComponentWithName("Name"); assertNotNull(component); assertEquals("", component.getDescription()); - assertEquals(null, component.getTechnology()); + assertEquals("", component.getTechnology()); assertEquals("Element,Component", component.getTags()); } @@ -51,13 +52,13 @@ void test_parse_CreatesAComponentWithADescription() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); ContainerDslContext context = new ContainerDslContext(container); - parser.parse(context, tokens("component", "Name", "Description")); + parser.parse(context, tokens("component", "Name", "Description"), archetype); assertEquals(3, model.getElements().size()); Component component = container.getComponentWithName("Name"); assertNotNull(component); assertEquals("Description", component.getDescription()); - assertEquals(null, component.getTechnology()); + assertEquals("", component.getTechnology()); assertEquals("Element,Component", component.getTags()); } @@ -66,7 +67,7 @@ void test_parse_CreatesAComponentWithADescriptionAndTechnology() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); ContainerDslContext context = new ContainerDslContext(container); - parser.parse(context, tokens("component", "Name", "Description", "Technology")); + parser.parse(context, tokens("component", "Name", "Description", "Technology"), archetype); assertEquals(3, model.getElements().size()); Component component = container.getComponentWithName("Name"); @@ -81,7 +82,7 @@ void test_parse_CreatesAComponentWithADescriptionAndTechnologyAndTags() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); ContainerDslContext context = new ContainerDslContext(container); - parser.parse(context, tokens("component", "Name", "Description", "Technology", "Tag 1, Tag 2")); + parser.parse(context, tokens("component", "Name", "Description", "Technology", "Tag 1, Tag 2"), archetype); assertEquals(3, model.getElements().size()); Component component = container.getComponentWithName("Name"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java index 79fc43a77..758f7503c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java @@ -9,11 +9,12 @@ class ContainerParserTests extends AbstractTests { private ContainerParser parser = new ContainerParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(new SoftwareSystemDslContext(null), tokens("container", "name", "description", "technology", "tags", "extra")); + parser.parse(new SoftwareSystemDslContext(null), tokens("container", "name", "description", "technology", "tags", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: container [description] [technology] [tags]", e.getMessage()); @@ -23,7 +24,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { try { - parser.parse(new SoftwareSystemDslContext(null), tokens("container")); + parser.parse(new SoftwareSystemDslContext(null), tokens("container"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: container [description] [technology] [tags]", e.getMessage()); @@ -34,7 +35,7 @@ void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { void test_parse_CreatesAContainer() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); - parser.parse(context, tokens("container", "Name")); + parser.parse(context, tokens("container", "Name"), archetype); assertEquals(2, model.getElements().size()); Container container = softwareSystem.getContainerWithName("Name"); @@ -48,7 +49,7 @@ void test_parse_CreatesAContainer() { void test_parse_CreatesAContainerWithADescription() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); - parser.parse(context, tokens("container", "Name", "Description")); + parser.parse(context, tokens("container", "Name", "Description"), archetype); assertEquals(2, model.getElements().size()); Container container = softwareSystem.getContainerWithName("Name"); @@ -62,7 +63,7 @@ void test_parse_CreatesAContainerWithADescription() { void test_parse_CreatesAContainerWithADescriptionAndTechnology() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); - parser.parse(context, tokens("container", "Name", "Description", "Technology")); + parser.parse(context, tokens("container", "Name", "Description", "Technology"), archetype); assertEquals(2, model.getElements().size()); Container container = softwareSystem.getContainerWithName("Name"); @@ -76,7 +77,7 @@ void test_parse_CreatesAContainerWithADescriptionAndTechnology() { void test_parse_CreatesAContainerWithADescriptionAndTechnologyAndTags() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); - parser.parse(context, tokens("container", "Name", "Description", "Technology", "Tag 1, Tag 2")); + parser.parse(context, tokens("container", "Name", "Description", "Technology", "Tag 1, Tag 2"), archetype); assertEquals(2, model.getElements().size()); Container container = softwareSystem.getContainerWithName("Name"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java index d9ec9be15..21bbd052c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java @@ -9,11 +9,12 @@ class DeploymentNodeParserTests extends AbstractTests { private DeploymentNodeParser parser = new DeploymentNodeParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(new DeploymentEnvironmentDslContext("env"), tokens("deploymentNode", "name", "description", "technology", "tags", "instances", "extra")); + parser.parse(new DeploymentEnvironmentDslContext("env"), tokens("deploymentNode", "name", "description", "technology", "tags", "instances", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: deploymentNode [description] [technology] [tags] [instances] {", e.getMessage()); @@ -23,7 +24,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { try { - parser.parse(new DeploymentEnvironmentDslContext("env"), tokens("deploymentNode")); + parser.parse(new DeploymentEnvironmentDslContext("env"), tokens("deploymentNode"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: deploymentNode [description] [technology] [tags] [instances] {", e.getMessage()); @@ -34,7 +35,7 @@ void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { void test_parse_CreatesADeploymentNode() { DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); context.setWorkspace(workspace); - parser.parse(context, tokens("deploymentNode", "Name")); + parser.parse(context, tokens("deploymentNode", "Name"), archetype); assertEquals(1, model.getElements().size()); DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); @@ -50,7 +51,7 @@ void test_parse_CreatesADeploymentNode() { void test_parse_CreatesADeploymentNodeWithADescription() { DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); context.setWorkspace(workspace); - parser.parse(context, tokens("deploymentNode", "Name", "Description")); + parser.parse(context, tokens("deploymentNode", "Name", "Description"), archetype); assertEquals(1, model.getElements().size()); DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); @@ -66,7 +67,7 @@ void test_parse_CreatesADeploymentNodeWithADescription() { void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnology() { DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); context.setWorkspace(workspace); - parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology")); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology"), archetype); assertEquals(1, model.getElements().size()); DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); @@ -82,7 +83,7 @@ void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnology() { void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnologyAndTags() { DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); context.setWorkspace(workspace); - parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2")); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2"), archetype); assertEquals(1, model.getElements().size()); DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); @@ -98,7 +99,7 @@ void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnologyAndTags() { void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnologyAndTagsAndInstances() { DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); context.setWorkspace(workspace); - parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "8")); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "8"), archetype); assertEquals(1, model.getElements().size()); DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); @@ -116,7 +117,7 @@ void test_parse_ThrowsAnException_WhenTheNumberOfInstancesIsNotValid() { context.setWorkspace(workspace); try { - parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "abc")); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "abc"), archetype); System.out.println(model.getDeploymentNodes().iterator().next().getInstances()); fail(); } catch (Exception e) { @@ -129,7 +130,7 @@ void test_parse_CreatesAChildDeploymentNode() { DeploymentNode parent = model.addDeploymentNode("Live", "Parent", "Description", "Technology"); DeploymentNodeDslContext context = new DeploymentNodeDslContext(parent); context.setWorkspace(workspace); - parser.parse(context, tokens("deploymentNode", "Name")); + parser.parse(context, tokens("deploymentNode", "Name"), archetype); assertEquals(2, model.getElements().size()); DeploymentNode deploymentNode = parent.getDeploymentNodeWithName("Name"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index d85175225..4b33251a2 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1483,6 +1483,55 @@ void test_archetypes_WhenEnabled() throws Exception { assertTrue(customerApi.getTagsAsSet().contains("Application")); assertTrue(customerApi.getTagsAsSet().contains("Spring Boot")); assertEquals("Spring Boot", customerApi.getTechnology()); + + Relationship relationship = workspace.getModel().getSoftwareSystemWithName("A").getEfferentRelationshipWith(workspace.getModel().getSoftwareSystemWithName("X")); + assertEquals("HTTPS", relationship.getTechnology()); + } + + @Test + void test_archetypesForDefaults() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/archetypes-for-defaults.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.getFeatures().enable(Features.ARCHETYPES); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + + SoftwareSystem a = workspace.getModel().getSoftwareSystemWithName("A"); + assertEquals("Default Description", a.getDescription()); + assertTrue(a.hasTag("Default Tag")); + + SoftwareSystem b = workspace.getModel().getSoftwareSystemWithName("B"); + assertEquals("Default Description", b.getDescription()); + assertTrue(b.hasTag("Default Tag")); + + Relationship r = a.getEfferentRelationshipWith(b); + assertEquals("Default Description", r.getDescription()); + assertEquals("Default Technology", r.getTechnology()); + assertTrue(r.hasTag("Default Tag")); + } + + @Test + void test_archetypesForExtension() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/archetypes-for-extension.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.getFeatures().enable(Features.ARCHETYPES); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + + SoftwareSystem a = workspace.getModel().getSoftwareSystemWithName("A"); + assertEquals("Description of A.", a.getDescription()); + assertTrue(a.hasTag("Default Tag")); + + SoftwareSystem b = workspace.getModel().getSoftwareSystemWithName("B"); + assertEquals("Description of B.", b.getDescription()); + assertTrue(b.hasTag("Default Tag")); + assertTrue(b.hasTag("External Software System")); + + Relationship r = a.getEfferentRelationshipWith(b); + assertEquals("Makes API calls to", r.getDescription()); + assertEquals("HTTPS", r.getTechnology()); + assertTrue(r.hasTag("Default Tag")); + assertTrue(r.hasTag("HTTPS")); } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java index 1c9b5c401..0ac671673 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java @@ -8,11 +8,12 @@ class ExplicitRelationshipParserTests extends AbstractTests { private ExplicitRelationshipParser parser = new ExplicitRelationshipParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(), tokens("source", "->", "destination", "description", "technology", "tags", "extra")); + parser.parse(context(), tokens("source", "->", "destination", "description", "technology", "tags", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: -> [description] [technology] [tags]", e.getMessage()); @@ -22,7 +23,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { try { - parser.parse(context(), tokens("source", "->")); + parser.parse(context(), tokens("source", "->"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: -> [description] [technology] [tags]", e.getMessage()); @@ -32,7 +33,7 @@ void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { @Test void test_parse_ThrowsAnException_WhenTheSourceElementIsNotDefined() { try { - parser.parse(context(), tokens("source", "->", "destination")); + parser.parse(context(), tokens("source", "->", "destination"), archetype); fail(); } catch (Exception e) { assertEquals("The source element \"source\" does not exist", e.getMessage()); @@ -47,7 +48,7 @@ void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { context.setIdentifierRegister(elements); try { - parser.parse(context, tokens("source", "->", "destination")); + parser.parse(context, tokens("source", "->", "destination"), archetype); fail(); } catch (Exception e) { assertEquals("The destination element \"destination\" does not exist", e.getMessage()); @@ -67,7 +68,7 @@ void test_parse_AddsTheRelationship() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("source", "->", "destination")); + parser.parse(context, tokens("source", "->", "destination"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -91,7 +92,7 @@ void test_parse_AddsTheRelationshipWithADescription() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("source", "->", "destination", "Uses")); + parser.parse(context, tokens("source", "->", "destination", "Uses"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -115,7 +116,7 @@ void test_parse_AddsTheRelationshipWithADescriptionAndTechnology() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP")); + parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -138,7 +139,7 @@ void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTags() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP", "Tag 1,Tag 2"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -163,7 +164,7 @@ void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTe assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP", "Tag 1,Tag 2"), archetype); assertEquals(2, model.getRelationships().size()); // this is the relationship that was created @@ -195,7 +196,7 @@ void test_parse_AddsTheRelationship_WithASourceOfThis() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("this", "->", "destination")); + parser.parse(context, tokens("this", "->", "destination"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -218,7 +219,7 @@ void test_parse_AddsTheRelationship_WithADestinationOfThis() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("source", "->", "this")); + parser.parse(context, tokens("source", "->", "this"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java index c0786cf19..a84c7fd06 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java @@ -8,6 +8,7 @@ class ImplicitRelationshipParserTests extends AbstractTests { private ImplicitRelationshipParser parser = new ImplicitRelationshipParser(); + private Archetype archetype = new Archetype("name", "type"); private ElementDslContext context(Person person) { PersonDslContext context = new PersonDslContext(person); @@ -20,7 +21,7 @@ private ElementDslContext context(Person person) { @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse((ElementDslContext)null, tokens("->", "destination", "description", "technology", "tags", "extra")); + parser.parse((ElementDslContext)null, tokens("->", "destination", "description", "technology", "tags", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: -> [description] [technology] [tags]", e.getMessage()); @@ -30,7 +31,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { try { - parser.parse((ElementDslContext)null, tokens("->")); + parser.parse((ElementDslContext)null, tokens("->"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: -> [description] [technology] [tags]", e.getMessage()); @@ -45,7 +46,7 @@ void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { context.setIdentifierRegister(elements); try { - parser.parse(context, tokens("->", "destination")); + parser.parse(context, tokens("->", "destination"), archetype); fail(); } catch (Exception e) { assertEquals("The destination element \"destination\" does not exist", e.getMessage()); @@ -64,7 +65,7 @@ void test_parse_AddsTheRelationship() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("->", "destination")); + parser.parse(context, tokens("->", "destination"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -87,7 +88,7 @@ void test_parse_AddsTheRelationshipWithADescription() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("->", "destination", "Uses")); + parser.parse(context, tokens("->", "destination", "Uses"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -110,7 +111,7 @@ void test_parse_AddsTheRelationshipWithADescriptionAndTechnology() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("->", "destination", "Uses", "HTTP")); + parser.parse(context, tokens("->", "destination", "Uses", "HTTP"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -132,7 +133,7 @@ void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTags() { assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + parser.parse(context, tokens("->", "destination", "Uses", "HTTP", "Tag 1,Tag 2"), archetype); assertEquals(1, model.getRelationships().size()); Relationship r = model.getRelationships().iterator().next(); @@ -156,7 +157,7 @@ void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTe assertEquals(0, model.getRelationships().size()); - parser.parse(context, tokens("->", "destination", "Uses", "HTTP", "Tag 1,Tag 2")); + parser.parse(context, tokens("->", "destination", "Uses", "HTTP", "Tag 1,Tag 2"), archetype); assertEquals(2, model.getRelationships().size()); // this is the relationship that was created diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java index 9c0aa769c..d631709cc 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java @@ -9,11 +9,12 @@ class InfrastructureNodeParserTests extends AbstractTests { private InfrastructureNodeParser parser = new InfrastructureNodeParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(new DeploymentNodeDslContext(null), tokens("infrastructureNode", "name", "description", "technology", "tags", "extra")); + parser.parse(new DeploymentNodeDslContext(null), tokens("infrastructureNode", "name", "description", "technology", "tags", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: infrastructureNode [description] [technology] [tags]", e.getMessage()); @@ -23,7 +24,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { try { - parser.parse(new DeploymentNodeDslContext(null), tokens("infrastructureNode")); + parser.parse(new DeploymentNodeDslContext(null), tokens("infrastructureNode"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: infrastructureNode [description] [technology] [tags]", e.getMessage()); @@ -35,7 +36,7 @@ void test_parse_CreatesAnInfrastructureNode() { DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); - parser.parse(context, tokens("infrastructureNode", "Name")); + parser.parse(context, tokens("infrastructureNode", "Name"), archetype); assertEquals(2, model.getElements().size()); assertEquals(1, deploymentNode.getInfrastructureNodes().size()); @@ -52,7 +53,7 @@ void test_parse_CreatesAnInfrastructureNodeWithADescription() { DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); - parser.parse(context, tokens("infrastructureNode", "Name", "Description")); + parser.parse(context, tokens("infrastructureNode", "Name", "Description"), archetype); assertEquals(2, model.getElements().size()); assertEquals(1, deploymentNode.getInfrastructureNodes().size()); @@ -69,7 +70,7 @@ void test_parse_CreatesAnInfrastructureNodeWithADescriptionAndTechnology() { DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); - parser.parse(context, tokens("infrastructureNode", "Name", "Description", "Technology")); + parser.parse(context, tokens("infrastructureNode", "Name", "Description", "Technology"), archetype); assertEquals(2, model.getElements().size()); assertEquals(1, deploymentNode.getInfrastructureNodes().size()); @@ -86,7 +87,7 @@ void test_parse_CreatesAnInfrastructureNodeWithADescriptionAndTechnologyAndTags( DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); - parser.parse(context, tokens("infrastructureNode", "Name", "Description", "Technology", "Tag 1, Tag 2")); + parser.parse(context, tokens("infrastructureNode", "Name", "Description", "Technology", "Tag 1, Tag 2"), archetype); assertEquals(2, model.getElements().size()); assertEquals(1, deploymentNode.getInfrastructureNodes().size()); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java index ed6454623..dac705b99 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java @@ -8,11 +8,12 @@ class PersonParserTests extends AbstractTests { private final PersonParser parser = new PersonParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(), tokens("person", "name", "description", "tags", "tokens")); + parser.parse(context(), tokens("person", "name", "description", "tags", "tokens"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: person [description] [tags]", e.getMessage()); @@ -22,7 +23,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { try { - parser.parse(context(), tokens("person")); + parser.parse(context(), tokens("person"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: person [description] [tags]", e.getMessage()); @@ -31,7 +32,7 @@ void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { @Test void test_parse_CreatesAPerson() { - parser.parse(context(), tokens("person", "User")); + parser.parse(context(), tokens("person", "User"), archetype); assertEquals(1, model.getElements().size()); Person user = model.getPersonWithName("User"); @@ -42,7 +43,7 @@ void test_parse_CreatesAPerson() { @Test void test_parse_CreatesAPersonWithADescription() { - parser.parse(context(), tokens("person", "User", "Description")); + parser.parse(context(), tokens("person", "User", "Description"), archetype); assertEquals(1, model.getElements().size()); Person user = model.getPersonWithName("User"); @@ -53,7 +54,7 @@ void test_parse_CreatesAPersonWithADescription() { @Test void test_parse_CreatesAPersonWithADescriptionAndTags() { - parser.parse(context(), tokens("person", "User", "Description", "Tag 1, Tag 2")); + parser.parse(context(), tokens("person", "User", "Description", "Tag 1, Tag 2"), archetype); assertEquals(1, model.getElements().size()); Person user = model.getPersonWithName("User"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java index d11724335..b33974520 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java @@ -8,11 +8,12 @@ class SoftwareSystemParserTests extends AbstractTests { private final SoftwareSystemParser parser = new SoftwareSystemParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(), tokens("softwareSystem", "name", "description", "tags", "extra")); + parser.parse(context(), tokens("softwareSystem", "name", "description", "tags", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: softwareSystem [description] [tags]", e.getMessage()); @@ -22,7 +23,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { try { - parser.parse(context(), tokens("softwareSystem")); + parser.parse(context(), tokens("softwareSystem"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: softwareSystem [description] [tags]", e.getMessage()); @@ -31,7 +32,7 @@ void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { @Test void test_parse_CreatesASoftwareSystem() { - parser.parse(context(), tokens("softwareSystem", "Name")); + parser.parse(context(), tokens("softwareSystem", "Name"), archetype); assertEquals(1, model.getElements().size()); SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); @@ -42,7 +43,7 @@ void test_parse_CreatesASoftwareSystem() { @Test void test_parse_CreatesASoftwareSystemWithADescription() { - parser.parse(context(), tokens("softwareSystem", "Name", "Description")); + parser.parse(context(), tokens("softwareSystem", "Name", "Description"), archetype); assertEquals(1, model.getElements().size()); SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); @@ -53,7 +54,7 @@ void test_parse_CreatesASoftwareSystemWithADescription() { @Test void test_parse_CreatesASoftwareSystemWithADescriptionAndTags() { - parser.parse(context(), tokens("softwareSystem", "Name", "Description", "Tag 1, Tag 2")); + parser.parse(context(), tokens("softwareSystem", "Name", "Description", "Tag 1, Tag 2"), archetype); assertEquals(1, model.getElements().size()); SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl new file mode 100644 index 000000000..63ed7ce07 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl @@ -0,0 +1,22 @@ +workspace { + + model { + archetypes { + softwaresystem { + description "Default Description" + tag "Default Tag" + } + + -> { + description "Default Description" + technology "Default Technology" + tag "Default Tag" + } + } + + a = softwareSystem "A" + b = softwareSystem "B" + a -> b + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl new file mode 100644 index 000000000..ac5116163 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl @@ -0,0 +1,29 @@ +workspace { + + model { + archetypes { + softwaresystem { + tag "Default Tag" + } + + externalSoftwareSystem = softwareSystem { + tag "External Software System" + } + + -> { + technology "Default Technology" + tag "Default Tag" + } + + https = -> { + technology "HTTPS" + tag "HTTPS" + } + } + + a = softwareSystem "A" "Description of A." + b = externalSoftwareSystem "B" "Description of B." + a --https-> b "Makes API calls to" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes.dsl index d7524e06e..d892dcb5d 100644 --- a/structurizr-dsl/src/test/resources/dsl/archetypes.dsl +++ b/structurizr-dsl/src/test/resources/dsl/archetypes.dsl @@ -24,13 +24,21 @@ workspace { technology "Spring Data Repository" tag "Spring Data Repository" } + + https = -> { + technology "HTTPS" + } } + a = softwareSystem "A" + x = softwareSystem "X" { customerService = microservice "Customer Service" { db = datastore "Customer database" api = springBootApplication "Customer API" { - customerController = restController "Customer Controller" + customerController = restController "Customer Controller" { + a --https-> this "Makes API calls using" + } customerRepository = repository "Customer Repository" { customerController -> this this -> db From 1d23a0d62648def03a10737547d2dd160c199e6e Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:44:00 +0000 Subject: [PATCH 323/418] Adds properties to archetypes. --- .../java/com/structurizr/model/ModelItem.java | 11 ++++++ .../java/com/structurizr/dsl/Archetype.java | 37 +++++++++++++++++-- .../dsl/ComponentArchetypeDslContext.java | 1 + .../com/structurizr/dsl/ComponentParser.java | 2 + .../dsl/ContainerArchetypeDslContext.java | 1 + .../com/structurizr/dsl/ContainerParser.java | 2 + .../DeploymentNodeArchetypeDslContext.java | 1 + .../structurizr/dsl/DeploymentNodeParser.java | 4 ++ .../dsl/ExplicitRelationshipParser.java | 10 ++++- .../dsl/ImplicitRelationshipParser.java | 10 ++++- ...InfrastructureNodeArchetypeDslContext.java | 1 + .../dsl/InfrastructureNodeParser.java | 2 + .../dsl/PersonArchetypeDslContext.java | 1 + .../com/structurizr/dsl/PersonParser.java | 2 + .../dsl/RelationshipArchetypeDslContext.java | 1 + .../SoftwareSystemArchetypeDslContext.java | 1 + .../structurizr/dsl/SoftwareSystemParser.java | 2 + .../structurizr/dsl/StructurizrDslParser.java | 7 ++++ .../java/com/structurizr/dsl/DslTests.java | 3 ++ .../resources/dsl/archetypes-for-defaults.dsl | 8 ++++ 20 files changed, 100 insertions(+), 7 deletions(-) diff --git a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java index 398eacdc1..f9d789481 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java @@ -173,6 +173,17 @@ public void addProperty(String name, String value) { properties.put(name, value); } + /** + * Adds a collection of name-value pair properties to this model item. + * + * @param properties Map of properties + */ + public void addProperties(Map properties) { + for (String key : properties.keySet()) { + this.addProperty(key, properties.get(key)); + } + } + void setProperties(Map properties) { if (properties != null) { this.properties = new HashMap<>(properties); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java index a6781b426..999ece4a4 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java @@ -1,11 +1,12 @@ package com.structurizr.dsl; +import com.structurizr.PropertyHolder; +import com.structurizr.model.Perspective; import com.structurizr.util.StringUtils; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; -final class Archetype { +final class Archetype implements PropertyHolder { private final String name; private final String type; @@ -13,6 +14,9 @@ final class Archetype { private String technology = ""; private final Set tags = new LinkedHashSet<>(); + private Map properties = new HashMap<>(); + private final Set perspectives = new TreeSet<>(); + Archetype(String name, String type) { if (StringUtils.isNullOrEmpty(name)) { name = type; @@ -62,4 +66,31 @@ Set getTags() { return new LinkedHashSet<>(tags); } + /** + * Gets the collection of name-value property pairs associated with this model item, as a Map. + * + * @return a Map (String, String) (empty if there are no properties) + */ + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + /** + * Adds a name-value pair property to this model item. + * + * @param name the name of the property + * @param value the value of the property + */ + public void addProperty(String name, String value) { + if (StringUtils.isNullOrEmpty(name)) { + throw new IllegalArgumentException("A property name must be specified."); + } + + if (StringUtils.isNullOrEmpty(value)) { + throw new IllegalArgumentException("A property value must be specified."); + } + + properties.put(name, value); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java index a204af35c..44474ce46 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java @@ -13,6 +13,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java index 6600845f1..b22d36c05 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java @@ -53,6 +53,8 @@ Component parse(ContainerDslContext context, Tokens tokens, Archetype archetype) } component.addTags(tags); + component.addProperties(archetype.getProperties()); + if (context.hasGroup()) { component.setGroup(context.getGroup().getName()); context.getGroup().addElement(component); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java index df4c4d015..3432f62a3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java @@ -13,6 +13,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java index c937849de..ad570e722 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java @@ -53,6 +53,8 @@ Container parse(SoftwareSystemDslContext context, Tokens tokens, Archetype arche } container.addTags(tags); + container.addProperties(archetype.getProperties()); + if (context.hasGroup()) { container.setGroup(context.getGroup().getName()); context.getGroup().addElement(container); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java index 65b411b5c..82037bd0b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java @@ -13,6 +13,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java index 7e1bb3569..b551076d9 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java @@ -44,6 +44,8 @@ DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens, Arc } deploymentNode.addTags(tags); + deploymentNode.addProperties(archetype.getProperties()); + String instances = "1"; if (tokens.includes(INSTANCES_INDEX)) { instances = tokens.get(INSTANCES_INDEX); @@ -91,6 +93,8 @@ DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens, Archetype } deploymentNode.addTags(tags); + deploymentNode.addProperties(archetype.getProperties()); + String instances = "1"; if (tokens.includes(INSTANCES_INDEX)) { instances = tokens.get(INSTANCES_INDEX); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index f67820aee..18e166433 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -55,7 +55,10 @@ Relationship parse(DslContext context, Tokens tokens, Archetype archetype) { tags = tokens.get(TAGS_INDEX).split(","); } - return createRelationship(sourceElement, description, technology, tags, destinationElement); + Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + relationship.addProperties(archetype.getProperties()); + + return relationship; } Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { @@ -99,7 +102,10 @@ Set parse(ElementsDslContext context, Tokens tokens, Archetype arc Set relationships = new LinkedHashSet<>(); for (Element sourceElement : sourceElements) { for (Element destinationElement : destinationElements) { - relationships.add(createRelationship(sourceElement, description, technology, tags, destinationElement)); + Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + relationship.addProperties(archetype.getProperties()); + + relationships.add(relationship); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index e5dd02246..73349d877 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -50,7 +50,10 @@ Relationship parse(ElementDslContext context, Tokens tokens, Archetype archetype tags = tokens.get(TAGS_INDEX).split(","); } - return createRelationship(sourceElement, description, technology, tags, destinationElement); + Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + relationship.addProperties(archetype.getProperties()); + + return relationship; } Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { @@ -89,7 +92,10 @@ Set parse(ElementsDslContext context, Tokens tokens, Archetype arc Set relationships = new LinkedHashSet<>(); for (Element sourceElement : sourceElements) { - relationships.add(createRelationship(sourceElement, description, technology, tags, destinationElement)); + Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + relationship.addProperties(archetype.getProperties()); + + relationships.add(relationship); } return relationships; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java index 197da828c..b123d80ce 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java @@ -13,6 +13,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java index 126c90776..a18baed55 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java @@ -45,6 +45,8 @@ InfrastructureNode parse(DeploymentNodeDslContext context, Tokens tokens, Archet } infrastructureNode.addTags(tags); + infrastructureNode.addProperties(archetype.getProperties()); + if (context.hasGroup()) { infrastructureNode.setGroup(context.getGroup().getName()); context.getGroup().addElement(infrastructureNode); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java index 0ecd4b4d1..78edf577b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java @@ -12,6 +12,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.DESCRIPTION_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java index f16209982..4b0d6c9b5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -44,6 +44,8 @@ Person parse(ModelDslContext context, Tokens tokens, Archetype archetype) { } person.addTags(tags); + person.addProperties(archetype.getProperties()); + if (context.hasGroup()) { person.setGroup(context.getGroup().getName()); context.getGroup().addElement(person); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java index 4bd231e68..798b883c7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java @@ -13,6 +13,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java index 5170b064a..137e55e3b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java @@ -12,6 +12,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.DESCRIPTION_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java index cdbef98a5..affb4ff68 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -44,6 +44,8 @@ SoftwareSystem parse(ModelDslContext context, Tokens tokens, Archetype archetype } softwareSystem.addTags(tags); + softwareSystem.addProperties(archetype.getProperties()); + if (context.hasGroup()) { softwareSystem.setGroup(context.getGroup().getName()); context.getGroup().addElement(softwareSystem); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 8423e6ac6..fd80f187e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -745,6 +745,13 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (TAGS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { new ArchetypeParser().parseTags(getContext(ArchetypeDslContext.class), tokens); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { + Archetype archetype = getContext(ArchetypeDslContext.class).getArchetype(); + startContext(new PropertiesDslContext(archetype)); + + } else if (inContext(PropertiesDslContext.class)) { + new PropertyParser().parse(getContext(PropertiesDslContext.class), tokens); + } else if (VIEWS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { if (parsedTokens.contains(VIEWS_TOKEN)) { throw new RuntimeException("Multiple view sets are not permitted in a DSL definition"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 4b33251a2..b2a5c7a64 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1499,15 +1499,18 @@ void test_archetypesForDefaults() throws Exception { SoftwareSystem a = workspace.getModel().getSoftwareSystemWithName("A"); assertEquals("Default Description", a.getDescription()); assertTrue(a.hasTag("Default Tag")); + assertTrue(a.hasProperty("Default Property Name", "Default Property Value")); SoftwareSystem b = workspace.getModel().getSoftwareSystemWithName("B"); assertEquals("Default Description", b.getDescription()); assertTrue(b.hasTag("Default Tag")); + assertTrue(b.hasProperty("Default Property Name", "Default Property Value")); Relationship r = a.getEfferentRelationshipWith(b); assertEquals("Default Description", r.getDescription()); assertEquals("Default Technology", r.getTechnology()); assertTrue(r.hasTag("Default Tag")); + assertTrue(r.hasProperty("Default Property Name", "Default Property Value")); } @Test diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl index 63ed7ce07..b12400ae9 100644 --- a/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl @@ -5,12 +5,20 @@ workspace { softwaresystem { description "Default Description" tag "Default Tag" + + properties { + "Default Property Name" "Default Property Value" + } } -> { description "Default Description" technology "Default Technology" tag "Default Tag" + + properties { + "Default Property Name" "Default Property Value" + } } } From e8149373bb0773bc861e5c2601fa47ec7706575e Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:06:59 +0000 Subject: [PATCH 324/418] Adds perspectives to archetypes. --- .../com/structurizr/PerspectivesHolder.java | 40 +++++++++++++ .../java/com/structurizr/PropertyHolder.java | 4 +- .../java/com/structurizr/model/ModelItem.java | 14 ++++- .../com/structurizr/model/Perspective.java | 2 +- .../java/com/structurizr/dsl/Archetype.java | 58 +++++++++++++++++-- .../dsl/ComponentArchetypeDslContext.java | 3 +- .../com/structurizr/dsl/ComponentParser.java | 1 + .../dsl/ContainerArchetypeDslContext.java | 3 +- .../com/structurizr/dsl/ContainerParser.java | 1 + .../DeploymentNodeArchetypeDslContext.java | 3 +- .../structurizr/dsl/DeploymentNodeParser.java | 2 + .../dsl/ExplicitRelationshipParser.java | 2 + .../dsl/ImplicitRelationshipParser.java | 2 + ...InfrastructureNodeArchetypeDslContext.java | 3 +- .../dsl/InfrastructureNodeParser.java | 1 + .../dsl/PersonArchetypeDslContext.java | 3 +- .../com/structurizr/dsl/PersonParser.java | 1 + .../structurizr/dsl/PerspectiveParser.java | 6 +- .../dsl/PerspectivesDslContext.java | 15 ++--- .../dsl/RelationshipArchetypeDslContext.java | 3 +- .../SoftwareSystemArchetypeDslContext.java | 3 +- .../structurizr/dsl/SoftwareSystemParser.java | 1 + .../structurizr/dsl/StructurizrDslParser.java | 6 +- .../java/com/structurizr/dsl/DslTests.java | 3 + .../resources/dsl/archetypes-for-defaults.dsl | 8 +++ 25 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 structurizr-core/src/main/java/com/structurizr/PerspectivesHolder.java diff --git a/structurizr-core/src/main/java/com/structurizr/PerspectivesHolder.java b/structurizr-core/src/main/java/com/structurizr/PerspectivesHolder.java new file mode 100644 index 000000000..1df88a62c --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/PerspectivesHolder.java @@ -0,0 +1,40 @@ +package com.structurizr; + +import com.structurizr.model.Perspective; +import com.structurizr.util.StringUtils; + +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public interface PerspectivesHolder { + + /** + * Gets the set of perspectives associated with this object. + * + * @return a Set of Perspective objects (empty if there are none) + */ + Set getPerspectives(); + + /** + * Adds a perspective to this object. + * + * @param name the name of the perspective (e.g. "Security", must be unique) + * @param description the description of the perspective + * @return a Perspective object + * @throws IllegalArgumentException if perspective details are not specified, or the named perspective exists already + */ + Perspective addPerspective(String name, String description); + + /** + * Adds a perspective to this object. + * + * @param name the name of the perspective (e.g. "Technical Debt", must be unique) + * @param description the description of the perspective (e.g. "High") + * @param value the value of the perspective + * @return a Perspective object + * @throws IllegalArgumentException if perspective details are not specified, or the named perspective exists already + */ + Perspective addPerspective(String name, String description, String value); + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/PropertyHolder.java b/structurizr-core/src/main/java/com/structurizr/PropertyHolder.java index ec22c5567..b90616a91 100644 --- a/structurizr-core/src/main/java/com/structurizr/PropertyHolder.java +++ b/structurizr-core/src/main/java/com/structurizr/PropertyHolder.java @@ -5,14 +5,14 @@ public interface PropertyHolder { /** - * Gets the collection of name-value property pairs associated with this workspace, as a Map. + * Gets the collection of name-value property pairs associated with this object, as a Map. * * @return a Map (String, String) (empty if there are no properties) */ public Map getProperties(); /** - * Adds a name-value pair property to this workspace. + * Adds a name-value pair property to this object. * * @param name the name of the property * @param value the value of the property diff --git a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java index f9d789481..9cba7e71a 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java @@ -1,6 +1,7 @@ package com.structurizr.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.PerspectivesHolder; import com.structurizr.PropertyHolder; import com.structurizr.util.StringUtils; import com.structurizr.util.TagUtils; @@ -11,7 +12,7 @@ /** * The base class for elements and relationships. */ -public abstract class ModelItem implements PropertyHolder, Comparable { +public abstract class ModelItem implements PropertyHolder, PerspectivesHolder, Comparable { private String id = ""; private final Set tags = new LinkedHashSet<>(); @@ -249,6 +250,17 @@ public Perspective addPerspective(String name, String description, String value) return perspective; } + /** + * Adds a collection of name-value pair properties to this model item. + * + * @param perspectives Set of Perspective objects + */ + public void addPerspectives(Set perspectives) { + for (Perspective perspective : perspectives) { + addPerspective(perspective.getName(), perspective.getDescription(), perspective.getValue()); + } + } + @Override public int compareTo(ModelItem modelItem) { try { diff --git a/structurizr-core/src/main/java/com/structurizr/model/Perspective.java b/structurizr-core/src/main/java/com/structurizr/model/Perspective.java index 5b55e757e..fdb9e95f5 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Perspective.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Perspective.java @@ -13,7 +13,7 @@ public final class Perspective implements Comparable { Perspective() { } - Perspective(String name, String description, String value) { + public Perspective(String name, String description, String value) { this.name = name; this.description = description; this.value = value; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java index 999ece4a4..2a0b0fbe3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java @@ -1,12 +1,13 @@ package com.structurizr.dsl; +import com.structurizr.PerspectivesHolder; import com.structurizr.PropertyHolder; import com.structurizr.model.Perspective; import com.structurizr.util.StringUtils; import java.util.*; -final class Archetype implements PropertyHolder { +final class Archetype implements PropertyHolder, PerspectivesHolder { private final String name; private final String type; @@ -14,7 +15,7 @@ final class Archetype implements PropertyHolder { private String technology = ""; private final Set tags = new LinkedHashSet<>(); - private Map properties = new HashMap<>(); + private final Map properties = new HashMap<>(); private final Set perspectives = new TreeSet<>(); Archetype(String name, String type) { @@ -67,7 +68,7 @@ Set getTags() { } /** - * Gets the collection of name-value property pairs associated with this model item, as a Map. + * Gets the collection of name-value property pairs associated with this archetype, as a Map. * * @return a Map (String, String) (empty if there are no properties) */ @@ -76,7 +77,7 @@ public Map getProperties() { } /** - * Adds a name-value pair property to this model item. + * Adds a name-value pair property to this archetype. * * @param name the name of the property * @param value the value of the property @@ -93,4 +94,53 @@ public void addProperty(String name, String value) { properties.put(name, value); } + /** + * Gets the set of perspectives associated with this archetype. + * + * @return a Set of Perspective objects (empty if there are none) + */ + public Set getPerspectives() { + return new TreeSet<>(perspectives); + } + + /** + * Adds a perspective to this archetype. + * + * @param name the name of the perspective (e.g. "Security", must be unique) + * @param description the description of the perspective + * @return a Perspective object + * @throws IllegalArgumentException if perspective details are not specified, or the named perspective exists already + */ + public Perspective addPerspective(String name, String description) { + return addPerspective(name, description, ""); + } + + /** + * Adds a perspective to this archetype. + * + * @param name the name of the perspective (e.g. "Technical Debt", must be unique) + * @param description the description of the perspective (e.g. "High") + * @param value the value of the perspective + * @return a Perspective object + * @throws IllegalArgumentException if perspective details are not specified, or the named perspective exists already + */ + public Perspective addPerspective(String name, String description, String value) { + if (StringUtils.isNullOrEmpty(name)) { + throw new IllegalArgumentException("A name must be specified."); + } + + if (StringUtils.isNullOrEmpty(description)) { + throw new IllegalArgumentException("A description must be specified."); + } + + if (perspectives.stream().anyMatch(p -> p.getName().equals(name))) { + throw new IllegalArgumentException("A perspective named \"" + name + "\" already exists."); + } + + Perspective perspective = new Perspective(name, description, value); + perspectives.add(perspective); + + return perspective; + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java index 44474ce46..f81a925cb 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java @@ -13,7 +13,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, - StructurizrDslTokens.PROPERTIES_TOKEN + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java index b22d36c05..0b587d2e8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java @@ -54,6 +54,7 @@ Component parse(ContainerDslContext context, Tokens tokens, Archetype archetype) component.addTags(tags); component.addProperties(archetype.getProperties()); + component.addPerspectives(archetype.getPerspectives()); if (context.hasGroup()) { component.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java index 3432f62a3..676f1d548 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java @@ -13,7 +13,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, - StructurizrDslTokens.PROPERTIES_TOKEN + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java index ad570e722..c8abc1ec8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java @@ -54,6 +54,7 @@ Container parse(SoftwareSystemDslContext context, Tokens tokens, Archetype arche container.addTags(tags); container.addProperties(archetype.getProperties()); + container.addPerspectives(archetype.getPerspectives()); if (context.hasGroup()) { container.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java index 82037bd0b..0a5b3dc4a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java @@ -13,7 +13,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, - StructurizrDslTokens.PROPERTIES_TOKEN + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java index b551076d9..b2a5a2c3d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java @@ -45,6 +45,7 @@ DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens, Arc deploymentNode.addTags(tags); deploymentNode.addProperties(archetype.getProperties()); + deploymentNode.addPerspectives(archetype.getPerspectives()); String instances = "1"; if (tokens.includes(INSTANCES_INDEX)) { @@ -94,6 +95,7 @@ DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens, Archetype deploymentNode.addTags(tags); deploymentNode.addProperties(archetype.getProperties()); + deploymentNode.addPerspectives(archetype.getPerspectives()); String instances = "1"; if (tokens.includes(INSTANCES_INDEX)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index 18e166433..de846e382 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -57,6 +57,7 @@ Relationship parse(DslContext context, Tokens tokens, Archetype archetype) { Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); return relationship; } @@ -104,6 +105,7 @@ Set parse(ElementsDslContext context, Tokens tokens, Archetype arc for (Element destinationElement : destinationElements) { Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); relationships.add(relationship); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index 73349d877..e3fb1df93 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -52,6 +52,7 @@ Relationship parse(ElementDslContext context, Tokens tokens, Archetype archetype Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); return relationship; } @@ -94,6 +95,7 @@ Set parse(ElementsDslContext context, Tokens tokens, Archetype arc for (Element sourceElement : sourceElements) { Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); relationships.add(relationship); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java index b123d80ce..fc6f06ae5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java @@ -13,7 +13,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, - StructurizrDslTokens.PROPERTIES_TOKEN + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java index a18baed55..964f989e9 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java @@ -46,6 +46,7 @@ InfrastructureNode parse(DeploymentNodeDslContext context, Tokens tokens, Archet infrastructureNode.addTags(tags); infrastructureNode.addProperties(archetype.getProperties()); + infrastructureNode.addPerspectives(archetype.getPerspectives()); if (context.hasGroup()) { infrastructureNode.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java index 78edf577b..b8eefe9f8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java @@ -12,7 +12,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.DESCRIPTION_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, - StructurizrDslTokens.PROPERTIES_TOKEN + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java index 4b0d6c9b5..8525e5d36 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -45,6 +45,7 @@ Person parse(ModelDslContext context, Tokens tokens, Archetype archetype) { person.addTags(tags); person.addProperties(archetype.getProperties()); + person.addPerspectives(archetype.getPerspectives()); if (context.hasGroup()) { person.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java index 3a38ad3ab..6fcc92c1d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -import com.structurizr.model.ModelItem; +import com.structurizr.PerspectivesHolder; final class PerspectiveParser extends AbstractParser { @@ -27,8 +27,8 @@ void parse(PerspectivesDslContext context, Tokens tokens) { value = tokens.get(PERSPECTIVE_VALUE_INDEX); } - for (ModelItem modelItem : context.getModelItems()) { - modelItem.addPerspective(name, description, value); + for (PerspectivesHolder perspectivesHolder : context.getPerspectivesHolders()) { + perspectivesHolder.addPerspective(name, description, value); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java index 50ff2e6d3..fe1338f57 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.PerspectivesHolder; import com.structurizr.model.ModelItem; import java.util.ArrayList; @@ -7,18 +8,18 @@ final class PerspectivesDslContext extends DslContext { - private final Collection modelItems = new ArrayList<>(); + private final Collection perspectivesHolders = new ArrayList<>(); - PerspectivesDslContext(ModelItem modelItem) { - this.modelItems.add(modelItem); + PerspectivesDslContext(PerspectivesHolder perspectivesHolder) { + this.perspectivesHolders.add(perspectivesHolder); } - PerspectivesDslContext(Collection modelItems) { - this.modelItems.addAll(modelItems); + PerspectivesDslContext(Collection perspectivesHolders) { + this.perspectivesHolders.addAll(perspectivesHolders); } - Collection getModelItems() { - return this.modelItems; + Collection getPerspectivesHolders() { + return this.perspectivesHolders; } @Override diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java index 798b883c7..d25d329ae 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java @@ -13,7 +13,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.TECHNOLOGY_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, - StructurizrDslTokens.PROPERTIES_TOKEN + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java index 137e55e3b..0283d5716 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java @@ -12,7 +12,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.DESCRIPTION_TOKEN, StructurizrDslTokens.TAG_TOKEN, StructurizrDslTokens.TAGS_TOKEN, - StructurizrDslTokens.PROPERTIES_TOKEN + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java index affb4ff68..fd1ef8949 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -45,6 +45,7 @@ SoftwareSystem parse(ModelDslContext context, Tokens tokens, Archetype archetype softwareSystem.addTags(tags); softwareSystem.addProperties(archetype.getProperties()); + softwareSystem.addPerspectives(archetype.getPerspectives()); if (context.hasGroup()) { softwareSystem.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index fd80f187e..c2a68603a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.PerspectivesHolder; import com.structurizr.PropertyHolder; import com.structurizr.Workspace; import com.structurizr.model.*; @@ -749,8 +750,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn Archetype archetype = getContext(ArchetypeDslContext.class).getArchetype(); startContext(new PropertiesDslContext(archetype)); - } else if (inContext(PropertiesDslContext.class)) { - new PropertyParser().parse(getContext(PropertiesDslContext.class), tokens); + } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { + Archetype archetype = getContext(ArchetypeDslContext.class).getArchetype(); + startContext(new PerspectivesDslContext(archetype)); } else if (VIEWS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { if (parsedTokens.contains(VIEWS_TOKEN)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index b2a5c7a64..9b766fa9f 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1500,17 +1500,20 @@ void test_archetypesForDefaults() throws Exception { assertEquals("Default Description", a.getDescription()); assertTrue(a.hasTag("Default Tag")); assertTrue(a.hasProperty("Default Property Name", "Default Property Value")); + assertEquals("Default Perspective Description", (a.getPerspectives().stream().filter(p -> p.getName().equals("Default Perspective Name")).findFirst().get().getDescription())); SoftwareSystem b = workspace.getModel().getSoftwareSystemWithName("B"); assertEquals("Default Description", b.getDescription()); assertTrue(b.hasTag("Default Tag")); assertTrue(b.hasProperty("Default Property Name", "Default Property Value")); + assertEquals("Default Perspective Description", (b.getPerspectives().stream().filter(p -> p.getName().equals("Default Perspective Name")).findFirst().get().getDescription())); Relationship r = a.getEfferentRelationshipWith(b); assertEquals("Default Description", r.getDescription()); assertEquals("Default Technology", r.getTechnology()); assertTrue(r.hasTag("Default Tag")); assertTrue(r.hasProperty("Default Property Name", "Default Property Value")); + assertEquals("Default Perspective Description", (r.getPerspectives().stream().filter(p -> p.getName().equals("Default Perspective Name")).findFirst().get().getDescription())); } @Test diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl index b12400ae9..cc6d71aee 100644 --- a/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl @@ -9,6 +9,10 @@ workspace { properties { "Default Property Name" "Default Property Value" } + + perspectives { + "Default Perspective Name" "Default Perspective Description" + } } -> { @@ -19,6 +23,10 @@ workspace { properties { "Default Property Name" "Default Property Value" } + + perspectives { + "Default Perspective Name" "Default Perspective Description" + } } } From d9a6f1cdc778fea029e1df95d8faa2cdee063cb9 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:30:55 +0000 Subject: [PATCH 325/418] structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for system context, container, and component views. --- changelog.md | 3 + .../com/structurizr/view/ComponentView.java | 13 ++++ .../com/structurizr/view/ContainerView.java | 13 ++++ .../java/com/structurizr/view/ModelView.java | 14 ++++ .../java/com/structurizr/view/StaticView.java | 9 ++- .../structurizr/view/SystemContextView.java | 13 ++++ .../structurizr/view/SystemLandscapeView.java | 13 +++- .../structurizr/view/ComponentViewTests.java | 64 +++++++++++++++++++ .../structurizr/view/ContainerViewTests.java | 62 ++++++++++++++++++ .../view/SystemContextViewTests.java | 38 +++++++++++ .../dsl/ModelViewContentParser.java | 1 + .../dsl/StaticViewContentParser.java | 16 +++-- .../dsl/StaticViewContentParserTests.java | 44 ++++++++++++- 13 files changed, 292 insertions(+), 11 deletions(-) diff --git a/changelog.md b/changelog.md index e703945db..c4b7acf70 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,9 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/374 (!identifiers hierarchical isn't propagated when extending a workspace). - structurizr-dsl: Adds the ability to use the `group` keyword inside a component definition, to set the group name of that component. - structurizr-dsl: Adds the ability to use the `group` keyword inside the component finder strategy `forEach` block. +- structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for system context views that only adds relationships to/from the scoped software system. +- structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for container views that only adds relationships to/from the containers in the scoped software system. +- structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for component views that only adds relationships to/from the components in the scoped container. ## 3.2.1 (10th December 2024) diff --git a/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java index d03756b37..b543dc876 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java @@ -156,6 +156,15 @@ public String getName() { */ @Override public void addDefaultElements() { + addDefaultElements(true); + } + + /** + * Adds the default set of elements and relationships to this view. + * + * @param greedy true (add all relationships) or false (adds relationships to/from the components in the scoped container only) + */ + public void addDefaultElements(boolean greedy) { for (Component component : getContainer().getComponents()) { add(component); @@ -169,6 +178,10 @@ public void addDefaultElements() { addNearestNeighbours(component, Person.class); addNearestNeighbours(component, SoftwareSystem.class); } + + if (!greedy) { + removeRelationshipsNotConnectedToElements(getContainer().getComponents()); + } } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java index b4d02fb9a..664164974 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java @@ -85,12 +85,25 @@ public String getName() { */ @Override public void addDefaultElements() { + addDefaultElements(true); + } + + /** + * Adds the default set of elements and relationships to this view. + * + * @param greedy true (add all relationships) or false (adds relationships to/from the containers in the scoped software system only) + */ + public void addDefaultElements(boolean greedy) { for (Container container : getSoftwareSystem().getContainers()) { add(container); addNearestNeighbours(container, CustomElement.class); addNearestNeighbours(container, Person.class); addNearestNeighbours(container, SoftwareSystem.class); } + + if (!greedy) { + removeRelationshipsNotConnectedToElements(getSoftwareSystem().getContainers()); + } } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java index 1128dcd9b..5ced49fed 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java @@ -302,6 +302,20 @@ public void removeRelationshipsNotConnectedToElement(Element element) { } } + /** + * Removes relationships that are not connected to the specified elements. + * + * @param elements the Set of Element objects to test against + */ + public void removeRelationshipsNotConnectedToElements(Set elements) { + if (elements != null) { + getRelationships().stream() + .map(RelationshipView::getRelationship) + .filter(r -> !elements.contains(r.getSource()) && !elements.contains(r.getDestination())) + .forEach(this::remove); + } + } + /** * Gets the set of elements in this view. * diff --git a/structurizr-core/src/main/java/com/structurizr/view/StaticView.java b/structurizr-core/src/main/java/com/structurizr/view/StaticView.java index f7ea1d9dd..1be47ef96 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/StaticView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/StaticView.java @@ -25,10 +25,17 @@ public abstract class StaticView extends ModelView implements AnimatedView { } /** - * Adds the default set of elements to this view. + * Adds the default set of elements and relationships to this view. */ public abstract void addDefaultElements(); + /** + * Adds the default set of elements and relationships to this view. + * + * @param greedy true (add all relationships) or false (depends on view type) + */ + public abstract void addDefaultElements(boolean greedy); + /** * Adds all software systems in the model to this view. */ diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java index bef228fd8..513fe02c1 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java @@ -39,9 +39,22 @@ public String getName() { */ @Override public void addDefaultElements() { + addDefaultElements(true); + } + + /** + * Adds the default set of elements and relationships to this view. + * + * @param greedy true (add all relationships) or false (adds relationships to/from the scoped software system only) + */ + public void addDefaultElements(boolean greedy) { addNearestNeighbours(getSoftwareSystem(), CustomElement.class); addNearestNeighbours(getSoftwareSystem(), Person.class); addNearestNeighbours(getSoftwareSystem(), SoftwareSystem.class); + + if (!greedy) { + removeRelationshipsNotConnectedToElement(getSoftwareSystem()); + } } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java index ce719e07f..359245180 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java @@ -54,6 +54,15 @@ void setModel(Model model) { */ @Override public void addDefaultElements() { + addDefaultElements(true); + } + + /** + * Adds the default set of elements and relationships to this view. + * + * @param greedy true (add all relationships) or false (add all relationships) + */ + public void addDefaultElements(boolean greedy) { addAllSoftwareSystems(); addAllPeople(); @@ -61,8 +70,8 @@ public void addDefaultElements() { } /** - * Adds all software systems and all people to this view. - */ + * Adds all software systems and all people to this view. + */ @Override public void addAllElements() { addAllSoftwareSystems(); diff --git a/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java index 9c80529d7..db21ee52e 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java @@ -614,6 +614,70 @@ void addDefaultElements() { assertFalse(view.getElements().contains(new ElementView(component2))); } + @Test + void addDefaultElements_WhenGreedyIsTrue() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + SoftwareSystem a = model.addSoftwareSystem("A"); + Container a1 = a.addContainer("A1", "", ""); + Component aa1 = a1.addComponent("AA1", "", ""); + Component aa2 = a1.addComponent("AA2", "", ""); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + aa1.uses(aa2, "Uses"); + aa1.uses(b, "Uses"); + aa2.uses(c, "Uses"); + b.uses(c, "Uses"); + + view = new ComponentView(a1, "components", "Description"); + view.addDefaultElements(true); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(aa1))); + assertTrue(view.getElements().contains(new ElementView(aa2))); + assertTrue(view.getElements().contains(new ElementView(b))); + assertTrue(view.getElements().contains(new ElementView(c))); + + assertEquals(4, view.getRelationships().size()); + assertNotNull(view.getRelationshipView(aa1.getEfferentRelationshipWith(aa2))); + assertNotNull(view.getRelationshipView(aa1.getEfferentRelationshipWith(b))); + assertNotNull(view.getRelationshipView(aa2.getEfferentRelationshipWith(c))); + assertNotNull(view.getRelationshipView(b.getEfferentRelationshipWith(c))); + } + + @Test + void addDefaultElements_WhenGreedyIsFalse() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + SoftwareSystem a = model.addSoftwareSystem("A"); + Container a1 = a.addContainer("A1", "", ""); + Component aa1 = a1.addComponent("AA1", "", ""); + Component aa2 = a1.addComponent("AA2", "", ""); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + aa1.uses(aa2, "Uses"); + aa1.uses(b, "Uses"); + aa2.uses(c, "Uses"); + b.uses(c, "Uses"); + + view = new ComponentView(a1, "components", "Description"); + view.addDefaultElements(false); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(aa1))); + assertTrue(view.getElements().contains(new ElementView(aa2))); + assertTrue(view.getElements().contains(new ElementView(b))); + assertTrue(view.getElements().contains(new ElementView(c))); + + assertEquals(3, view.getRelationships().size()); + assertNotNull(view.getRelationshipView(aa1.getEfferentRelationshipWith(aa2))); + assertNotNull(view.getRelationshipView(aa1.getEfferentRelationshipWith(b))); + assertNotNull(view.getRelationshipView(aa2.getEfferentRelationshipWith(c))); + assertNull(view.getRelationshipView(b.getEfferentRelationshipWith(c))); + } + @Test void addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); diff --git a/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java index 356cbd0dc..a9caf817d 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java @@ -306,6 +306,68 @@ void addDefaultElements() { assertFalse(view.getElements().contains(new ElementView(container2))); } + @Test + void addDefaultElements_WhenGreedyIsTrue() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + SoftwareSystem a = model.addSoftwareSystem("A"); + Container a1 = a.addContainer("A1"); + Container a2 = a.addContainer("A2"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + a1.uses(a2, "Uses"); + a1.uses(b, "Uses"); + a2.uses(c, "Uses"); + b.uses(c, "Uses"); + + view = new ContainerView(a, "containers", "Description"); + view.addDefaultElements(true); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(a1))); + assertTrue(view.getElements().contains(new ElementView(a2))); + assertTrue(view.getElements().contains(new ElementView(b))); + assertTrue(view.getElements().contains(new ElementView(c))); + + assertEquals(4, view.getRelationships().size()); + assertNotNull(view.getRelationshipView(a1.getEfferentRelationshipWith(a2))); + assertNotNull(view.getRelationshipView(a1.getEfferentRelationshipWith(b))); + assertNotNull(view.getRelationshipView(a2.getEfferentRelationshipWith(c))); + assertNotNull(view.getRelationshipView(b.getEfferentRelationshipWith(c))); + } + + @Test + void addDefaultElements_WhenGreedyIsFalse() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + SoftwareSystem a = model.addSoftwareSystem("A"); + Container a1 = a.addContainer("A1"); + Container a2 = a.addContainer("A2"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + a1.uses(a2, "Uses"); + a1.uses(b, "Uses"); + a2.uses(c, "Uses"); + b.uses(c, "Uses"); + + view = new ContainerView(a, "containers", "Description"); + view.addDefaultElements(false); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(a1))); + assertTrue(view.getElements().contains(new ElementView(a2))); + assertTrue(view.getElements().contains(new ElementView(b))); + assertTrue(view.getElements().contains(new ElementView(c))); + + assertEquals(3, view.getRelationships().size()); + assertNotNull(view.getRelationshipView(a1.getEfferentRelationshipWith(a2))); + assertNotNull(view.getRelationshipView(a1.getEfferentRelationshipWith(b))); + assertNotNull(view.getRelationshipView(a2.getEfferentRelationshipWith(c))); + assertNull(view.getRelationshipView(b.getEfferentRelationshipWith(c))); + } + @Test void addSoftwareSystem_ThrowsAnException_WhenTheSoftwareSystemIsTheScopeOfTheView() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); diff --git a/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java index 87f5a631a..bc7694728 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java @@ -305,4 +305,42 @@ void addDefaultElements() { assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); } + @Test + void addDefaultElements_WhenGreedyIsTrue() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + Relationship ab = a.uses(b, "Uses"); + Relationship ac = a.uses(c, "Uses"); + Relationship bc = b.uses(c, "Uses"); + + view = views.createSystemContextView(a, "key", "description"); + view.addDefaultElements(true); + + assertEquals(3, view.getElements().size()); + assertNotNull(view.getRelationshipView(ab)); + assertNotNull(view.getRelationshipView(ac)); + assertNotNull(view.getRelationshipView(bc)); + } + + @Test + void addDefaultElements_WhenGreedyIsFalse() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + SoftwareSystem c = model.addSoftwareSystem("C"); + + Relationship ab = a.uses(b, "Uses"); + Relationship ac = a.uses(c, "Uses"); + Relationship bc = b.uses(c, "Uses"); + + view = views.createSystemContextView(a, "key", "description"); + view.addDefaultElements(false); + + assertEquals(3, view.getElements().size()); + assertNotNull(view.getRelationshipView(ab)); + assertNotNull(view.getRelationshipView(ac)); + assertNull(view.getRelationshipView(bc)); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java index 2f89808c0..4f16ebcdb 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java @@ -6,6 +6,7 @@ abstract class ModelViewContentParser extends AbstractParser { protected static final String WILDCARD = "*"; + protected static final String WILDCARD_RELUCTANT = "*?"; protected static final String ELEMENT_WILDCARD = "element==*"; protected boolean isExpression(String token) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java index 85d7dec9d..a6c653d0b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java @@ -2,10 +2,7 @@ import com.structurizr.model.*; import com.structurizr.util.StringUtils; -import com.structurizr.view.ComponentView; -import com.structurizr.view.ContainerView; -import com.structurizr.view.ElementNotPermittedInViewException; -import com.structurizr.view.StaticView; +import com.structurizr.view.*; final class StaticViewContentParser extends ModelViewContentParser { @@ -13,7 +10,11 @@ final class StaticViewContentParser extends ModelViewContentParser { void parseInclude(StaticViewDslContext context, Tokens tokens) { if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { - throw new RuntimeException("Expected: include <*|identifier|expression> [*|identifier|expression...]"); + if (context.getView() instanceof SystemContextView || context.getView() instanceof ContainerView || context.getView() instanceof ComponentView) { + throw new RuntimeException("Expected: include <*|*?|identifier|expression> [*|identifier|expression...]"); + } else { + throw new RuntimeException("Expected: include <*|identifier|expression> [*|identifier|expression...]"); + } } StaticView view = context.getView(); @@ -24,7 +25,10 @@ void parseInclude(StaticViewDslContext context, Tokens tokens) { if (token.equals(WILDCARD) || token.equals(ELEMENT_WILDCARD)) { // include * or include element==* - view.addDefaultElements(); + view.addDefaultElements(true); + } else if (token.equals(WILDCARD_RELUCTANT)) { + // include *? + view.addDefaultElements(false); } else if (isExpression(token)) { new StaticViewExpressionParser().parseExpression(token, context).forEach(mi -> addModelItemToView(mi, view, null)); } else { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java index 82d572de7..043a723fe 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java @@ -76,7 +76,7 @@ void test_parseInclude_ThrowsAnException_WhenTryingToAddAComponentToASystemLands } @Test - void test_parseInclude_AddsAllPeopleAndSoftwareSystemsToASystemLandscapeView_WhenTheWildcardIsSpecified() { + void test_parseInclude_AddsAllPeopleAndSoftwareSystemsToASystemLandscapeView_WhenTheGreedyWildcardIsSpecified() { Person user = model.addPerson("User", "Description"); SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); softwareSystem1.addContainer("Container 1", "Description", "Technology"); @@ -94,6 +94,25 @@ void test_parseInclude_AddsAllPeopleAndSoftwareSystemsToASystemLandscapeView_Whe assertEquals(2, view.getRelationships().size()); } + @Test + void test_parseInclude_AddsAllPeopleAndSoftwareSystemsToASystemLandscapeView_WhenTheReluctantWildcardIsSpecified() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + softwareSystem1.addContainer("Container 1", "Description", "Technology"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + user.uses(softwareSystem1, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + SystemLandscapeViewDslContext context = new SystemLandscapeViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "*?")); + + assertEquals(3, view.getElements().size()); + assertEquals(2, view.getRelationships().size()); + } + @Test void test_parseInclude_AddsTheSpecifiedElementsToASystemLandscapeView() { Person user = model.addPerson("User", "Description"); @@ -126,12 +145,13 @@ void test_parseInclude_AddsTheSpecifiedElementsToASystemLandscapeView() { } @Test - void test_parseInclude_AddsNearestNeighboursToASystemContextView_WhenTheWildcardIsSpecified() { + void test_parseInclude_AddsNearestNeighboursToASystemContextView_WhenTheGreedyWildcardIsSpecified() { Person user = model.addPerson("User", "Description"); SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); softwareSystem1.addContainer("Container 1", "Description", "Technology"); SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); user.uses(softwareSystem1, "Uses"); + user.uses(softwareSystem2, "Uses"); softwareSystem1.uses(softwareSystem2, "Uses"); SystemContextView view = views.createSystemContextView(softwareSystem1, "key", "Description"); @@ -140,6 +160,26 @@ void test_parseInclude_AddsNearestNeighboursToASystemContextView_WhenTheWildcard parser.parseInclude(context, tokens("include", "*")); + assertEquals(3, view.getElements().size()); + assertEquals(3, view.getRelationships().size()); + } + + @Test + void test_parseInclude_AddsNearestNeighboursToASystemContextView_WhenTheReluctantWildcardIsSpecified() { + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + softwareSystem1.addContainer("Container 1", "Description", "Technology"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + user.uses(softwareSystem1, "Uses"); + user.uses(softwareSystem2, "Uses"); + softwareSystem1.uses(softwareSystem2, "Uses"); + + SystemContextView view = views.createSystemContextView(softwareSystem1, "key", "Description"); + SystemContextViewDslContext context = new SystemContextViewDslContext(view); + context.setWorkspace(workspace); + + parser.parseInclude(context, tokens("include", "*?")); + assertEquals(3, view.getElements().size()); assertEquals(2, view.getRelationships().size()); } From a3af9314dfc0e3f58235be1331696e47ed61dcc3 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 20 Feb 2025 09:08:35 +0000 Subject: [PATCH 326/418] Fixes an issue extending relationship archetypes. --- .../com/structurizr/dsl/StructurizrDslParser.java | 11 +++++------ .../src/test/java/com/structurizr/dsl/DslTests.java | 1 + .../test/resources/dsl/archetypes-for-extension.dsl | 6 +++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index c2a68603a..1c9750175 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1361,12 +1361,11 @@ private Archetype getArchetype(String archetypeType, String archetypeName) { private void extendArchetype(Archetype archetype, String archetypeName) { archetypeName = archetypeName.toLowerCase(); - Archetype parentArchetype = archetypes.get(archetype.getType()).get(archetypeName); - if (parentArchetype != null) { - archetype.setDescription(parentArchetype.getDescription()); - archetype.setTechnology(parentArchetype.getTechnology()); - archetype.addTags(parentArchetype.getTags().toArray(new String[0])); - } + Archetype parentArchetype = getArchetype(archetype.getType(), archetypeName); + + archetype.setDescription(parentArchetype.getDescription()); + archetype.setTechnology(parentArchetype.getTechnology()); + archetype.addTags(parentArchetype.getTags().toArray(new String[0])); } /** diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 9b766fa9f..b92c0da3c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1537,6 +1537,7 @@ void test_archetypesForExtension() throws Exception { assertEquals("Makes API calls to", r.getDescription()); assertEquals("HTTPS", r.getTechnology()); assertTrue(r.hasTag("Default Tag")); + assertTrue(r.hasTag("Synchronous")); assertTrue(r.hasTag("HTTPS")); } diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl index ac5116163..105e25b44 100644 --- a/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl @@ -15,7 +15,11 @@ workspace { tag "Default Tag" } - https = -> { + sync = -> { + tag "Synchronous" + } + + https = --sync-> { technology "HTTPS" tag "HTTPS" } From 183cbfd7bb309b7383a8a8031b92bfd3c48d9055 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:32:52 +0000 Subject: [PATCH 327/418] GraphvizAutomaticLayout.apply(Workspace) now only applies automatic layout to views that have Graphviz configured. --- .../autolayout/graphviz/DOTExporter.java | 39 ++------- .../graphviz/GraphvizAutomaticLayout.java | 63 ++++++++++----- .../autolayout/graphviz/DOTExporterTests.java | 79 ++++++++++--------- .../GraphvizAutomaticLayoutTests.java | 19 +++-- 4 files changed, 104 insertions(+), 96 deletions(-) diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java index bff97a7e3..013cb3446 100644 --- a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java @@ -20,16 +20,16 @@ class DOTExporter extends AbstractDiagramExporter { private static final int CLUSTER_INTERNAL_MARGIN = 25; private Locale locale = Locale.US; - private RankDirection rankDirection; - private double rankSeparation; - private double nodeSeparation; + private final RankDirection rankDirection; + private final double rankSeparation; + private final double nodeSeparation; private int groupId = 1; DOTExporter(RankDirection rankDirection, double rankSeparation, double nodeSeparation) { - this.rankDirection = rankDirection; - this.rankSeparation = rankSeparation; - this.nodeSeparation = nodeSeparation; + this.rankDirection = rankDirection != null ? rankDirection : RankDirection.TopBottom; + this.rankSeparation = rankSeparation / Constants.STRUCTURIZR_DPI; + this.nodeSeparation = nodeSeparation / Constants.STRUCTURIZR_DPI; } void setLocale(Locale locale) { @@ -38,33 +38,6 @@ void setLocale(Locale locale) { @Override protected void writeHeader(ModelView view, IndentingWriter writer) { - if (view.getAutomaticLayout() != null) { - if (view.getAutomaticLayout().getRankDirection() == null) { - rankDirection = RankDirection.TopBottom; - } else { - switch (view.getAutomaticLayout().getRankDirection()) { - case TopBottom: - rankDirection = RankDirection.TopBottom; - break; - case BottomTop: - rankDirection = RankDirection.BottomTop; - break; - case LeftRight: - rankDirection = RankDirection.LeftRight; - break; - case RightLeft: - rankDirection = RankDirection.RightLeft; - break; - } - } - - rankSeparation = view.getAutomaticLayout().getRankSeparation(); - nodeSeparation = view.getAutomaticLayout().getNodeSeparation(); - } - - rankSeparation = rankSeparation / Constants.STRUCTURIZR_DPI; - nodeSeparation = nodeSeparation / Constants.STRUCTURIZR_DPI; - writer.writeLine("digraph {"); writer.indent(); writer.writeLine("compound=true"); diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java index 07ca03630..efd3c5c7e 100644 --- a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java @@ -30,8 +30,8 @@ public class GraphvizAutomaticLayout { private final File path; private RankDirection rankDirection = RankDirection.TopBottom; - private double rankSeparation = 1.0; - private double nodeSeparation = 1.0; + private double rankSeparation = 300; + private double nodeSeparation = 300; private int margin = 400; private boolean changePaperSize = true; @@ -75,8 +75,21 @@ public void setLocale(Locale locale) { this.locale = locale; } - private DOTExporter createDOTExporter() { - DOTExporter exporter = new DOTExporter(rankDirection, rankSeparation, nodeSeparation); + private DOTExporter createDOTExporter(AutomaticLayout automaticLayout) { + DOTExporter exporter; + + if (automaticLayout == null) { + // use the configured defaults + exporter = new DOTExporter(rankDirection, rankSeparation, nodeSeparation); + } else { + // use the values from the automatic layout configuration associated with the view + exporter = new DOTExporter( + RankDirection.valueOf(automaticLayout.getRankDirection().name()), + automaticLayout.getRankSeparation(), + automaticLayout.getNodeSeparation() + ); + } + exporter.setLocale(locale); return exporter; @@ -130,7 +143,7 @@ private void runGraphviz(View view) throws Exception { public void apply(CustomView view) throws Exception { log.debug("Running Graphviz for view with key " + view.getKey()); - Diagram diagram = createDOTExporter().export(view); + Diagram diagram = createDOTExporter(view.getAutomaticLayout()).export(view); writeFile(diagram); runGraphviz(view); createSVGReader().parseAndApplyLayout(view); @@ -138,7 +151,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); + Diagram diagram = createDOTExporter(view.getAutomaticLayout()).export(view); writeFile(diagram); runGraphviz(view); createSVGReader().parseAndApplyLayout(view); @@ -146,7 +159,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); + Diagram diagram = createDOTExporter(view.getAutomaticLayout()).export(view); writeFile(diagram); runGraphviz(view); createSVGReader().parseAndApplyLayout(view); @@ -154,7 +167,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); + Diagram diagram = createDOTExporter(view.getAutomaticLayout()).export(view); writeFile(diagram); runGraphviz(view); createSVGReader().parseAndApplyLayout(view); @@ -162,7 +175,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); + Diagram diagram = createDOTExporter(view.getAutomaticLayout()).export(view); writeFile(diagram); runGraphviz(view); createSVGReader().parseAndApplyLayout(view); @@ -170,7 +183,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); + Diagram diagram = createDOTExporter(view.getAutomaticLayout()).export(view); writeFile(diagram); runGraphviz(view); createSVGReader().parseAndApplyLayout(view); @@ -178,7 +191,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); + Diagram diagram = createDOTExporter(view.getAutomaticLayout()).export(view); writeFile(diagram); runGraphviz(view); createSVGReader().parseAndApplyLayout(view); @@ -186,31 +199,45 @@ public void apply(DeploymentView view) throws Exception { public void apply(Workspace workspace) throws Exception { for (CustomView view : workspace.getViews().getCustomViews()) { - apply(view); + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } } for (SystemLandscapeView view : workspace.getViews().getSystemLandscapeViews()) { - apply(view); + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } } for (SystemContextView view : workspace.getViews().getSystemContextViews()) { - apply(view); + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } } for (ContainerView view : workspace.getViews().getContainerViews()) { - apply(view); + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } } for (ComponentView view : workspace.getViews().getComponentViews()) { - apply(view); + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } } for (DynamicView view : workspace.getViews().getDynamicViews()) { - apply(view); + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } } for (DeploymentView view : workspace.getViews().getDeploymentViews()) { - apply(view); + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } } } diff --git a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java index 367015556..fc58220ce 100644 --- a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java @@ -483,49 +483,50 @@ public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSepar @Test public void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("src/test/resources/structurizr-54915-workspace.json")); - DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + DOTExporter exporter = new DOTExporter(RankDirection.LeftRight, 300, 300); Diagram diagram = exporter.export(workspace.getViews().getDeploymentViews().iterator().next()); String content = diagram.getDefinition(); - String expectedResult = "digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=LR,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " subgraph cluster_5 {\n" + - " margin=25\n" + - " subgraph cluster_6 {\n" + - " margin=25\n" + - " subgraph cluster_12 {\n" + - " margin=25\n" + - " subgraph cluster_13 {\n" + - " margin=25\n" + - " 14 [width=1.500000,height=1.000000,fixedsize=true,id=14,label=\"14: Database\"]\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - " 7 [width=1.500000,height=1.000000,fixedsize=true,id=7,label=\"7: Route 53\"]\n" + - " 8 [width=1.500000,height=1.000000,fixedsize=true,id=8,label=\"8: Elastic Load Balancer\"]\n" + - " subgraph cluster_9 {\n" + - " margin=25\n" + - " subgraph cluster_10 {\n" + - " margin=25\n" + - " 11 [width=1.500000,height=1.000000,fixedsize=true,id=11,label=\"11: Web Application\"]\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - " 11 -> 14 [id=15]\n" + - " 7 -> 8 [id=16]\n" + - " 8 -> 11 [id=17]\n" + - "}"; + String expectedResult = """ + digraph { + compound=true + graph [splines=polyline,rankdir=LR,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph cluster_5 { + margin=25 + subgraph cluster_6 { + margin=25 + subgraph cluster_12 { + margin=25 + subgraph cluster_13 { + margin=25 + 14 [width=1.500000,height=1.000000,fixedsize=true,id=14,label="14: Database"] + } + + } + + 7 [width=1.500000,height=1.000000,fixedsize=true,id=7,label="7: Route 53"] + 8 [width=1.500000,height=1.000000,fixedsize=true,id=8,label="8: Elastic Load Balancer"] + subgraph cluster_9 { + margin=25 + subgraph cluster_10 { + margin=25 + 11 [width=1.500000,height=1.000000,fixedsize=true,id=11,label="11: Web Application"] + } + + } + + } + + } + + 11 -> 14 [id=15] + 7 -> 8 [id=16] + 8 -> 11 [id=17] + }"""; assertEquals(expectedResult, content); } diff --git a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java index 233423401..c499674a4 100644 --- a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java @@ -5,6 +5,7 @@ import com.structurizr.model.Person; import com.structurizr.model.SoftwareSystem; import com.structurizr.model.Tags; +import com.structurizr.view.AutomaticLayout; import com.structurizr.view.Shape; import com.structurizr.view.SystemContextView; import org.junit.jupiter.api.Test; @@ -17,7 +18,10 @@ public class GraphvizAutomaticLayoutTests { @Test - public void test() throws Exception { + public void apply_Workspace() throws Exception { + File tempDir = Files.createTempDirectory("graphviz").toFile(); + GraphvizAutomaticLayout graphviz = new GraphvizAutomaticLayout(tempDir); + Workspace workspace = new Workspace("Name", ""); Person user = workspace.getModel().addPerson("User"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); @@ -33,12 +37,15 @@ public void test() throws Exception { assertEquals(0, view.getElementView(softwareSystem).getX()); assertEquals(0, view.getElementView(softwareSystem).getY()); - File tempDir = Files.createTempDirectory("graphviz").toFile(); - GraphvizAutomaticLayout graphviz = new GraphvizAutomaticLayout(tempDir); - graphviz.setRankSeparation(300); - graphviz.setNodeSeparation(300); - graphviz.setMargin(400); + graphviz.apply(workspace); + + // no change - the view doesn't have automatic layout configured + assertEquals(0, view.getElementView(user).getX()); + assertEquals(0, view.getElementView(user).getY()); + assertEquals(0, view.getElementView(softwareSystem).getX()); + assertEquals(0, view.getElementView(softwareSystem).getY()); + view.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom); graphviz.apply(workspace); assertEquals(233, view.getElementView(user).getX()); From db96f9ad6181e63b2294072e382119c2feb8909a Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 12 Mar 2025 08:41:58 +0000 Subject: [PATCH 328/418] structurizr-dsl: Removes deprecated `!ref` and `!extend` keywords. --- changelog.md | 1 + .../main/java/com/structurizr/dsl/StructurizrDslParser.java | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index c4b7acf70..d9501024d 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ - structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for system context views that only adds relationships to/from the scoped software system. - structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for container views that only adds relationships to/from the containers in the scoped software system. - structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for component views that only adds relationships to/from the components in the scoped container. +- structurizr-dsl: Removes deprecated `!ref` and `!extend` keywords. ## 3.2.1 (10th December 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 1c9750175..acf83a3b2 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -361,11 +361,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn ModelItem modelItem = null; if (REF_TOKEN.equalsIgnoreCase(firstToken)) { - log.warn(REF_TOKEN + " has been deprecated and will be removed in a future release - please use !element or !relationship instead"); - modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); + throw new RuntimeException(REF_TOKEN + " was previously deprecated, and has now been removed - please use " + FIND_ELEMENT_TOKEN + " or " + FIND_RELATIONSHIP_TOKEN + " instead"); } else if (EXTEND_TOKEN.equalsIgnoreCase(firstToken)) { - log.warn(EXTEND_TOKEN + " has been deprecated and will be removed in a future release - please use !element or !relationship instead"); - modelItem = new RefParser().parse(getContext(), tokens.withoutContextStartToken()); + throw new RuntimeException(EXTEND_TOKEN + " was previously deprecated, and has now been removed - please use " + FIND_ELEMENT_TOKEN + " or " + FIND_RELATIONSHIP_TOKEN + " instead"); } else if (FIND_ELEMENT_TOKEN.equalsIgnoreCase(firstToken)) { modelItem = new FindElementParser().parse(getContext(), tokens.withoutContextStartToken()); } else if (FIND_RELATIONSHIP_TOKEN.equalsIgnoreCase(firstToken)) { From 1f7212606c8b1258c8ad327ed3af7341304fa34b Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:18:48 +0000 Subject: [PATCH 329/418] structurizr-dsl: Adds support for Java style `"""` multi-line text blocks. --- changelog.md | 1 + .../structurizr/dsl/StructurizrDslParser.java | 31 +++++++++++++++++-- .../java/com/structurizr/dsl/DslTests.java | 12 +++++++ .../src/test/resources/dsl/text-block.dsl | 13 ++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/text-block.dsl diff --git a/changelog.md b/changelog.md index d9501024d..86ca83d9f 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ - structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for container views that only adds relationships to/from the containers in the scoped software system. - structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for component views that only adds relationships to/from the components in the scoped container. - structurizr-dsl: Removes deprecated `!ref` and `!extend` keywords. +- structurizr-dsl: Adds support for Java style `"""` multi-line text blocks. ## 3.2.1 (10th December 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index acf83a3b2..594b0316d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -34,6 +34,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private static final String MULTI_LINE_COMMENT_START_TOKEN = "/*"; private static final String MULTI_LINE_COMMENT_END_TOKEN = "*/"; private static final String MULTI_LINE_SEPARATOR = "\\"; + private static final String TEXT_BLOCK_MARKER = "\"\"\""; private static final Pattern STRING_SUBSTITUTION_PATTERN = Pattern.compile("(\\$\\{[a-zA-Z0-9-_.]+?})"); @@ -1207,11 +1208,37 @@ private List preProcessLines(List lines) { int lineNumber = 1; StringBuilder buf = new StringBuilder(); boolean lineComplete = true; + boolean textBlock = false; + int textBlockLeadingSpace = -1; for (String line : lines) { - if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(MULTI_LINE_SEPARATOR)) { - buf.append(line, 0, line.length()-1); + if (textBlock) { + if (line.endsWith(TEXT_BLOCK_MARKER)) { + buf.append("\""); + textBlock = false; + textBlockLeadingSpace = -1; + lineComplete = true; + } else { + if (textBlockLeadingSpace == -1) { + textBlockLeadingSpace = 0; + for (int i = 0; i < line.length(); i++) { + if (Character.isWhitespace(line.charAt(i))) { + textBlockLeadingSpace++; + } else { + break; + } + } + } + buf.append(line, textBlockLeadingSpace, line.length()); + buf.append("\n"); + } + } else if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(MULTI_LINE_SEPARATOR)) { + buf.append(line, 0, line.length() - 1); + lineComplete = false; + } else if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(TEXT_BLOCK_MARKER)) { + buf.append(line, 0, line.length() - 2); lineComplete = false; + textBlock = true; } else { if (lineComplete) { buf.append(line); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index b92c0da3c..01b2f261b 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1541,4 +1541,16 @@ void test_archetypesForExtension() throws Exception { assertTrue(r.hasTag("HTTPS")); } + @Test + void test_textBlock() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/text-block.dsl")); + + SoftwareSystem softwareSystem = parser.getWorkspace().getModel().getSoftwareSystemWithName("Name"); + assertEquals(""" + - Line 1 + - Line 2 + - Line 3""", softwareSystem.getDescription()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/text-block.dsl b/structurizr-dsl/src/test/resources/dsl/text-block.dsl new file mode 100644 index 000000000..a96042760 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/text-block.dsl @@ -0,0 +1,13 @@ +workspace { + + model { + softwareSystem = softwareSystem "Name" { + description """ + - Line 1 + - Line 2 + - Line 3 + """ + } + } + +} \ No newline at end of file From 2b857d8f7205410be69b528a38156ecca36f24ad Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Fri, 28 Mar 2025 08:32:45 +0000 Subject: [PATCH 330/418] Removes the feature flag around archetypes. --- changelog.md | 1 + .../java/com/structurizr/dsl/Features.java | 3 -- .../structurizr/dsl/StructurizrDslParser.java | 36 ++++++++----------- .../java/com/structurizr/dsl/DslTests.java | 18 +--------- 4 files changed, 16 insertions(+), 42 deletions(-) diff --git a/changelog.md b/changelog.md index 86ca83d9f..b7d3dd120 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ - structurizr-dsl: Adds a reluctant version of `include *` (`include *?`) for component views that only adds relationships to/from the components in the scoped container. - structurizr-dsl: Removes deprecated `!ref` and `!extend` keywords. - structurizr-dsl: Adds support for Java style `"""` multi-line text blocks. +- structurizr-dsl: Adds support for defining element and relationship archetypes. ## 3.2.1 (10th December 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java index 03a99eb4f..3d40aabb4 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java @@ -1,7 +1,4 @@ package com.structurizr.dsl; public final class Features extends com.structurizr.util.Features { - - public static final String ARCHETYPES = "structurizr.feature.dsl.archetypes"; - } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 594b0316d..c529246ae 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -661,7 +661,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new ModelDslContext()); parsedTokens.add(MODEL_TOKEN); - } else if (ARCHETYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class) && features.isEnabled(Features.ARCHETYPES)) { + } else if (ARCHETYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { startContext(new ArchetypesDslContext()); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ArchetypesDslContext.class)) { @@ -1337,28 +1337,22 @@ public Features getFeatures() { } private boolean isElementKeywordOrArchetype(String token, String keyword) { - if (features.isEnabled(Features.ARCHETYPES)) { - if (token.equalsIgnoreCase(keyword)) { - return true; - } else { - return (archetypes.get(keyword).containsKey(token.toLowerCase())); - } + if (token.equalsIgnoreCase(keyword)) { + return true; } else { - return token.equalsIgnoreCase(keyword); + return (archetypes.get(keyword).containsKey(token.toLowerCase())); } } private boolean isRelationshipKeywordOrArchetype(String token) { - if (features.isEnabled(Features.ARCHETYPES)) { - if (token.equalsIgnoreCase(RELATIONSHIP_TOKEN)) { - return true; - } else if (token.startsWith(RELATIONSHIP_ARCHETYPE_PREFIX) && token.endsWith(RELATIONSHIP_ARCHETYPE_SUFFIX)) { - token = token.substring(RELATIONSHIP_ARCHETYPE_PREFIX.length(), token.length()-RELATIONSHIP_ARCHETYPE_SUFFIX.length()); - return (archetypes.get(RELATIONSHIP_TOKEN).containsKey(token.toLowerCase())); - } + if (token.equalsIgnoreCase(RELATIONSHIP_TOKEN)) { + return true; + } else if (token.startsWith(RELATIONSHIP_ARCHETYPE_PREFIX) && token.endsWith(RELATIONSHIP_ARCHETYPE_SUFFIX)) { + token = token.substring(RELATIONSHIP_ARCHETYPE_PREFIX.length(), token.length()-RELATIONSHIP_ARCHETYPE_SUFFIX.length()); + return (archetypes.get(RELATIONSHIP_TOKEN).containsKey(token.toLowerCase())); } - return token.equalsIgnoreCase(RELATIONSHIP_TOKEN); + return false; } private void addArchetype(Archetype archetype) { @@ -1368,14 +1362,12 @@ private void addArchetype(Archetype archetype) { private Archetype getArchetype(String archetypeType, String archetypeName) { Archetype archetype = null; - if (features.isEnabled(Features.ARCHETYPES)) { - if (RELATIONSHIP_TOKEN.equals(archetypeType)) { - if (archetypeName.startsWith(RELATIONSHIP_ARCHETYPE_PREFIX) && archetypeName.endsWith(RELATIONSHIP_ARCHETYPE_SUFFIX)) { - archetypeName = archetypeName.substring(RELATIONSHIP_ARCHETYPE_PREFIX.length(), archetypeName.length() - RELATIONSHIP_ARCHETYPE_SUFFIX.length()); - } + if (RELATIONSHIP_TOKEN.equals(archetypeType)) { + if (archetypeName.startsWith(RELATIONSHIP_ARCHETYPE_PREFIX) && archetypeName.endsWith(RELATIONSHIP_ARCHETYPE_SUFFIX)) { + archetypeName = archetypeName.substring(RELATIONSHIP_ARCHETYPE_PREFIX.length(), archetypeName.length() - RELATIONSHIP_ARCHETYPE_SUFFIX.length()); } - archetype = archetypes.get(archetypeType).get(archetypeName.toLowerCase()); } + archetype = archetypes.get(archetypeType).get(archetypeName.toLowerCase()); if (archetype == null) { archetype = new Archetype(archetypeName, archetypeType); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 01b2f261b..be661dd21 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1459,23 +1459,9 @@ void test_sourceIsNotRetained() throws Exception { } @Test - void test_archetypes_WhenDisabled() throws Exception { - try { - File parentDslFile = new File("src/test/resources/dsl/archetypes.dsl"); - StructurizrDslParser parser = new StructurizrDslParser(); - parser.parse(parentDslFile); - fail(); - } catch (StructurizrDslParserException e) { - assertTrue(e.getMessage().startsWith("Unexpected tokens (expected: !identifiers, group, person, softwareSystem, deploymentEnvironment, element, ->) at line 4")); - assertTrue(e.getMessage().endsWith("archetypes {")); - } - } - - @Test - void test_archetypes_WhenEnabled() throws Exception { + void test_archetypes() throws Exception { File parentDslFile = new File("src/test/resources/dsl/archetypes.dsl"); StructurizrDslParser parser = new StructurizrDslParser(); - parser.getFeatures().enable(Features.ARCHETYPES); parser.parse(parentDslFile); Workspace workspace = parser.getWorkspace(); @@ -1492,7 +1478,6 @@ void test_archetypes_WhenEnabled() throws Exception { void test_archetypesForDefaults() throws Exception { File parentDslFile = new File("src/test/resources/dsl/archetypes-for-defaults.dsl"); StructurizrDslParser parser = new StructurizrDslParser(); - parser.getFeatures().enable(Features.ARCHETYPES); parser.parse(parentDslFile); Workspace workspace = parser.getWorkspace(); @@ -1520,7 +1505,6 @@ void test_archetypesForDefaults() throws Exception { void test_archetypesForExtension() throws Exception { File parentDslFile = new File("src/test/resources/dsl/archetypes-for-extension.dsl"); StructurizrDslParser parser = new StructurizrDslParser(); - parser.getFeatures().enable(Features.ARCHETYPES); parser.parse(parentDslFile); Workspace workspace = parser.getWorkspace(); From 36114825e5ce9f8be92876ca8478e4256baf3eea Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 28 Mar 2025 10:04:59 +0000 Subject: [PATCH 331/418] Updated to reflect release. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index b7d3dd120..fd5fc022b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## v4.0.0 (unreleased) +## v4.0.0 (28th March 2025) - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/374 (!identifiers hierarchical isn't propagated when extending a workspace). - structurizr-dsl: Adds the ability to use the `group` keyword inside a component definition, to set the group name of that component. From 471726d6810c9dfdafed41cde7a0fe241691c0e4 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 28 Mar 2025 10:50:36 +0000 Subject: [PATCH 332/418] Bump dependencies. --- build.gradle | 6 ++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- structurizr-autolayout/build.gradle | 1 - structurizr-client/build.gradle | 6 ++---- structurizr-component/build.gradle | 1 - structurizr-core/build.gradle | 7 +++---- structurizr-dsl/build.gradle | 6 ++---- structurizr-export/build.gradle | 1 - structurizr-import/build.gradle | 2 -- structurizr-inspection/build.gradle | 1 - structurizr-neo4j/build.gradle | 3 +-- 11 files changed, 15 insertions(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index 536ce612a..876b332b5 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,12 @@ subprojects { proj -> mavenCentral() } + dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.3' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.3' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.3' + } + test { useJUnitPlatform() } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fcea..3994438e2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/structurizr-autolayout/build.gradle b/structurizr-autolayout/build.gradle index 631c38fcc..23decccd6 100644 --- a/structurizr-autolayout/build.gradle +++ b/structurizr-autolayout/build.gradle @@ -3,7 +3,6 @@ dependencies { api project(':structurizr-export') testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index a67e1adf7..e078cd9cc 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,12 +2,10 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.18.1' - api 'org.apache.httpcomponents.client5:httpclient5:5.4.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.18.3' + api 'org.apache.httpcomponents.client5:httpclient5:5.4.3' api 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' - } sourceSets { diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle index 3c365fbb5..1c296c092 100644 --- a/structurizr-component/build.gradle +++ b/structurizr-component/build.gradle @@ -5,7 +5,6 @@ dependencies { implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.2' testImplementation project(':structurizr-annotation') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index d188aa2be..362d12137 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,10 +1,9 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.18.1' + api 'com.fasterxml.jackson.core:jackson-annotations:2.18.3' api 'com.google.code.findbugs:jsr305:3.0.2' - api 'commons-logging:commons-logging:1.3.4' + api 'commons-logging:commons-logging:1.3.5' - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' - testImplementation 'org.assertj:assertj-core:3.26.3' + testImplementation 'org.assertj:assertj-core:3.27.3' } \ No newline at end of file diff --git a/structurizr-dsl/build.gradle b/structurizr-dsl/build.gradle index b452a6366..262cdde49 100644 --- a/structurizr-dsl/build.gradle +++ b/structurizr-dsl/build.gradle @@ -5,12 +5,10 @@ dependencies { api project(':structurizr-export') api project(':structurizr-component') - testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.22' + testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.24' testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.25' - testImplementation 'org.jruby:jruby-core:9.4.8.0' + testImplementation 'org.jruby:jruby-core:9.4.12.0' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.3' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.3' } description = 'Structurizr DSL' \ No newline at end of file diff --git a/structurizr-export/build.gradle b/structurizr-export/build.gradle index ddef771b2..f908d3fb5 100644 --- a/structurizr-export/build.gradle +++ b/structurizr-export/build.gradle @@ -3,7 +3,6 @@ dependencies { api project(':structurizr-core') testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } diff --git a/structurizr-import/build.gradle b/structurizr-import/build.gradle index eeb4cdc43..7b5c3b32f 100644 --- a/structurizr-import/build.gradle +++ b/structurizr-import/build.gradle @@ -2,8 +2,6 @@ dependencies { api project(':structurizr-core') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' - } description = 'Utilities to import diagrams and documentation into a Structurizr workspace' \ No newline at end of file diff --git a/structurizr-inspection/build.gradle b/structurizr-inspection/build.gradle index bf3f60229..72a7ec6ba 100644 --- a/structurizr-inspection/build.gradle +++ b/structurizr-inspection/build.gradle @@ -3,6 +3,5 @@ dependencies { api project(':structurizr-core') testImplementation project(':structurizr-dsl') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } \ No newline at end of file diff --git a/structurizr-neo4j/build.gradle b/structurizr-neo4j/build.gradle index aa2bcf209..4703501bf 100644 --- a/structurizr-neo4j/build.gradle +++ b/structurizr-neo4j/build.gradle @@ -1,9 +1,8 @@ dependencies { api project(':structurizr-core') - implementation 'org.neo4j.driver:neo4j-java-driver:5.27.0' + implementation 'org.neo4j.driver:neo4j-java-driver:5.28.4' testImplementation project(':structurizr-client') - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' } \ No newline at end of file From 3e3a97a24c8872c09a318c094c48025f39ff34cb Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 29 Mar 2025 08:17:54 +0000 Subject: [PATCH 333/418] structurizr-dsl: Allows archetypes to be used via workspace extension. --- changelog.md | 4 +++ gradle.properties | 2 +- .../structurizr/dsl/StructurizrDslParser.java | 10 ++++-- .../com/structurizr/dsl/WorkspaceParser.java | 4 +-- .../java/com/structurizr/dsl/DslTests.java | 27 +++++++++++++++ ...hetypes-from-workspace-extension-child.dsl | 9 +++++ ...etypes-from-workspace-extension-parent.dsl | 34 +++++++++++++++++++ 7 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-child.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-parent.dsl diff --git a/changelog.md b/changelog.md index fd5fc022b..3789f2f40 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## v4.0.1 (unreleased) + +- structurizr-dsl: Allows archetypes to be used via workspace extension. + ## v4.0.0 (28th March 2025) - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/374 (!identifiers hierarchical isn't propagated when extending a workspace). diff --git a/gradle.properties b/gradle.properties index 3b854bb7b..1f68eb29c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=4.0.0 \ No newline at end of file +version=4.0.1 \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index c529246ae..856edfd80 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.PerspectivesHolder; import com.structurizr.PropertyHolder; import com.structurizr.Workspace; import com.structurizr.model.*; @@ -48,7 +47,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private final Map constantsAndVariables; private final Features features = new Features(); - private final Map> archetypes = Map.of( + private Map> archetypes = Map.of( StructurizrDslTokens.GROUP_TOKEN, new HashMap<>(), StructurizrDslTokens.PERSON_TOKEN, new HashMap<>(), StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, new HashMap<>(), @@ -74,6 +73,11 @@ public StructurizrDslParser() { constantsAndVariables = new HashMap<>(); } + void configureFrom(StructurizrDslParser parser) { + setIdentifierScope(parser.getIdentifierScope()); + archetypes = parser.archetypes; + } + /** * Provides a way to change the character encoding used by the DSL parser. * @@ -91,7 +95,7 @@ IdentifierScope getIdentifierScope() { return identifierScope; } - void setIdentifierScope(IdentifierScope identifierScope) { + private void setIdentifierScope(IdentifierScope identifierScope) { if (identifierScope == null) { identifierScope = IdentifierScope.Flat; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java index 7f70fde3a..56bc1035e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java @@ -49,7 +49,7 @@ Workspace parse(DslParserContext context, Tokens tokens) { structurizrDslParser.setRestricted(context.isRestricted()); structurizrDslParser.parse(context, dsl); workspace = structurizrDslParser.getWorkspace(); - context.getParser().setIdentifierScope(structurizrDslParser.getIdentifierScope()); + context.getParser().configureFrom(structurizrDslParser); } } else { if (context.isRestricted()) { @@ -73,7 +73,7 @@ Workspace parse(DslParserContext context, Tokens tokens) { StructurizrDslParser structurizrDslParser = new StructurizrDslParser(); structurizrDslParser.parse(context, file); workspace = structurizrDslParser.getWorkspace(); - context.getParser().setIdentifierScope(structurizrDslParser.getIdentifierScope()); + context.getParser().configureFrom(structurizrDslParser); } } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index be661dd21..7be992a73 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1501,6 +1501,33 @@ void test_archetypesForDefaults() throws Exception { assertEquals("Default Perspective Description", (r.getPerspectives().stream().filter(p -> p.getName().equals("Default Perspective Name")).findFirst().get().getDescription())); } + @Test + void test_archetypesFromWorkspaceExtension() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/archetypes-from-workspace-extension-child.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + + SoftwareSystem a = workspace.getModel().getSoftwareSystemWithName("A"); + assertEquals("Default Description", a.getDescription()); + assertTrue(a.hasTag("Default Tag")); + assertTrue(a.hasProperty("Default Property Name", "Default Property Value")); + assertEquals("Default Perspective Description", (a.getPerspectives().stream().filter(p -> p.getName().equals("Default Perspective Name")).findFirst().get().getDescription())); + + SoftwareSystem b = workspace.getModel().getSoftwareSystemWithName("B"); + assertEquals("Default Description", b.getDescription()); + assertTrue(b.hasTag("Default Tag")); + assertTrue(b.hasProperty("Default Property Name", "Default Property Value")); + assertEquals("Default Perspective Description", (b.getPerspectives().stream().filter(p -> p.getName().equals("Default Perspective Name")).findFirst().get().getDescription())); + + Relationship r = a.getEfferentRelationshipWith(b); + assertEquals("Default Description", r.getDescription()); + assertEquals("Default Technology", r.getTechnology()); + assertTrue(r.hasTag("Default Tag")); + assertTrue(r.hasProperty("Default Property Name", "Default Property Value")); + assertEquals("Default Perspective Description", (r.getPerspectives().stream().filter(p -> p.getName().equals("Default Perspective Name")).findFirst().get().getDescription())); + } + @Test void test_archetypesForExtension() throws Exception { File parentDslFile = new File("src/test/resources/dsl/archetypes-for-extension.dsl"); diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-child.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-child.dsl new file mode 100644 index 000000000..0b5d03293 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-child.dsl @@ -0,0 +1,9 @@ +workspace extends archetypes-from-workspace-extension-parent.dsl { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + a -> b + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-parent.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-parent.dsl new file mode 100644 index 000000000..2ae521527 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-from-workspace-extension-parent.dsl @@ -0,0 +1,34 @@ +workspace { + + model { + archetypes { + softwaresystem { + description "Default Description" + tag "Default Tag" + + properties { + "Default Property Name" "Default Property Value" + } + + perspectives { + "Default Perspective Name" "Default Perspective Description" + } + } + + -> { + description "Default Description" + technology "Default Technology" + tag "Default Tag" + + properties { + "Default Property Name" "Default Property Value" + } + + perspectives { + "Default Perspective Name" "Default Perspective Description" + } + } + } + } + +} \ No newline at end of file From 8a9db84ec42ce9b28756ca4afb6ae4b3ee31160f Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:50:15 +0000 Subject: [PATCH 334/418] Closes #392. --- changelog.md | 1 + .../java/com/structurizr/util/ImageUtils.java | 6 +-- .../com/structurizr/util/ImageUtilsTests.java | 3 +- .../com/structurizr/dsl/BrandingParser.java | 7 +--- .../structurizr/dsl/ElementStyleParser.java | 7 +--- .../java/com/structurizr/dsl/IconUtils.java | 38 ------------------- 6 files changed, 10 insertions(+), 52 deletions(-) delete mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java diff --git a/changelog.md b/changelog.md index 3789f2f40..6045876c8 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## v4.0.1 (unreleased) - structurizr-dsl: Allows archetypes to be used via workspace extension. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/392 (SVG not supported in base 64 encoding not mentioned in documentation). ## v4.0.0 (28th March 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java index a73e43269..21cc4f25a 100644 --- a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java +++ b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java @@ -136,18 +136,18 @@ public static void validateImage(String imageDescriptor) { return; } - if (imageDescriptor.toLowerCase().endsWith(".png") || imageDescriptor.toLowerCase().endsWith(".jpg") || imageDescriptor.toLowerCase().endsWith(".jpeg") || imageDescriptor.toLowerCase().endsWith(".gif")) { + if (imageDescriptor.toLowerCase().endsWith(".png") || imageDescriptor.toLowerCase().endsWith(".jpg") || imageDescriptor.toLowerCase().endsWith(".jpeg") || imageDescriptor.toLowerCase().endsWith(".gif") || imageDescriptor.toLowerCase().endsWith(".svg")) { // it's just a filename return; } if (imageDescriptor.startsWith(DATA_URI_PREFIX)) { if (ImageUtils.isSupportedDataUri(imageDescriptor)) { - // it's a PNG/JPG data URI + // it's a PNG/JPG/SVG data URI return; } else { // it's a data URI, but not supported - throw new IllegalArgumentException("Only PNG and JPG data URIs are supported: " + imageDescriptor); + throw new IllegalArgumentException("Only PNG, JPG, and SVG data URIs are supported: " + imageDescriptor); } } diff --git a/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java index fea1a3397..fe5b5883f 100644 --- a/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java @@ -214,6 +214,7 @@ void validateImage() { ImageUtils.validateImage("image.jpg"); ImageUtils.validateImage("image.jpeg"); ImageUtils.validateImage("image.gif"); + ImageUtils.validateImage("image.svg"); ImageUtils.validateImage("data:image/svg+xml;utf8,iVBORw0KGg"); //disallowed @@ -221,7 +222,7 @@ void validateImage() { ImageUtils.validateImage("data:image/other"); fail(); } catch (Exception e) { - assertEquals("Only PNG and JPG data URIs are supported: data:image/other", e.getMessage()); + assertEquals("Only PNG, JPG, and SVG data URIs are supported: data:image/other", e.getMessage()); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java index 62c08fd4b..b4f10353d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java @@ -24,11 +24,8 @@ void parseLogo(BrandingDslContext context, Tokens tokens, boolean restricted) { String path = tokens.get(1); if (path.startsWith("data:image/") || path.startsWith("https://") || path.startsWith("http://")) { - if (IconUtils.isSupported(path)) { - context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(path); - } else { - throw new IllegalArgumentException("Only PNG and JPG URLs and data URIs are supported: " + path); - } + ImageUtils.validateImage(path); + context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(path); } else { if (!restricted) { File file = new File(context.getFile().getParent(), path); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java index 1769ec5c9..039abab6c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java @@ -297,11 +297,8 @@ void parseIcon(ElementStyleDslContext context, Tokens tokens, boolean restricted String path = tokens.get(1); if (path.startsWith("data:image/") || path.startsWith("https://") || path.startsWith("http://")) { - if (IconUtils.isSupported(path)) { - style.setIcon(path); - } else { - throw new IllegalArgumentException("Only PNG and JPG URLs/data URIs are supported: " + path); - } + ImageUtils.validateImage(path); + style.setIcon(path); } else { if (!restricted) { File file = new File(context.getFile().getParent(), path); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java deleted file mode 100644 index b503c0d5f..000000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/IconUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.util.Url; - -class IconUtils { - - public static boolean isSupported(String url) { - url = url.trim(); - - if (Url.isUrl(url) && isSupportedUrl(url)) { - // all good - return true; - } - - if (url.startsWith("data:image")) { - if (isSupportedDataUri(url)) { - // all good - return true; - } else { - // it's a data URI, but not supported - return false; - } - } - - return false; - } - - private static boolean isSupportedDataUri(String uri) { - return uri.startsWith("data:image/png;base64,") || uri.startsWith("data:image/jpeg;base64,"); - } - - private static boolean isSupportedUrl(String url) { - url = url.toLowerCase(); - - return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg"); - } - -} \ No newline at end of file From 7c448e189f86df57bbdcebc43122e2cf9d9f03ac Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Mon, 7 Apr 2025 10:09:12 +0100 Subject: [PATCH 335/418] structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. --- changelog.md | 1 + .../export/plantuml/StructurizrPlantUMLExporter.java | 6 ++++++ .../export/plantuml/structurizr/36141-Components.puml | 2 ++ .../export/plantuml/structurizr/36141-Containers.puml | 2 ++ .../plantuml/structurizr/36141-DevelopmentDeployment.puml | 2 ++ .../export/plantuml/structurizr/36141-LiveDeployment.puml | 2 ++ .../export/plantuml/structurizr/36141-SignIn.puml | 2 ++ .../export/plantuml/structurizr/36141-SystemContext.puml | 2 ++ .../export/plantuml/structurizr/36141-SystemLandscape.puml | 2 ++ .../structurizr/54915-AmazonWebServicesDeployment.puml | 2 ++ .../export/plantuml/structurizr/groups-Components.puml | 2 ++ .../export/plantuml/structurizr/groups-Containers.puml | 2 ++ .../export/plantuml/structurizr/groups-SystemLandscape.puml | 2 ++ 13 files changed, 29 insertions(+) diff --git a/changelog.md b/changelog.md index 6045876c8..ada09c66d 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - structurizr-dsl: Allows archetypes to be used via workspace extension. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/392 (SVG not supported in base 64 encoding not mentioned in documentation). +- structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. ## v4.0.0 (28th March 2025) diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 320213b11..2c8d3c775 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -42,6 +42,12 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { writer.writeLine("top to bottom direction"); break; } + + // the default 300px rank separation in the Structurizr UI is equivalent to a default of 60 in PlantUML + writer.writeLine("skinparam ranksep " + view.getAutomaticLayout().getRankSeparation() / (300/60)); + + // the default 300px node separation in the Structurizr UI is equivalent to a default of 30 in PlantUML + writer.writeLine("skinparam nodesep " + view.getAutomaticLayout().getNodeSeparation() / (300/30)); } else { writer.writeLine("top to bottom direction"); } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml index abd0c8043..09c551dda 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml @@ -3,6 +3,8 @@ set separator none title Internet Banking System - API Application - Components top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml index 40963b9e9..dab94a130 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml @@ -3,6 +3,8 @@ set separator none title Internet Banking System - Containers top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml index 59d7c4b30..b09bc9df7 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml @@ -3,6 +3,8 @@ set separator none title Internet Banking System - Deployment - Development top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml index 413f4b3a3..3f6d368ca 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml @@ -3,6 +3,8 @@ set separator none title Internet Banking System - Deployment - Live top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml index fc0b86f0e..f70ca4a45 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml @@ -3,6 +3,8 @@ set separator none title API Application - Dynamic top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml index 5b0dc94ec..a3a399968 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml @@ -3,6 +3,8 @@ set separator none title Internet Banking System - System Context top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml index 0518cb173..89d127718 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml @@ -3,6 +3,8 @@ set separator none title System Landscape top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml index a32f9c329..f1753149b 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml @@ -3,6 +3,8 @@ set separator none title Spring PetClinic - Deployment - Live left to right direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml index 8528285cb..3a899a68f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml @@ -3,6 +3,8 @@ set separator none title D - F - Components top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml index fe0ad43b9..6b63e7509 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml @@ -3,6 +3,8 @@ set separator none title D - Containers top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml index 7c398d745..09aa97ddd 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml @@ -3,6 +3,8 @@ set separator none title System Landscape top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 skinparam { arrowFontSize 10 From 1bc95cc9d6f160d1d8ba8f6029c5e76c9d3a226e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 8 Apr 2025 17:03:30 +0100 Subject: [PATCH 336/418] structurizr-dsl: Adds archetype support for custom elements. --- changelog.md | 1 + .../java/com/structurizr/dsl/Archetype.java | 9 ++++++++ .../com/structurizr/dsl/ArchetypeParser.java | 14 +++++++++++++ .../dsl/CustomElementArchetypeDslContext.java | 21 +++++++++++++++++++ .../structurizr/dsl/CustomElementParser.java | 11 +++++----- .../structurizr/dsl/StructurizrDslParser.java | 19 +++++++++++++++-- .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../dsl/CustomElementParserTests.java | 13 ++++++------ .../java/com/structurizr/dsl/DslTests.java | 12 +++++++++++ .../dsl/archetypes-for-custom-elements.dsl | 17 +++++++++++++++ 10 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementArchetypeDslContext.java create mode 100644 structurizr-dsl/src/test/resources/dsl/archetypes-for-custom-elements.dsl diff --git a/changelog.md b/changelog.md index ada09c66d..2caaad35e 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## v4.0.1 (unreleased) - structurizr-dsl: Allows archetypes to be used via workspace extension. +- structurizr-dsl: Adds archetype support for custom elements. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/392 (SVG not supported in base 64 encoding not mentioned in documentation). - structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java index 2a0b0fbe3..71a92fa0a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java @@ -11,6 +11,7 @@ final class Archetype implements PropertyHolder, PerspectivesHolder { private final String name; private final String type; + private String metadata = ""; private String description = ""; private String technology = ""; private final Set tags = new LinkedHashSet<>(); @@ -35,6 +36,14 @@ String getType() { return type; } + String getMetadata() { + return metadata; + } + + void setMetadata(String metadata) { + this.metadata = metadata; + } + String getDescription() { return description; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java index 3c78a1ba6..1b5c00668 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java @@ -31,6 +31,20 @@ void parseTags(ArchetypeDslContext context, Tokens tokens) { } } + void parseMetadata(ArchetypeDslContext context, Tokens tokens) { + // metadata + if (tokens.hasMoreThan(VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: metadata "); + } + + if (!tokens.includes(NAME_INDEX)) { + throw new RuntimeException("Expected: metadata "); + } + + String metadata = tokens.get(VALUE_INDEX); + context.getArchetype().setMetadata(metadata); + } + void parseDescription(ArchetypeDslContext context, Tokens tokens) { // description if (tokens.hasMoreThan(VALUE_INDEX)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementArchetypeDslContext.java new file mode 100644 index 000000000..4bad680b8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementArchetypeDslContext.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class CustomElementArchetypeDslContext extends ElementArchetypeDslContext { + + CustomElementArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.METADATA_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java index 493b573b9..341918a87 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java @@ -13,7 +13,7 @@ final class CustomElementParser extends AbstractParser { private final static int DESCRIPTION_INDEX = 3; private final static int TAGS_INDEX = 4; - CustomElement parse(ModelDslContext context, Tokens tokens) { + CustomElement parse(ModelDslContext context, Tokens tokens, Archetype archetype) { // element [metadata] [description] [tags] if (tokens.hasMoreThan(TAGS_INDEX)) { @@ -26,22 +26,23 @@ CustomElement parse(ModelDslContext context, Tokens tokens) { String name = tokens.get(NAME_INDEX); - String metadata = ""; + String metadata = archetype.getMetadata(); if (tokens.includes(METADATA_INDEX)) { metadata = tokens.get(METADATA_INDEX); } - String description = ""; + String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); } CustomElement customElement = context.getWorkspace().getModel().addCustomElement(name, metadata, description); + String[] tags = archetype.getTags().toArray(new String[0]); if (tokens.includes(TAGS_INDEX)) { - String tags = tokens.get(TAGS_INDEX); - customElement.addTags(tags.split(",")); + tags = tokens.get(TAGS_INDEX).split(","); } + customElement.addTags(tags); if (context.hasGroup()) { customElement.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 856edfd80..89e9f7adf 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -49,6 +49,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private Map> archetypes = Map.of( StructurizrDslTokens.GROUP_TOKEN, new HashMap<>(), + StructurizrDslTokens.CUSTOM_ELEMENT_TOKEN, new HashMap<>(), StructurizrDslTokens.PERSON_TOKEN, new HashMap<>(), StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, new HashMap<>(), StructurizrDslTokens.CONTAINER_TOKEN, new HashMap<>(), @@ -421,8 +422,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new RelationshipsDslContext(getContext(), relationships)); } - } else if (CUSTOM_ELEMENT_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ModelDslContext.class))) { - CustomElement customElement = new CustomElementParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken()); + } else if (isElementKeywordOrArchetype(firstToken, CUSTOM_ELEMENT_TOKEN) && (inContext(ModelDslContext.class))) { + Archetype archetype = getArchetype(CUSTOM_ELEMENT_TOKEN, firstToken); + CustomElement customElement = new CustomElementParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken(), archetype); if (shouldStartContext(tokens)) { startContext(new CustomElementDslContext(customElement)); @@ -673,6 +675,15 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn extendArchetype(archetype, firstToken); addArchetype(archetype); + } else if (isElementKeywordOrArchetype(firstToken, CUSTOM_ELEMENT_TOKEN) && inContext(ArchetypesDslContext.class)) { + Archetype archetype = new Archetype(identifier, CUSTOM_ELEMENT_TOKEN); + extendArchetype(archetype, firstToken); + addArchetype(archetype); + + if (shouldStartContext(tokens)) { + startContext(new CustomElementArchetypeDslContext(archetype)); + } + } else if (isElementKeywordOrArchetype(firstToken, PERSON_TOKEN) && inContext(ArchetypesDslContext.class)) { Archetype archetype = new Archetype(identifier, PERSON_TOKEN); extendArchetype(archetype, firstToken); @@ -737,6 +748,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new RelationshipArchetypeDslContext(archetype)); } + } else if (METADATA_TOKEN.equalsIgnoreCase(firstToken) && inContext(CustomElementArchetypeDslContext.class)) { + new ArchetypeParser().parseMetadata(getContext(ArchetypeDslContext.class), tokens); + } else if (DESCRIPTION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { new ArchetypeParser().parseDescription(getContext(ArchetypeDslContext.class), tokens); @@ -1384,6 +1398,7 @@ private void extendArchetype(Archetype archetype, String archetypeName) { archetypeName = archetypeName.toLowerCase(); Archetype parentArchetype = getArchetype(archetype.getType(), archetypeName); + archetype.setMetadata(parentArchetype.getMetadata()); archetype.setDescription(parentArchetype.getDescription()); archetype.setTechnology(parentArchetype.getTechnology()); archetype.addTags(parentArchetype.getTags().toArray(new String[0])); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index fef341aad..ae2d1ecd8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -15,6 +15,7 @@ class StructurizrDslTokens { static final String COMPONENT_TOKEN = "component"; static final String GROUP_TOKEN = "group"; static final String NAME_TOKEN = "name"; + static final String METADATA_TOKEN = "metadata"; static final String DESCRIPTION_TOKEN = "description"; static final String TECHNOLOGY_TOKEN = "technology"; static final String INSTANCES_TOKEN = "instances"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java index 37c108b19..c216fbba8 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java @@ -8,11 +8,12 @@ class CustomElementParserTests extends AbstractTests { private CustomElementParser parser = new CustomElementParser(); + private Archetype archetype = new Archetype("name", "type"); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(), tokens("element", "name", "metadata", "description", "tags", "extra")); + parser.parse(context(), tokens("element", "name", "metadata", "description", "tags", "extra"), archetype); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: element [metadata] [description] [tags]", e.getMessage()); @@ -22,7 +23,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { try { - parser.parse(context(), tokens("element")); + parser.parse(context(), tokens("element"), archetype); fail(); } catch (Exception e) { assertEquals("Expected: element [metadata] [description] [tags]", e.getMessage()); @@ -31,7 +32,7 @@ void test_parse_ThrowsAnException_WhenTheNameIsNotSpecified() { @Test void test_parse_CreatesACustomElement() { - parser.parse(context(), tokens("element", "Name")); + parser.parse(context(), tokens("element", "Name"), archetype); assertEquals(1, model.getElements().size()); CustomElement element = model.getCustomElementWithName("Name"); @@ -42,7 +43,7 @@ void test_parse_CreatesACustomElement() { @Test void test_parse_CreatesACustomElementWithMetadata() { - parser.parse(context(), tokens("element", "Name", "Box")); + parser.parse(context(), tokens("element", "Name", "Box"), archetype); assertEquals(1, model.getElements().size()); CustomElement element = model.getCustomElementWithName("Name"); @@ -54,7 +55,7 @@ void test_parse_CreatesACustomElementWithMetadata() { @Test void test_parse_CreatesACustomElementWithMetadataAndDescription() { - parser.parse(context(), tokens("element", "Name", "Box", "Description")); + parser.parse(context(), tokens("element", "Name", "Box", "Description"), archetype); assertEquals(1, model.getElements().size()); CustomElement element = model.getCustomElementWithName("Name"); @@ -66,7 +67,7 @@ void test_parse_CreatesACustomElementWithMetadataAndDescription() { @Test void test_parse_CreatesACustomElementWithMetadataAndDescriptionAndTags() { - parser.parse(context(), tokens("element", "Name", "Box", "Description", "Tag 1, Tag 2")); + parser.parse(context(), tokens("element", "Name", "Box", "Description", "Tag 1, Tag 2"), archetype); assertEquals(1, model.getElements().size()); CustomElement element = model.getCustomElementWithName("Name"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 7be992a73..cb957a545 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1474,6 +1474,18 @@ void test_archetypes() throws Exception { assertEquals("HTTPS", relationship.getTechnology()); } + @Test + void test_archetypesForCustomElements() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/archetypes-for-custom-elements.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + + CustomElement b = workspace.getModel().getCustomElementWithName("B"); + assertEquals("Hardware System", b.getMetadata()); + assertTrue(b.getTagsAsSet().contains("Hardware System")); + } + @Test void test_archetypesForDefaults() throws Exception { File parentDslFile = new File("src/test/resources/dsl/archetypes-for-defaults.dsl"); diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-for-custom-elements.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-for-custom-elements.dsl new file mode 100644 index 000000000..c3d2a3cf7 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-custom-elements.dsl @@ -0,0 +1,17 @@ +workspace { + + model { + archetypes { + hardwareSystem = element { + metadata "Hardware System" + tag "Hardware System" + } + } + + a = softwareSystem "A" + b = hardwareSystem "B" + + a -> b "Gets data from" + } + +} \ No newline at end of file From c44696f143cbcccff0b3fd102a8bf89177ad832e Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:47:42 +0100 Subject: [PATCH 337/418] structurizr-dsl: Adds support for setting the symbols surrounding element/relationship metadata used when rendering diagrams. --- changelog.md | 1 + .../structurizr/dsl/StructurizrDslParser.java | 3 ++ .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../dsl/TerminologyDslContext.java | 3 +- .../structurizr/dsl/TerminologyParser.java | 31 ++++++++++++++ .../dsl/TerminologyParserTests.java | 42 +++++++++++++++++++ .../src/test/resources/dsl/test.dsl | 1 + 7 files changed, 81 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 2caaad35e..029b93e2c 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ - structurizr-dsl: Allows archetypes to be used via workspace extension. - structurizr-dsl: Adds archetype support for custom elements. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/392 (SVG not supported in base 64 encoding not mentioned in documentation). +- structurizr-dsl: Adds support for setting the symbols surrounding element/relationship metadata used when rendering diagrams. - structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. ## v4.0.0 (28th March 2025) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 89e9f7adf..563d40dcd 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1082,6 +1082,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (TERMINOLOGY_RELATIONSHIP_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { new TerminologyParser().parseRelationship(getContext(), tokens); + } else if (METADATA_SYMBOLS_TOKEN.equalsIgnoreCase(firstToken) && inContext(TerminologyDslContext.class)) { + new TerminologyParser().parseMetadataSymbols(getContext(), tokens); + } else if (CONFIGURATION_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { startContext(new ConfigurationDslContext()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index ae2d1ecd8..9b295eff0 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -95,6 +95,7 @@ class StructurizrDslTokens { static final String VISIBILITY_TOKEN = "visibility"; static final String TERMINOLOGY_TOKEN = "terminology"; static final String TERMINOLOGY_RELATIONSHIP_TOKEN = "relationship"; + static final String METADATA_SYMBOLS_TOKEN = "metadata"; static final String USERS_TOKEN = "users"; static final String THIS_TOKEN = "this"; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java index be42d7490..32bc07e81 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyDslContext.java @@ -11,7 +11,8 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.COMPONENT_TOKEN, StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, - StructurizrDslTokens.TERMINOLOGY_RELATIONSHIP_TOKEN + StructurizrDslTokens.TERMINOLOGY_RELATIONSHIP_TOKEN, + StructurizrDslTokens.METADATA_SYMBOLS_TOKEN }; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java index d95e2409c..1fdaab43b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java @@ -1,8 +1,13 @@ package com.structurizr.dsl; +import com.structurizr.view.MetadataSymbols; + +import java.util.*; + final class TerminologyParser extends AbstractParser { private final static int TERM_INDEX = 1; + private final static int SYMBOL_TYPE_INDEX = 1; void parsePerson(DslContext context, Tokens tokens) { // person @@ -67,4 +72,30 @@ void parseRelationship(DslContext context, Tokens tokens) { context.getWorkspace().getViews().getConfiguration().getTerminology().setRelationship(tokens.get(TERM_INDEX)); } + void parseMetadataSymbols(DslContext context, Tokens tokens) { + Map symbols = new LinkedHashMap<>(); + symbols.put("square", MetadataSymbols.SquareBrackets); + symbols.put("round", MetadataSymbols.RoundBrackets); + symbols.put("curly", MetadataSymbols.CurlyBrackets); + symbols.put("angle", MetadataSymbols.AngleBrackets); + symbols.put("double-angle", MetadataSymbols.DoubleAngleBrackets); + symbols.put("none", MetadataSymbols.None); + + String symbolsAsString = String.join("|", symbols.keySet()); + + // metadata + if (!tokens.includes(SYMBOL_TYPE_INDEX)) { + throw new RuntimeException("Expected: metadata <" + symbolsAsString + ">"); + } + + String symbolAsString = tokens.get(SYMBOL_TYPE_INDEX).toLowerCase(); + MetadataSymbols symbol = symbols.get(symbolAsString); + if (symbol != null) { + context.getWorkspace().getViews().getConfiguration().setMetadataSymbols(symbol); + } else { + throw new RuntimeException("The symbol type \"" + symbolAsString + "\" is not valid"); + } + + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java index 5d4592627..d0438c6b6 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.view.MetadataSymbols; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -128,4 +129,45 @@ void test_parseRelationship_SetsTheTerm_WhenOneIsSpecified() { assertEquals("TERM", workspace.getViews().getConfiguration().getTerminology().getRelationship()); } + @Test + void test_parseMetadataSymbols_ThrowsAnException_WhenNoSymbolTypeIsSpecified() { + try { + parser.parseMetadataSymbols(context(), tokens("metadata")); + fail(); + } catch (Exception e) { + assertEquals("Expected: metadata ", e.getMessage()); + } + } + + @Test + void test_parseMetadataSymbols_ThrowsAnException_WhenAnInvalidSymbolTypeIsSpecified() { + try { + parser.parseMetadataSymbols(context(), tokens("metadata", "invalid")); + fail(); + } catch (Exception e) { + assertEquals("The symbol type \"invalid\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseMetadataSymbols_SetsTheMetadataSymbols_WhenSpecified() { + parser.parseMetadataSymbols(context(), tokens("metadata", "square")); + assertEquals(MetadataSymbols.SquareBrackets, workspace.getViews().getConfiguration().getMetadataSymbols()); + + parser.parseMetadataSymbols(context(), tokens("metadata", "round")); + assertEquals(MetadataSymbols.RoundBrackets, workspace.getViews().getConfiguration().getMetadataSymbols()); + + parser.parseMetadataSymbols(context(), tokens("metadata", "curly")); + assertEquals(MetadataSymbols.CurlyBrackets, workspace.getViews().getConfiguration().getMetadataSymbols()); + + parser.parseMetadataSymbols(context(), tokens("metadata", "angle")); + assertEquals(MetadataSymbols.AngleBrackets, workspace.getViews().getConfiguration().getMetadataSymbols()); + + parser.parseMetadataSymbols(context(), tokens("metadata", "double-angle")); + assertEquals(MetadataSymbols.DoubleAngleBrackets, workspace.getViews().getConfiguration().getMetadataSymbols()); + + parser.parseMetadataSymbols(context(), tokens("metadata", "none")); + assertEquals(MetadataSymbols.None, workspace.getViews().getConfiguration().getMetadataSymbols()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 045aa311d..c6150affd 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -359,6 +359,7 @@ workspace "Name" "Description" { deploymentNode "Deployment Node" infrastructureNode "Infrastructure Node" relationship "Relationship" + metadata angle } properties { From 518da43784c5227adbee5baa7443a3be02759c94 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 10 Apr 2025 08:22:51 +0100 Subject: [PATCH 338/418] Fixes #399. --- changelog.md | 1 + .../com/structurizr/dsl/ComponentParser.java | 10 ++- .../com/structurizr/dsl/ContainerParser.java | 10 ++- .../structurizr/dsl/CustomElementParser.java | 10 ++- .../structurizr/dsl/DeploymentNodeParser.java | 85 +++++++------------ .../dsl/ExplicitRelationshipParser.java | 9 +- .../dsl/ImplicitRelationshipParser.java | 9 +- .../dsl/InfrastructureNodeParser.java | 10 ++- .../com/structurizr/dsl/PersonParser.java | 10 ++- .../structurizr/dsl/SoftwareSystemParser.java | 10 ++- .../structurizr/dsl/ComponentParserTests.java | 20 +++++ .../structurizr/dsl/ContainerParserTests.java | 19 +++++ .../dsl/CustomElementParserTests.java | 16 ++++ .../dsl/DeploymentNodeParserTests.java | 43 ++++++++++ .../dsl/ExplicitRelationshipParserTests.java | 29 +++++++ .../dsl/ImplicitRelationshipParserTests.java | 28 ++++++ .../dsl/InfrastructureNodeParserTests.java | 22 +++++ .../structurizr/dsl/PersonParserTests.java | 15 ++++ .../dsl/SoftwareSystemParserTests.java | 15 ++++ 19 files changed, 287 insertions(+), 84 deletions(-) diff --git a/changelog.md b/changelog.md index 029b93e2c..f90c8ca3d 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - structurizr-dsl: Allows archetypes to be used via workspace extension. - structurizr-dsl: Adds archetype support for custom elements. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/399 (Archetype tags sometimes missing). - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/392 (SVG not supported in base 64 encoding not mentioned in documentation). - structurizr-dsl: Adds support for setting the symbols surrounding element/relationship metadata used when rendering diagrams. - structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java index 0b587d2e8..cb3f88098 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java @@ -3,6 +3,10 @@ import com.structurizr.model.Component; import com.structurizr.model.Container; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + final class ComponentParser extends AbstractParser { private static final String GRAMMAR = "component [description] [technology] [tags]"; @@ -47,11 +51,11 @@ Component parse(ContainerDslContext context, Tokens tokens, Archetype archetype) } component.setTechnology(technology); - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - component.addTags(tags); + component.addTags(tags.toArray(new String[0])); component.addProperties(archetype.getProperties()); component.addPerspectives(archetype.getPerspectives()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java index c8abc1ec8..33e81e553 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java @@ -3,6 +3,10 @@ import com.structurizr.model.Container; import com.structurizr.model.SoftwareSystem; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + final class ContainerParser extends AbstractParser { private static final String GRAMMAR = "container [description] [technology] [tags]"; @@ -47,11 +51,11 @@ Container parse(SoftwareSystemDslContext context, Tokens tokens, Archetype arche } container.setTechnology(technology); - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - container.addTags(tags); + container.addTags(tags.toArray(new String[0])); container.addProperties(archetype.getProperties()); container.addPerspectives(archetype.getPerspectives()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java index 341918a87..06d9e6e65 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java @@ -4,6 +4,10 @@ import com.structurizr.model.Location; import com.structurizr.model.Person; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + final class CustomElementParser extends AbstractParser { private static final String GRAMMAR = "element [metadata] [description] [tags]"; @@ -38,11 +42,11 @@ CustomElement parse(ModelDslContext context, Tokens tokens, Archetype archetype) CustomElement customElement = context.getWorkspace().getModel().addCustomElement(name, metadata, description); - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - customElement.addTags(tags); + customElement.addTags(tags.toArray(new String[0])); if (context.hasGroup()) { customElement.setGroup(context.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java index b2a5a2c3d..93866302b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java @@ -1,6 +1,11 @@ package com.structurizr.dsl; import com.structurizr.model.DeploymentNode; +import com.structurizr.model.Model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; final class DeploymentNodeParser extends AbstractParser { @@ -13,55 +18,14 @@ final class DeploymentNodeParser extends AbstractParser { private static final int INSTANCES_INDEX = 5; DeploymentNode parse(DeploymentEnvironmentDslContext context, Tokens tokens, Archetype archetype) { - // deploymentNode [description] [technology] [tags] [instances] - - if (tokens.hasMoreThan(INSTANCES_INDEX)) { - throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); - } - - if (!tokens.includes(NAME_INDEX)) { - throw new RuntimeException("Expected: " + GRAMMAR); - } - - DeploymentNode deploymentNode = null; - String name = tokens.get(NAME_INDEX); - - String description = archetype.getDescription(); - if (tokens.includes(DESCRIPTION_INDEX)) { - description = tokens.get(DESCRIPTION_INDEX); - } - - String technology = archetype.getTechnology(); - if (tokens.includes(TECHNOLOGY_INDEX)) { - technology = tokens.get(TECHNOLOGY_INDEX); - } - - deploymentNode = context.getWorkspace().getModel().addDeploymentNode(context.getEnvironment(), name, description, technology); - - String[] tags = archetype.getTags().toArray(new String[0]); - if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); - } - deploymentNode.addTags(tags); - - deploymentNode.addProperties(archetype.getProperties()); - deploymentNode.addPerspectives(archetype.getPerspectives()); - - String instances = "1"; - if (tokens.includes(INSTANCES_INDEX)) { - instances = tokens.get(INSTANCES_INDEX); - deploymentNode.setInstances(instances); - } - - if (context.hasGroup()) { - deploymentNode.setGroup(context.getGroup().getName()); - context.getGroup().addElement(deploymentNode); - } - - return deploymentNode; + return parse(context, null, tokens, archetype); } DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens, Archetype archetype) { + return parse(null, context, tokens, archetype); + } + + DeploymentNode parse(DeploymentEnvironmentDslContext deploymentEnvironmentDslContext, DeploymentNodeDslContext deploymentNodeDslContext, Tokens tokens, Archetype archetype) { // deploymentNode [description] [technology] [tags] [instances] if (tokens.hasMoreThan(INSTANCES_INDEX)) { @@ -85,14 +49,28 @@ DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens, Archetype technology = tokens.get(TECHNOLOGY_INDEX); } - DeploymentNode parent = context.getDeploymentNode(); - deploymentNode = parent.addDeploymentNode(name, description, technology); + if (deploymentEnvironmentDslContext != null) { + // add a root deployment node + deploymentNode = deploymentEnvironmentDslContext.getWorkspace().getModel().addDeploymentNode(deploymentEnvironmentDslContext.getEnvironment(), name, description, technology); - String[] tags = archetype.getTags().toArray(new String[0]); + if (deploymentEnvironmentDslContext.hasGroup()) { + deploymentNode.setGroup(deploymentEnvironmentDslContext.getGroup().getName()); + deploymentEnvironmentDslContext.getGroup().addElement(deploymentNode); + } + } else { + deploymentNode = deploymentNodeDslContext.getDeploymentNode().addDeploymentNode(name, description, technology); + + if (deploymentNodeDslContext.hasGroup()) { + deploymentNode.setGroup(deploymentNodeDslContext.getGroup().getName()); + deploymentNodeDslContext.getGroup().addElement(deploymentNode); + } + } + + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - deploymentNode.addTags(tags); + deploymentNode.addTags(tags.toArray(new String[0])); deploymentNode.addProperties(archetype.getProperties()); deploymentNode.addPerspectives(archetype.getPerspectives()); @@ -103,11 +81,6 @@ DeploymentNode parse(DeploymentNodeDslContext context, Tokens tokens, Archetype deploymentNode.setInstances(instances); } - if (context.hasGroup()) { - deploymentNode.setGroup(context.getGroup().getName()); - context.getGroup().addElement(deploymentNode); - } - return deploymentNode; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index de846e382..38e225b22 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -4,8 +4,7 @@ import com.structurizr.model.Relationship; import javax.lang.model.util.Elements; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; final class ExplicitRelationshipParser extends AbstractRelationshipParser { @@ -50,12 +49,12 @@ Relationship parse(DslContext context, Tokens tokens, Archetype archetype) { technology = tokens.get(TECHNOLOGY_INDEX); } - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + Relationship relationship = createRelationship(sourceElement, description, technology, tags.toArray(new String[0]), destinationElement); relationship.addProperties(archetype.getProperties()); relationship.addPerspectives(archetype.getPerspectives()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index e3fb1df93..7c10b7015 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -3,8 +3,7 @@ import com.structurizr.model.Element; import com.structurizr.model.Relationship; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; final class ImplicitRelationshipParser extends AbstractRelationshipParser { @@ -45,12 +44,12 @@ Relationship parse(ElementDslContext context, Tokens tokens, Archetype archetype technology = tokens.get(TECHNOLOGY_INDEX); } - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + Relationship relationship = createRelationship(sourceElement, description, technology, tags.toArray(new String[0]), destinationElement); relationship.addProperties(archetype.getProperties()); relationship.addPerspectives(archetype.getPerspectives()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java index 964f989e9..8d8165a65 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java @@ -3,6 +3,10 @@ import com.structurizr.model.DeploymentNode; import com.structurizr.model.InfrastructureNode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + final class InfrastructureNodeParser extends AbstractParser { private static final String GRAMMAR = "infrastructureNode [description] [technology] [tags]"; @@ -39,11 +43,11 @@ InfrastructureNode parse(DeploymentNodeDslContext context, Tokens tokens, Archet infrastructureNode = deploymentNode.addInfrastructureNode(name, description, technology); - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - infrastructureNode.addTags(tags); + infrastructureNode.addTags(tags.toArray(new String[0])); infrastructureNode.addProperties(archetype.getProperties()); infrastructureNode.addPerspectives(archetype.getPerspectives()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java index 8525e5d36..992fbf6ca 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -2,6 +2,10 @@ import com.structurizr.model.Person; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + final class PersonParser extends AbstractParser { private static final String GRAMMAR = "person [description] [tags]"; @@ -38,11 +42,11 @@ Person parse(ModelDslContext context, Tokens tokens, Archetype archetype) { } person.setDescription(description); - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - person.addTags(tags); + person.addTags(tags.toArray(new String[0])); person.addProperties(archetype.getProperties()); person.addPerspectives(archetype.getPerspectives()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java index fd1ef8949..ffae2403e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -2,6 +2,10 @@ import com.structurizr.model.SoftwareSystem; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + final class SoftwareSystemParser extends AbstractParser { private static final String GRAMMAR = "softwareSystem [description] [tags]"; @@ -38,11 +42,11 @@ SoftwareSystem parse(ModelDslContext context, Tokens tokens, Archetype archetype } softwareSystem.setDescription(description); - String[] tags = archetype.getTags().toArray(new String[0]); + List tags = new ArrayList<>(archetype.getTags()); if (tokens.includes(TAGS_INDEX)) { - tags = tokens.get(TAGS_INDEX).split(","); + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - softwareSystem.addTags(tags); + softwareSystem.addTags(tags.toArray(new String[0])); softwareSystem.addProperties(archetype.getProperties()); softwareSystem.addPerspectives(archetype.getPerspectives()); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java index 77ddc23fa..babac9aab 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java @@ -92,6 +92,26 @@ void test_parse_CreatesAComponentWithADescriptionAndTechnologyAndTags() { assertEquals("Element,Component,Tag 1,Tag 2", component.getTags()); } + @Test + void test_parse_CreatesAComponentWithADescriptionAndTechnologyAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.setTechnology("Default Technology"); + archetype.addTags("Default Tag"); + + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ContainerDslContext context = new ContainerDslContext(container); + parser.parse(context, tokens("component", "Name", "Description", "Technology", "Tag 1, Tag 2"), archetype); + + assertEquals(3, model.getElements().size()); + Component component = container.getComponentWithName("Name"); + assertNotNull(component); + assertEquals("Description", component.getDescription()); // overridden from archetype + assertEquals("Technology", component.getTechnology()); // overridden from archetype + assertEquals("Element,Component,Default Tag,Tag 1,Tag 2", component.getTags()); + } + @Test void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { try { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java index 758f7503c..3215df02c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java @@ -87,6 +87,25 @@ void test_parse_CreatesAContainerWithADescriptionAndTechnologyAndTags() { assertEquals("Element,Container,Tag 1,Tag 2", container.getTags()); } + @Test + void test_parse_CreatesAContainerWithADescriptionAndTechnologyAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.setTechnology("Default Technology"); + archetype.addTags("Default Tag"); + + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + SoftwareSystemDslContext context = new SoftwareSystemDslContext(softwareSystem); + parser.parse(context, tokens("container", "Name", "Description", "Technology", "Tag 1, Tag 2"), archetype); + + assertEquals(2, model.getElements().size()); + Container container = softwareSystem.getContainerWithName("Name"); + assertNotNull(container); + assertEquals("Description", container.getDescription()); // overridden from archetype + assertEquals("Technology", container.getTechnology()); // overridden from archetype + assertEquals("Element,Container,Default Tag,Tag 1,Tag 2", container.getTags()); + } + @Test void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { try { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java index c216fbba8..27d48b963 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java @@ -77,4 +77,20 @@ void test_parse_CreatesACustomElementWithMetadataAndDescriptionAndTags() { assertEquals("Element,Tag 1,Tag 2", element.getTags()); } + @Test + void test_parse_CreatesACustomElementWithMetadataAndDescriptionAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.addTags("Default Tag"); + + parser.parse(context(), tokens("element", "Name", "Box", "Description", "Tag 1, Tag 2"), archetype); + + assertEquals(1, model.getElements().size()); + CustomElement element = model.getCustomElementWithName("Name"); + assertNotNull(element); + assertEquals("Box", element.getMetadata()); + assertEquals("Description", element.getDescription()); // overridden from archetype + assertEquals("Element,Default Tag,Tag 1,Tag 2", element.getTags()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java index 21bbd052c..f5694274e 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java @@ -111,6 +111,27 @@ void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnologyAndTagsAndIns assertEquals("Live", deploymentNode.getEnvironment()); } + @Test + void test_parse_CreatesADeploymentNodeWithADescriptionAndTechnologyAndTagsAndInstancesBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.setTechnology("Default Technology"); + archetype.addTags("Default Tag"); + + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "8"), archetype); + + assertEquals(1, model.getElements().size()); + DeploymentNode deploymentNode = model.getDeploymentNodeWithName("Name", "Live"); + assertNotNull(deploymentNode); + assertEquals("Description", deploymentNode.getDescription()); // overridden from archetype + assertEquals("Technology", deploymentNode.getTechnology()); // overridden from archetype + assertEquals("Element,Deployment Node,Default Tag,Tag 1,Tag 2", deploymentNode.getTags()); + assertEquals("8", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + @Test void test_parse_ThrowsAnException_WhenTheNumberOfInstancesIsNotValid() { DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("Live"); @@ -142,6 +163,28 @@ void test_parse_CreatesAChildDeploymentNode() { assertEquals("Live", deploymentNode.getEnvironment()); } + @Test + void test_parse_CreatesAChildDeploymentNodeWithADescriptionAndTechnologyAndTagsAndInstancesBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.setTechnology("Default Technology"); + archetype.addTags("Default Tag"); + + DeploymentNode parent = model.addDeploymentNode("Live", "Parent", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(parent); + context.setWorkspace(workspace); + parser.parse(context, tokens("deploymentNode", "Name", "Description", "Technology", "Tag 1, Tag 2", "8"), archetype); + + assertEquals(2, model.getElements().size()); + DeploymentNode deploymentNode = parent.getDeploymentNodeWithName("Name"); + assertNotNull(deploymentNode); + assertEquals("Description", deploymentNode.getDescription()); // overridden from archetype + assertEquals("Technology", deploymentNode.getTechnology()); // overridden from archetype + assertEquals("Element,Deployment Node,Default Tag,Tag 1,Tag 2", deploymentNode.getTags()); + assertEquals("8", deploymentNode.getInstances()); + assertEquals("Live", deploymentNode.getEnvironment()); + } + @Test void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { try { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java index 0ac671673..1cd8f213d 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java @@ -150,6 +150,35 @@ void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTags() { assertEquals("Relationship,Tag 1,Tag 2", r.getTags()); } + @Test + void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.setTechnology("Default Technology"); + archetype.addTags("Default Tag"); + + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DslContext context = context(); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("source", user); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("source", "->", "destination", "Uses", "HTTP", "Tag 1,Tag 2"), archetype); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); // overridden from archetype + assertEquals("HTTP", r.getTechnology()); // overridden from archetype + assertEquals("Relationship,Default Tag,Tag 1,Tag 2", r.getTags()); + } + @Test void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTechnologyAndTags() { Person user = model.addPerson("User", "Description"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java index a84c7fd06..fe6b84a61 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java @@ -144,6 +144,34 @@ void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTags() { assertEquals("Relationship,Tag 1,Tag 2", r.getTags()); } + @Test + void test_parse_AddsTheRelationshipWithADescriptionAndTechnologyAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.setTechnology("Default Technology"); + archetype.addTags("Default Tag"); + + Person user = model.addPerson("User", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + ElementDslContext context = context(user); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("destination", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + parser.parse(context, tokens("->", "destination", "Uses", "HTTP", "Tag 1,Tag 2"), archetype); + + assertEquals(1, model.getRelationships().size()); + Relationship r = model.getRelationships().iterator().next(); + assertSame(user, r.getSource()); + assertSame(softwareSystem, r.getDestination()); + assertEquals("Uses", r.getDescription()); // overridden from archetype + assertEquals("HTTP", r.getTechnology()); // overridden from archetype + assertEquals("Relationship,Default Tag,Tag 1,Tag 2", r.getTags()); + } + @Test void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTechnologyAndTags() { Person user = model.addPerson("User", "Description"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java index d631709cc..1fbd90759 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java @@ -99,6 +99,28 @@ void test_parse_CreatesAnInfrastructureNodeWithADescriptionAndTechnologyAndTags( assertEquals("Live", infrastructureNode.getEnvironment()); } + @Test + void test_parse_CreatesAnInfrastructureNodeWithADescriptionAndTechnologyAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.setTechnology("Default Technology"); + archetype.addTags("Default Tag"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + + parser.parse(context, tokens("infrastructureNode", "Name", "Description", "Technology", "Tag 1, Tag 2"), archetype); + + assertEquals(2, model.getElements().size()); + assertEquals(1, deploymentNode.getInfrastructureNodes().size()); + InfrastructureNode infrastructureNode = deploymentNode.getInfrastructureNodeWithName("Name"); + assertNotNull(infrastructureNode); + assertEquals("Description", infrastructureNode.getDescription()); // overridden from archetype + assertEquals("Technology", infrastructureNode.getTechnology()); // overridden from archetype + assertEquals("Element,Infrastructure Node,Default Tag,Tag 1,Tag 2", infrastructureNode.getTags()); + assertEquals("Live", infrastructureNode.getEnvironment()); + } + @Test void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { try { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java index dac705b99..38960e0e0 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java @@ -63,4 +63,19 @@ void test_parse_CreatesAPersonWithADescriptionAndTags() { assertEquals("Element,Person,Tag 1,Tag 2", user.getTags()); } + @Test + void test_parse_CreatesAPersonWithADescriptionAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.addTags("Default Tag"); + + parser.parse(context(), tokens("person", "User", "Description", "Tag 1, Tag 2"), archetype); + + assertEquals(1, model.getElements().size()); + Person user = model.getPersonWithName("User"); + assertNotNull(user); + assertEquals("Description", user.getDescription()); // overridden from archetype + assertEquals("Element,Person,Default Tag,Tag 1,Tag 2", user.getTags()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java index b33974520..29f9b7840 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java @@ -63,4 +63,19 @@ void test_parse_CreatesASoftwareSystemWithADescriptionAndTags() { assertEquals("Element,Software System,Tag 1,Tag 2", softwareSystem.getTags()); } + @Test + void test_parse_CreatesASoftwareSystemWithADescriptionAndTagsBasedUponAnArchetype() { + archetype = new Archetype("name", "type"); + archetype.setDescription("Default Description"); + archetype.addTags("Default Tag"); + + parser.parse(context(), tokens("softwareSystem", "Name", "Description", "Tag 1, Tag 2"), archetype); + + assertEquals(1, model.getElements().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("Description", softwareSystem.getDescription()); // overridden from archetype + assertEquals("Element,Software System,Default Tag,Tag 1,Tag 2", softwareSystem.getTags()); + } + } \ No newline at end of file From 4e8cfbfb064e03221ba614a0c7dd1f126892b41e Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:30:34 +0100 Subject: [PATCH 339/418] Fixes #413. --- changelog.md | 1 + .../src/main/java/com/structurizr/api/WorkspaceApiClient.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index f90c8ca3d..173f44310 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## v4.0.1 (unreleased) +- structurizr-client: Fixes https://github.com/structurizr/java/issues/413 (Cannot push to main branch, when branch feature is activated). - structurizr-dsl: Allows archetypes to be used via workspace extension. - structurizr-dsl: Adds archetype support for custom elements. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/399 (Archetype tags sometimes missing). diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java index 8524f753d..ce68fa41f 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -313,7 +313,7 @@ public void putWorkspace(long workspaceId, Workspace workspace) throws Structuri workspace.setLastModifiedUser(getUser()); HttpPut httpPut; - if (StringUtils.isNullOrEmpty(branch)) { + if (StringUtils.isNullOrEmpty(branch) || branch.equalsIgnoreCase(MAIN_BRANCH)) { httpPut = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId); } else { httpPut = new HttpPut(url + WORKSPACE_PATH + "/" + workspaceId + "/branch/" + branch); From 346095e4168f1f3222ab4897fe3a26be04e09674 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:27:52 +0100 Subject: [PATCH 340/418] Closes #408. --- changelog.md | 2 + .../com/structurizr/view/DeploymentView.java | 6 +- .../DeploymentViewAnimationStepParser.java | 63 ++++++++++------ .../dsl/DeploymentViewContentParser.java | 7 +- ...eploymentViewAnimationStepParserTests.java | 41 +++++++++- .../dsl/DeploymentViewContentParserTests.java | 4 +- .../java/com/structurizr/dsl/DslTests.java | 22 ++++++ .../resources/dsl/deployment-animation.dsl | 75 +++++++++++++++++++ 8 files changed, 187 insertions(+), 33 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/deployment-animation.dsl diff --git a/changelog.md b/changelog.md index 173f44310..b03ad7746 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,8 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/399 (Archetype tags sometimes missing). - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/392 (SVG not supported in base 64 encoding not mentioned in documentation). - structurizr-dsl: Adds support for setting the symbols surrounding element/relationship metadata used when rendering diagrams. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/408 (Animation steps cannot be added to deployment views via static structure element references). +- structurizr-dsl: Adds support for specifying view animation steps via element expressions (deployment views only; others to follow). - structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. ## v4.0.0 (28th March 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java b/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java index 04638445e..e38113e9a 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java @@ -403,14 +403,14 @@ private void addAnimationStep(Element... elements) { } } - if (elementsInThisAnimationStep.size() == 0) { - throw new IllegalArgumentException("None of the specified container instances exist in this view."); + if (elementsInThisAnimationStep.isEmpty()) { + throw new IllegalArgumentException("None of the specified elements exist in this view."); } for (RelationshipView relationshipView : this.getRelationships()) { if ( (elementsInThisAnimationStep.contains(relationshipView.getRelationship().getSource()) && elementIdsInPreviousAnimationSteps.contains(relationshipView.getRelationship().getDestination().getId())) || - (elementIdsInPreviousAnimationSteps.contains(relationshipView.getRelationship().getSource().getId()) && elementsInThisAnimationStep.contains(relationshipView.getRelationship().getDestination())) + (elementIdsInPreviousAnimationSteps.contains(relationshipView.getRelationship().getSource().getId()) && elementsInThisAnimationStep.contains(relationshipView.getRelationship().getDestination())) ) { relationshipsInThisAnimationStep.add(relationshipView.getRelationship()); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java index 93598de55..c85e23c89 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java @@ -1,59 +1,78 @@ package com.structurizr.dsl; -import com.structurizr.model.ContainerInstance; -import com.structurizr.model.Element; -import com.structurizr.model.InfrastructureNode; -import com.structurizr.model.StaticStructureElementInstance; +import com.structurizr.model.*; import com.structurizr.view.DeploymentView; import java.util.ArrayList; import java.util.List; +import java.util.Set; final class DeploymentViewAnimationStepParser extends AbstractParser { + private static final String GRAMMAR = " [identifier|element expression...]"; + void parse(DeploymentViewDslContext context, Tokens tokens) { - // animationStep [identifier...] + // animationStep [identifier|element expression...] if (!tokens.includes(1)) { - throw new RuntimeException("Expected: animationStep [identifier...]"); + throw new RuntimeException("Expected: animationStep " + GRAMMAR); } parse(context, context.getView(), tokens, 1); } void parse(DeploymentViewAnimationDslContext context, Tokens tokens) { - // animationStep [identifier...] + // [identifier|element expression...] if (!tokens.includes(0)) { - throw new RuntimeException("Expected: [identifier...]"); + throw new RuntimeException("Expected: " + GRAMMAR); } parse(context, context.getView(), tokens, 0); } - void parse(DslContext context, DeploymentView view, Tokens tokens, int startIndex) { + private void parse(DslContext context, DeploymentView view, Tokens tokens, int startIndex) { List staticStructureElementInstances = new ArrayList<>(); List infrastructureNodes = new ArrayList<>(); for (int i = startIndex; i < tokens.size(); i++) { - String identifier = tokens.get(i); - - Element element = context.getElement(identifier); - if (element == null) { - throw new RuntimeException("The element \"" + identifier + "\" does not exist"); - } - - if (element instanceof StaticStructureElementInstance) { - staticStructureElementInstances.add((StaticStructureElementInstance)element); - } - - if (element instanceof InfrastructureNode) { - infrastructureNodes.add((InfrastructureNode)element); + String token = tokens.get(i); + + if (ExpressionParser.isExpression(token.toLowerCase())) { + Set elements = new DeploymentViewExpressionParser().parseExpression(token, context); + + for (ModelItem element : elements) { + if (element instanceof StaticStructureElementInstance) { + staticStructureElementInstances.add((StaticStructureElementInstance)element); + } + + if (element instanceof InfrastructureNode) { + infrastructureNodes.add((InfrastructureNode)element); + } + } + } else { + Set elements = new DeploymentViewExpressionParser().parseIdentifier(token, context); + + if (elements.isEmpty()) { + throw new RuntimeException("The element \"" + token + "\" does not exist"); + } + + for (ModelItem element : elements) { + if (element instanceof StaticStructureElementInstance) { + staticStructureElementInstances.add((StaticStructureElementInstance)element); + } + + if (element instanceof InfrastructureNode) { + infrastructureNodes.add((InfrastructureNode)element); + } + } } } if (!(staticStructureElementInstances.isEmpty() && infrastructureNodes.isEmpty())) { view.addAnimation(staticStructureElementInstances.toArray(new StaticStructureElementInstance[0]), infrastructureNodes.toArray(new InfrastructureNode[0])); + } else { + throw new RuntimeException("No software system instances, container instances, or infrastructure nodes were found"); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java index 00a955543..755f50e14 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java @@ -14,12 +14,11 @@ final class DeploymentViewContentParser extends ModelViewContentParser { void parseInclude(DeploymentViewDslContext context, Tokens tokens) { if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { - throw new RuntimeException("Expected: include <*|identifier> [*|identifier...]"); + throw new RuntimeException("Expected: include <*|identifier|expression> [*|identifier|expression...]"); } DeploymentView view = context.getView(); - // include [identifier...] for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { String token = tokens.get(i); @@ -36,12 +35,12 @@ void parseInclude(DeploymentViewDslContext context, Tokens tokens) { void parseExclude(DeploymentViewDslContext context, Tokens tokens) { if (!tokens.includes(FIRST_IDENTIFIER_INDEX)) { - throw new RuntimeException("Expected: exclude [identifier...]"); + throw new RuntimeException("Expected: exclude [identifier|expression...]"); } DeploymentView view = context.getView(); - // exclude [identifier...] + for (int i = FIRST_IDENTIFIER_INDEX; i < tokens.size(); i++) { String token = tokens.get(i); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java index 359931273..a11bbf992 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java @@ -1,5 +1,7 @@ package com.structurizr.dsl; +import com.structurizr.model.DeploymentNode; +import com.structurizr.view.DeploymentView; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -15,7 +17,7 @@ void test_parseExplicit_ThrowsAnException_WhenElementsAreMissing() { parser.parse((DeploymentViewDslContext)null, tokens("animationStep")); fail(); } catch (Exception e) { - assertEquals("Expected: animationStep [identifier...]", e.getMessage()); + assertEquals("Expected: animationStep [identifier|element expression...]", e.getMessage()); } } @@ -25,7 +27,42 @@ void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { parser.parse((DeploymentViewAnimationDslContext)null, tokens()); fail(); } catch (Exception e) { - assertEquals("Expected: [identifier...]", e.getMessage()); + assertEquals("Expected: [identifier|element expression...]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementDoesNotExist() { + DeploymentView view = workspace.getViews().createDeploymentView("key", "Description"); + + DeploymentViewAnimationDslContext context = new DeploymentViewAnimationDslContext(view); + IdentifiersRegister map = new IdentifiersRegister(); + context.setIdentifierRegister(map); + + try { + parser.parse(context, tokens("dn")); + fail(); + } catch (Exception e) { + assertEquals("The element/relationship \"dn\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoAnimatableElementsAreFound() { + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node"); + DeploymentView view = workspace.getViews().createDeploymentView("key", "Description"); + view.add(deploymentNode); + + DeploymentViewAnimationDslContext context = new DeploymentViewAnimationDslContext(view); + IdentifiersRegister map = new IdentifiersRegister(); + map.register("dn", deploymentNode); + context.setIdentifierRegister(map); + + try { + parser.parse(context, tokens("dn")); + fail(); + } catch (Exception e) { + assertEquals("No software system instances, container instances, or infrastructure nodes were found", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java index 729ce189e..4bf5d0f30 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewContentParserTests.java @@ -17,7 +17,7 @@ void test_parseInclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { parser.parseInclude(new DeploymentViewDslContext(null), tokens("include")); fail(); } catch (RuntimeException iae) { - assertEquals("Expected: include <*|identifier> [*|identifier...]", iae.getMessage()); + assertEquals("Expected: include <*|identifier|expression> [*|identifier|expression...]", iae.getMessage()); } } @@ -210,7 +210,7 @@ void test_parseExclude_ThrowsAnException_WhenTheNoElementsAreSpecified() { parser.parseExclude(new DeploymentViewDslContext(null), tokens("exclude")); fail(); } catch (RuntimeException iae) { - assertEquals("Expected: exclude [identifier...]", iae.getMessage()); + assertEquals("Expected: exclude [identifier|expression...]", iae.getMessage()); } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index cb957a545..d9d567826 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1576,4 +1576,26 @@ void test_textBlock() throws Exception { - Line 3""", softwareSystem.getDescription()); } + @Test + void test_deploymentAnimation() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/deployment-animation.dsl")); + + Workspace workspace = parser.getWorkspace(); + Container webapp = workspace.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Web Application"); + Container db = workspace.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Database Schema"); + ContainerInstance webappInstance = workspace.getModel().getDeploymentNodeWithName("Deployment Node", "Live").getContainerInstances().stream().filter(ci -> ci.getContainer().equals(webapp)).findFirst().get(); + ContainerInstance dbInstance = workspace.getModel().getDeploymentNodeWithName("Deployment Node", "Live").getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + for (DeploymentView deploymentView : workspace.getViews().getDeploymentViews()) { + assertEquals(2, deploymentView.getAnimations().size()); + + // step 1 + assertTrue(deploymentView.getAnimations().get(0).getElements().contains(webappInstance.getId())); + + // step 2 + assertTrue(deploymentView.getAnimations().get(1).getElements().contains(dbInstance.getId())); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-animation.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-animation.dsl new file mode 100644 index 000000000..a46afbbfd --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/deployment-animation.dsl @@ -0,0 +1,75 @@ +workspace { + + model { + ss = softwaresystem "Software System" { + webapp = container "Web Application" { + tag "UI" + } + db = container "Database Schema" { + tag "DB" + } + } + + webapp -> db + + live = deploymentEnvironment "Live" { + dn = deploymentNode "Deployment Node" { + webappInstance = containerInstance webapp + dbInstance = containerInstance db + } + } + } + + views { + deployment ss "Live" { + include * + + // add animation steps via container instance identifiers + animation { + webappInstance + dbInstance + } + } + + deployment ss "Live" { + include * + + // add animation steps via container identifiers + animation { + webapp + db + } + } + + deployment ss "Live" { + include * + + // add animation steps via element expressions + animation { + webapp + webapp-> + } + } + + deployment ss "Live" { + include * + + // add animation steps via element expressions + animation { + webappInstance + webappInstance-> + } + } + + deployment ss "Live" { + include * + + // add animation steps via element expressions + animation { + element.tag==UI + element.tag==DB + } + } + } + +} \ No newline at end of file From af225d0b1143584d2534fd0840395a49d3bde69c Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:33:14 +0100 Subject: [PATCH 341/418] Fixes failing test. --- .../src/test/java/com/structurizr/view/DeploymentViewTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java index dd4e3c5e1..0fcafacca 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java @@ -347,7 +347,7 @@ void addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpecifiedButNon deploymentView.addAnimation(webApplicationInstance, databaseInstance); fail(); } catch (IllegalArgumentException iae) { - assertEquals("None of the specified container instances exist in this view.", iae.getMessage()); + assertEquals("None of the specified elements exist in this view.", iae.getMessage()); } } From 7d28dd5fd0e2ce1aee629137e0ed5965a8469823 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Sat, 26 Apr 2025 17:31:56 +0100 Subject: [PATCH 342/418] Adds support for specifying view animation steps via element expressions. --- changelog.md | 2 +- .../java/com/structurizr/view/CustomView.java | 2 +- .../java/com/structurizr/view/StaticView.java | 2 +- .../dsl/CustomViewAnimationStepParser.java | 49 +++++++++++------ .../dsl/StaticViewAnimationStepParser.java | 54 +++++++++++++------ .../CustomViewAnimationStepParserTests.java | 21 +++++++- .../java/com/structurizr/dsl/DslTests.java | 44 ++++++++++++++- .../StaticViewAnimationStepParserTests.java | 21 +++++++- .../resources/dsl/custom-view-animation.dsl | 32 +++++++++++ ...tion.dsl => deployment-view-animation.dsl} | 0 .../resources/dsl/static-view-animation.dsl | 32 +++++++++++ 11 files changed, 219 insertions(+), 40 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/custom-view-animation.dsl rename structurizr-dsl/src/test/resources/dsl/{deployment-animation.dsl => deployment-view-animation.dsl} (100%) create mode 100644 structurizr-dsl/src/test/resources/dsl/static-view-animation.dsl diff --git a/changelog.md b/changelog.md index b03ad7746..fca42ceb7 100644 --- a/changelog.md +++ b/changelog.md @@ -9,7 +9,7 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/392 (SVG not supported in base 64 encoding not mentioned in documentation). - structurizr-dsl: Adds support for setting the symbols surrounding element/relationship metadata used when rendering diagrams. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/408 (Animation steps cannot be added to deployment views via static structure element references). -- structurizr-dsl: Adds support for specifying view animation steps via element expressions (deployment views only; others to follow). +- structurizr-dsl: Adds support for specifying view animation steps via element expressions. - structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. ## v4.0.0 (28th March 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/view/CustomView.java b/structurizr-core/src/main/java/com/structurizr/view/CustomView.java index f5b9447ef..81558f604 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/CustomView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/CustomView.java @@ -110,7 +110,7 @@ public void addAnimation(CustomElement... elements) { } } - if (elementsInThisAnimationStep.size() == 0) { + if (elementsInThisAnimationStep.isEmpty()) { throw new IllegalArgumentException("None of the specified elements exist in this view."); } diff --git a/structurizr-core/src/main/java/com/structurizr/view/StaticView.java b/structurizr-core/src/main/java/com/structurizr/view/StaticView.java index 1be47ef96..d5934af05 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/StaticView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/StaticView.java @@ -218,7 +218,7 @@ public void addAnimation(Element... elements) { } } - if (elementsInThisAnimationStep.size() == 0) { + if (elementsInThisAnimationStep.isEmpty()) { throw new IllegalArgumentException("None of the specified elements exist in this view."); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java index c455c18d4..e0e32202b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java @@ -1,51 +1,70 @@ package com.structurizr.dsl; -import com.structurizr.model.CustomElement; -import com.structurizr.model.Element; +import com.structurizr.model.*; import com.structurizr.view.CustomView; import java.util.ArrayList; import java.util.List; +import java.util.Set; final class CustomViewAnimationStepParser extends AbstractParser { + private static final String GRAMMAR = " [identifier|element expression...]"; + void parse(CustomViewDslContext context, Tokens tokens) { - // animationStep [identifier...] + // animationStep [identifier|element expression...] if (!tokens.includes(1)) { - throw new RuntimeException("Expected: animationStep [identifier...]"); + throw new RuntimeException("Expected: animationStep " + GRAMMAR); } parse(context, context.getCustomView(), tokens, 1); } void parse(CustomViewAnimationDslContext context, Tokens tokens) { - // [identifier...] + // [identifier|element expression...] if (!tokens.includes(0)) { - throw new RuntimeException("Expected: [identifier...]"); + throw new RuntimeException("Expected: " + GRAMMAR); } parse(context, context.getView(), tokens, 0); } void parse(DslContext context, CustomView view, Tokens tokens, int startIndex) { - List elements = new ArrayList<>(); + List customElements = new ArrayList<>(); for (int i = startIndex; i < tokens.size(); i++) { - String elementIdentifier = tokens.get(i); + String token = tokens.get(i); - Element element = context.getElement(elementIdentifier); - if (element == null) { - throw new RuntimeException("The element \"" + elementIdentifier + "\" does not exist"); - } + if (ExpressionParser.isExpression(token.toLowerCase())) { + Set elements = new CustomViewExpressionParser().parseExpression(token, context); + + for (ModelItem element : elements) { + if (element instanceof CustomElement) { + customElements.add((CustomElement)element); + } + } + } else { + Set elements = new CustomViewExpressionParser().parseIdentifier(token, context); - if (element instanceof CustomElement) { - elements.add((CustomElement)element); + if (elements.isEmpty()) { + throw new RuntimeException("The element \"" + token + "\" does not exist"); + } + + for (ModelItem element : elements) { + if (element instanceof CustomElement) { + customElements.add((CustomElement)element); + } + } } } - view.addAnimation(elements.toArray(new CustomElement[0])); + if (!customElements.isEmpty()) { + view.addAnimation(customElements.toArray(new CustomElement[0])); + } else { + throw new RuntimeException("No custom elements were found"); + } } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java index 04f76a118..9ce90cfee 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java @@ -1,50 +1,72 @@ package com.structurizr.dsl; import com.structurizr.model.Element; +import com.structurizr.model.ModelItem; +import com.structurizr.model.StaticStructureElement; import com.structurizr.view.StaticView; import java.util.ArrayList; import java.util.List; +import java.util.Set; final class StaticViewAnimationStepParser extends AbstractParser { + private static final String GRAMMAR = " [identifier|element expression...]"; + void parse(StaticViewDslContext context, Tokens tokens) { - // animationStep [identifier...] + // animationStep [identifier|element expression...] if (!tokens.includes(1)) { - throw new RuntimeException("Expected: animationStep [identifier...]"); + throw new RuntimeException("Expected: animationStep " + GRAMMAR); } parse(context, context.getView(), tokens, 1); } void parse(StaticViewAnimationDslContext context, Tokens tokens) { - // [identifier...] + // [identifier|element expression...] if (!tokens.includes(0)) { - throw new RuntimeException("Expected: [identifier...]"); + throw new RuntimeException("Expected: " + GRAMMAR); } parse(context, context.getView(), tokens, 0); } - void parse(DslContext context, StaticView view, Tokens tokens, int startIndex) { - // [identifier...] - - List elements = new ArrayList<>(); + private void parse(DslContext context, StaticView view, Tokens tokens, int startIndex) { + List staticStructureElements = new ArrayList<>(); for (int i = startIndex; i < tokens.size(); i++) { - String elementIdentifier = tokens.get(i); - - Element element = context.getElement(elementIdentifier); - if (element == null) { - throw new RuntimeException("The element \"" + elementIdentifier + "\" does not exist"); + String token = tokens.get(i); + + if (ExpressionParser.isExpression(token.toLowerCase())) { + Set elements = new StaticViewExpressionParser().parseExpression(token, context); + + for (ModelItem element : elements) { + if (element instanceof StaticStructureElement) { + staticStructureElements.add((StaticStructureElement)element); + } + } + } else { + Set elements = new StaticViewExpressionParser().parseIdentifier(token, context); + + if (elements.isEmpty()) { + throw new RuntimeException("The element \"" + token + "\" does not exist"); + } + + for (ModelItem element : elements) { + if (element instanceof StaticStructureElement) { + staticStructureElements.add((StaticStructureElement)element); + } + } } - - elements.add(element); } - view.addAnimation(elements.toArray(new Element[0])); + if (!staticStructureElements.isEmpty()) { + view.addAnimation(staticStructureElements.toArray(new Element[0])); + } else { + throw new RuntimeException("No elements were found"); + } } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java index b820ae888..b1cd19d46 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.view.CustomView; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -15,7 +16,7 @@ void test_parseExplicit_ThrowsAnException_WhenElementsAreMissing() { parser.parse((CustomViewDslContext)null, tokens("animationStep")); fail(); } catch (Exception e) { - assertEquals("Expected: animationStep [identifier...]", e.getMessage()); + assertEquals("Expected: animationStep [identifier|element expression...]", e.getMessage()); } } @@ -25,7 +26,23 @@ void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { parser.parse((CustomViewAnimationDslContext) null, tokens()); fail(); } catch (Exception e) { - assertEquals("Expected: [identifier...]", e.getMessage()); + assertEquals("Expected: [identifier|element expression...]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementDoesNotExist() { + CustomView view = workspace.getViews().createCustomView("key", "Title", "Description"); + + CustomViewAnimationDslContext context = new CustomViewAnimationDslContext(view); + IdentifiersRegister map = new IdentifiersRegister(); + context.setIdentifierRegister(map); + + try { + parser.parse(context, tokens("e")); + fail(); + } catch (Exception e) { + assertEquals("The element/relationship \"e\" does not exist", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index d9d567826..5a082b7d5 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1577,9 +1577,49 @@ void test_textBlock() throws Exception { } @Test - void test_deploymentAnimation() throws Exception { + void test_customViewAnimation() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); - parser.parse(new File("src/test/resources/dsl/deployment-animation.dsl")); + parser.parse(new File("src/test/resources/dsl/custom-view-animation.dsl")); + + Workspace workspace = parser.getWorkspace(); + CustomElement a = workspace.getModel().getCustomElementWithName("A"); + CustomElement b = workspace.getModel().getCustomElementWithName("B"); + + for (CustomView view : workspace.getViews().getCustomViews()) { + assertEquals(2, view.getAnimations().size()); + + // step 1 + assertTrue(view.getAnimations().get(0).getElements().contains(a.getId())); + + // step 2 + assertTrue(view.getAnimations().get(1).getElements().contains(b.getId())); + } + } + + @Test + void test_staticViewAnimation() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/static-view-animation.dsl")); + + Workspace workspace = parser.getWorkspace(); + SoftwareSystem a = workspace.getModel().getSoftwareSystemWithName("A"); + SoftwareSystem b = workspace.getModel().getSoftwareSystemWithName("B"); + + for (SystemLandscapeView view : workspace.getViews().getSystemLandscapeViews()) { + assertEquals(2, view.getAnimations().size()); + + // step 1 + assertTrue(view.getAnimations().get(0).getElements().contains(a.getId())); + + // step 2 + assertTrue(view.getAnimations().get(1).getElements().contains(b.getId())); + } + } + + @Test + void test_deploymentViewAnimation() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/deployment-view-animation.dsl")); Workspace workspace = parser.getWorkspace(); Container webapp = workspace.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Web Application"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java index 557f1e378..61b0a0b4a 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.view.SystemLandscapeView; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -15,7 +16,7 @@ void test_parseExplicit_ThrowsAnException_WhenElementsAreMissing() { parser.parse((StaticViewDslContext)null, tokens("animationStep")); fail(); } catch (Exception e) { - assertEquals("Expected: animationStep [identifier...]", e.getMessage()); + assertEquals("Expected: animationStep [identifier|element expression...]", e.getMessage()); } } @@ -25,7 +26,23 @@ void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { parser.parse((StaticViewAnimationDslContext) null, tokens()); fail(); } catch (Exception e) { - assertEquals("Expected: [identifier...]", e.getMessage()); + assertEquals("Expected: [identifier|element expression...]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementDoesNotExist() { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + + StaticViewAnimationDslContext context = new StaticViewAnimationDslContext(view); + IdentifiersRegister map = new IdentifiersRegister(); + context.setIdentifierRegister(map); + + try { + parser.parse(context, tokens("user")); + fail(); + } catch (Exception e) { + assertEquals("The element/relationship \"user\" does not exist", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/resources/dsl/custom-view-animation.dsl b/structurizr-dsl/src/test/resources/dsl/custom-view-animation.dsl new file mode 100644 index 000000000..fe1679cca --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/custom-view-animation.dsl @@ -0,0 +1,32 @@ +workspace { + + model { + a = element "A" + b = element "B" + + a -> b + } + + views { + custom { + include * + + // add animation steps via element identifiers + animation { + a + b + } + } + + custom { + include * + + // add animation steps via element expressions + animation { + a + a-> + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-animation.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-view-animation.dsl similarity index 100% rename from structurizr-dsl/src/test/resources/dsl/deployment-animation.dsl rename to structurizr-dsl/src/test/resources/dsl/deployment-view-animation.dsl diff --git a/structurizr-dsl/src/test/resources/dsl/static-view-animation.dsl b/structurizr-dsl/src/test/resources/dsl/static-view-animation.dsl new file mode 100644 index 000000000..e68edd0d5 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/static-view-animation.dsl @@ -0,0 +1,32 @@ +workspace { + + model { + a = softwareSystem "A" + b = softwareSystem "B" + + a -> b + } + + views { + systemLandscape { + include * + + // add animation steps via element identifiers + animation { + a + b + } + } + + systemLandscape { + include * + + // add animation steps via element expressions + animation { + a + a-> + } + } + } + +} \ No newline at end of file From 0e14d602f4f7f63ba7fdd86d5ee6d475c672bd86 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 2 May 2025 15:47:48 +0100 Subject: [PATCH 343/418] Fixes #404. --- changelog.md | 1 + .../dsl/ContainerInstanceParser.java | 17 ++--- .../dsl/DeploymentEnvironmentDslContext.java | 8 +- .../com/structurizr/dsl/DeploymentGroup.java | 8 +- .../structurizr/dsl/DeploymentNodeParser.java | 2 +- .../java/com/structurizr/dsl/DslContext.java | 6 +- .../dsl/SoftwareSystemInstanceParser.java | 17 ++--- .../dsl/StaticStructureInstanceParser.java | 34 +++++++++ .../structurizr/dsl/StructurizrDslParser.java | 6 +- .../dsl/ContainerInstanceParserTests.java | 10 ++- .../java/com/structurizr/dsl/DslTests.java | 74 +++++++++++++++++++ .../SoftwareSystemInstanceParserTests.java | 10 ++- ...-groups.dsl => deployment-groups-flat.dsl} | 20 +---- .../dsl/deployment-groups-hierarchical.dsl | 51 +++++++++++++ 14 files changed, 210 insertions(+), 54 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureInstanceParser.java rename structurizr-dsl/src/test/resources/dsl/{deployment-groups.dsl => deployment-groups-flat.dsl} (70%) create mode 100644 structurizr-dsl/src/test/resources/dsl/deployment-groups-hierarchical.dsl diff --git a/changelog.md b/changelog.md index fca42ceb7..971f41c74 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ - structurizr-dsl: Adds support for setting the symbols surrounding element/relationship metadata used when rendering diagrams. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/408 (Animation steps cannot be added to deployment views via static structure element references). - structurizr-dsl: Adds support for specifying view animation steps via element expressions. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/404 (deploymentGroup does not obey !identifiers hierarchical). - structurizr-export: Adds support for rank and node separation to the StructurizrPlantUMLExporter. ## v4.0.0 (28th March 2025) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java index c58287956..04e52c35e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java @@ -1,11 +1,14 @@ package com.structurizr.dsl; -import com.structurizr.model.*; +import com.structurizr.model.Container; +import com.structurizr.model.ContainerInstance; +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.Element; import java.util.HashSet; import java.util.Set; -final class ContainerInstanceParser extends AbstractParser { +final class ContainerInstanceParser extends StaticStructureInstanceParser { private static final String GRAMMAR = "containerInstance [deploymentGroups] [tags]"; @@ -35,15 +38,7 @@ ContainerInstance parse(DeploymentNodeDslContext context, Tokens tokens) { Set deploymentGroups = new HashSet<>(); if (tokens.includes(DEPLOYMENT_GROUPS_TOKEN)) { - String token = tokens.get(DEPLOYMENT_GROUPS_TOKEN); - - String[] deploymentGroupReferences = token.split(","); - for (String deploymentGroupReference : deploymentGroupReferences) { - Element e = context.getElement(deploymentGroupReference); - if (e instanceof DeploymentGroup) { - deploymentGroups.add(e.getName()); - } - } + deploymentGroups = getDeploymentGroups(context, tokens.get(DEPLOYMENT_GROUPS_TOKEN)); } ContainerInstance containerInstance = deploymentNode.add((Container)element, deploymentGroups.toArray(new String[]{})); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java index ceb1c30b2..4cc53b0fc 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java @@ -2,20 +2,20 @@ final class DeploymentEnvironmentDslContext extends DslContext implements GroupableDslContext { - private final String environment; + private final DeploymentEnvironment environment; private final ElementGroup group; DeploymentEnvironmentDslContext(String environment) { - this.environment = environment; + this.environment = new DeploymentEnvironment(environment); this.group = null; } DeploymentEnvironmentDslContext(String environment, ElementGroup group) { - this.environment = environment; + this.environment = new DeploymentEnvironment(environment); this.group = group; } - String getEnvironment() { + DeploymentEnvironment getEnvironment() { return environment; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java index 0027da22f..a89e9fb49 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java @@ -6,9 +6,11 @@ class DeploymentGroup extends Element { - private String name; + private final Element parent; + private final String name; - DeploymentGroup(String name) { + DeploymentGroup(DeploymentEnvironment deploymentEnvironment, String name) { + this.parent = deploymentEnvironment; this.name = name; } @@ -24,7 +26,7 @@ public String getCanonicalName() { @Override public Element getParent() { - return null; + return parent; } @Override diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java index 93866302b..a6aa7d859 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java @@ -51,7 +51,7 @@ DeploymentNode parse(DeploymentEnvironmentDslContext deploymentEnvironmentDslCon if (deploymentEnvironmentDslContext != null) { // add a root deployment node - deploymentNode = deploymentEnvironmentDslContext.getWorkspace().getModel().addDeploymentNode(deploymentEnvironmentDslContext.getEnvironment(), name, description, technology); + deploymentNode = deploymentEnvironmentDslContext.getWorkspace().getModel().addDeploymentNode(deploymentEnvironmentDslContext.getEnvironment().getName(), name, description, technology); if (deploymentEnvironmentDslContext.hasGroup()) { deploymentNode.setGroup(deploymentEnvironmentDslContext.getGroup().getName()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java index 2711c9138..41417ba8e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java @@ -40,6 +40,10 @@ void setIdentifierRegister(IdentifiersRegister identifersRegister) { this.identifiersRegister = identifersRegister; } + String findIdentifier(Element element) { + return identifiersRegister.findIdentifier(element); + } + Element getElement(String identifier) { return getElement(identifier, null); } @@ -71,7 +75,7 @@ Element getElement(String identifier, Class type) { } } else if (this instanceof DeploymentEnvironmentDslContext) { DeploymentEnvironmentDslContext deploymentEnvironmentDslContext = (DeploymentEnvironmentDslContext)this; - DeploymentEnvironment deploymentEnvironment = new DeploymentEnvironment(deploymentEnvironmentDslContext.getEnvironment()); + DeploymentEnvironment deploymentEnvironment = deploymentEnvironmentDslContext.getEnvironment(); String parentIdentifier = identifiersRegister.findIdentifier(deploymentEnvironment); element = identifiersRegister.getElement(parentIdentifier + "." + identifier); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java index dee362c64..88f5da70d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java @@ -1,11 +1,14 @@ package com.structurizr.dsl; -import com.structurizr.model.*; +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.SoftwareSystemInstance; import java.util.HashSet; import java.util.Set; -final class SoftwareSystemInstanceParser extends AbstractParser { +final class SoftwareSystemInstanceParser extends StaticStructureInstanceParser { private static final String GRAMMAR = "softwareSystemInstance [deploymentGroups] [tags]"; @@ -36,15 +39,7 @@ SoftwareSystemInstance parse(DeploymentNodeDslContext context, Tokens tokens) { Set deploymentGroups = new HashSet<>(); if (tokens.includes(DEPLOYMENT_GROUPS_TOKEN)) { - String token = tokens.get(DEPLOYMENT_GROUPS_TOKEN); - - String[] deploymentGroupReferences = token.split(","); - for (String deploymentGroupReference : deploymentGroupReferences) { - Element e = context.getElement(deploymentGroupReference); - if (e instanceof DeploymentGroup) { - deploymentGroups.add(e.getName()); - } - } + deploymentGroups = getDeploymentGroups(context, tokens.get(DEPLOYMENT_GROUPS_TOKEN)); } SoftwareSystemInstance softwareSystemInstance = deploymentNode.add((SoftwareSystem)element, deploymentGroups.toArray(new String[]{})); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureInstanceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureInstanceParser.java new file mode 100644 index 000000000..c1b191be9 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticStructureInstanceParser.java @@ -0,0 +1,34 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; + +import java.util.HashSet; +import java.util.Set; + +abstract class StaticStructureInstanceParser extends AbstractParser { + + protected Set getDeploymentGroups(DeploymentNodeDslContext context, String token) { + Set deploymentGroups = new HashSet<>(); + String[] deploymentGroupReferences = token.split(","); + for (String deploymentGroupReference : deploymentGroupReferences) { + Element e = context.getElement(deploymentGroupReference, DeploymentGroup.class); + + if (e == null) { + // try to find deployment group via hierarchical identifier + String deploymentEnvironmentName = context.getDeploymentNode().getEnvironment(); + String deploymentEnvironmentIdentifier = context.findIdentifier(new DeploymentEnvironment(deploymentEnvironmentName)); + + e = context.getElement(deploymentEnvironmentIdentifier + "." + deploymentGroupReference, DeploymentGroup.class); + } + + if (e instanceof DeploymentGroup) { + deploymentGroups.add(e.getName()); + } else { + // backwards compatibility - deployment environment name rather than identifier + } + } + + return deploymentGroups; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 563d40dcd..4664c0266 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -549,8 +549,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { ElementGroup group = new GroupParser().parseContext(getContext(DeploymentEnvironmentDslContext.class), tokens); - String environment = getContext(DeploymentEnvironmentDslContext.class).getEnvironment(); - startContext(new DeploymentEnvironmentDslContext(environment, group)); + DeploymentEnvironment environment = getContext(DeploymentEnvironmentDslContext.class).getEnvironment(); + startContext(new DeploymentEnvironmentDslContext(environment.getName(), group)); registerIdentifier(identifier, group); } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentNodeDslContext.class)) { ElementGroup group = new GroupParser().parseContext(getContext(DeploymentNodeDslContext.class), tokens); @@ -877,7 +877,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (DEPLOYMENT_GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentEnvironmentDslContext.class)) { String group = new DeploymentGroupParser().parse(tokens.withoutContextStartToken()); - registerIdentifier(identifier, new DeploymentGroup(group)); + registerIdentifier(identifier, new DeploymentGroup(getContext(DeploymentEnvironmentDslContext.class).getEnvironment(), group)); } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { Archetype archetype = getArchetype(DEPLOYMENT_NODE_TOKEN, firstToken); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java index 073602d61..ba62530c0 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java @@ -103,10 +103,13 @@ void test_parse_CreatesAContainerInstanceInTheSpecifiedDeploymentGroup() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); elements.register("container", container); - elements.register("group", new DeploymentGroup("Group")); + elements.register("live", new DeploymentEnvironment("Live")); + elements.register("group", new DeploymentGroup(new DeploymentEnvironment("Live"), "Group")); context.setIdentifierRegister(elements); parser.parse(context, tokens("containerInstance", "container", "group")); @@ -126,10 +129,13 @@ void test_parse_CreatesAContainerInstanceInTheSpecifiedDeploymentGroupWithTags() SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); Container container = softwareSystem.addContainer("Container", "Description", "Technology"); DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); elements.register("container", container); - elements.register("group", new DeploymentGroup("Group")); + elements.register("live", new DeploymentEnvironment("Live")); + elements.register("group", new DeploymentGroup(new DeploymentEnvironment("Live"), "Group")); context.setIdentifierRegister(elements); parser.parse(context, tokens("containerInstance", "container", "group", "Tag 1, Tag 2")); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 5a082b7d5..8761713fd 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1638,4 +1638,78 @@ void test_deploymentViewAnimation() throws Exception { } } + @Test + void test_deploymentGroups_Flat() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/deployment-groups-flat.dsl")); + + Workspace workspace = parser.getWorkspace(); + + Container api = workspace.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("API"); + Container db = workspace.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("DB"); + + DeploymentNode server1 = workspace.getModel().getDeploymentNodeWithName("Server 1", "WithoutDeploymentGroups"); + ContainerInstance apiInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + ContainerInstance dbInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + DeploymentNode server2 = workspace.getModel().getDeploymentNodeWithName("Server 2", "WithoutDeploymentGroups"); + ContainerInstance apiInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + ContainerInstance dbInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + assertTrue(apiInstance1.hasEfferentRelationshipWith(dbInstance1)); + assertTrue(apiInstance1.hasEfferentRelationshipWith(dbInstance2)); + assertTrue(apiInstance2.hasEfferentRelationshipWith(dbInstance2)); + assertTrue(apiInstance2.hasEfferentRelationshipWith(dbInstance1)); + + server1 = workspace.getModel().getDeploymentNodeWithName("Server 1", "WithDeploymentGroups"); + apiInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + dbInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + server2 = workspace.getModel().getDeploymentNodeWithName("Server 2", "WithDeploymentGroups"); + apiInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + dbInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + assertTrue(apiInstance1.hasEfferentRelationshipWith(dbInstance1)); + assertFalse(apiInstance1.hasEfferentRelationshipWith(dbInstance2)); + assertTrue(apiInstance2.hasEfferentRelationshipWith(dbInstance2)); + assertFalse(apiInstance2.hasEfferentRelationshipWith(dbInstance1)); + } + + @Test + void test_deploymentGroups_Hierarchical() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/deployment-groups-hierarchical.dsl")); + + Workspace workspace = parser.getWorkspace(); + + Container api = workspace.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("API"); + Container db = workspace.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("DB"); + + DeploymentNode server1 = workspace.getModel().getDeploymentNodeWithName("Server 1", "WithoutDeploymentGroups"); + ContainerInstance apiInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + ContainerInstance dbInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + DeploymentNode server2 = workspace.getModel().getDeploymentNodeWithName("Server 2", "WithoutDeploymentGroups"); + ContainerInstance apiInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + ContainerInstance dbInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + assertTrue(apiInstance1.hasEfferentRelationshipWith(dbInstance1)); + assertTrue(apiInstance1.hasEfferentRelationshipWith(dbInstance2)); + assertTrue(apiInstance2.hasEfferentRelationshipWith(dbInstance2)); + assertTrue(apiInstance2.hasEfferentRelationshipWith(dbInstance1)); + + server1 = workspace.getModel().getDeploymentNodeWithName("Server 1", "WithDeploymentGroups"); + apiInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + dbInstance1 = server1.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + server2 = workspace.getModel().getDeploymentNodeWithName("Server 2", "WithDeploymentGroups"); + apiInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(api)).findFirst().get(); + dbInstance2 = server2.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(db)).findFirst().get(); + + assertTrue(apiInstance1.hasEfferentRelationshipWith(dbInstance1)); + assertFalse(apiInstance1.hasEfferentRelationshipWith(dbInstance2)); + assertTrue(apiInstance2.hasEfferentRelationshipWith(dbInstance2)); + assertFalse(apiInstance2.hasEfferentRelationshipWith(dbInstance1)); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java index 1d0b7e278..c25ad5e43 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java @@ -100,10 +100,13 @@ void test_parse_CreatesASoftwareSystemInstanceInTheDefaultDeploymentGroupWithTag void test_parse_CreatesASoftwareSystemInstanceInTheSpecifiedDeploymentGroup() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); elements.register("softwaresystem", softwareSystem); - elements.register("group", new DeploymentGroup("Group")); + elements.register("live", new DeploymentEnvironment("Live")); + elements.register("group", new DeploymentGroup(new DeploymentEnvironment("Live"), "Group")); context.setIdentifierRegister(elements); parser.parse(context, tokens("softwareSystemInstance", "softwareSystem", "group")); @@ -122,10 +125,13 @@ void test_parse_CreatesASoftwareSystemInstanceInTheSpecifiedDeploymentGroup() { void test_parse_CreatesASoftwareSystemInstanceInTheSpecifiedDeploymentGroupWithTags() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); elements.register("softwaresystem", softwareSystem); - elements.register("group", new DeploymentGroup("Group")); + elements.register("live", new DeploymentEnvironment("Live")); + elements.register("group", new DeploymentGroup(new DeploymentEnvironment("Live"), "Group")); context.setIdentifierRegister(elements); parser.parse(context, tokens("softwareSystemInstance", "softwareSystem", "group", "Tag 1, Tag 2")); diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-groups.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-groups-flat.dsl similarity index 70% rename from structurizr-dsl/src/test/resources/dsl/deployment-groups.dsl rename to structurizr-dsl/src/test/resources/dsl/deployment-groups-flat.dsl index e58f506ee..287b4abe6 100644 --- a/structurizr-dsl/src/test/resources/dsl/deployment-groups.dsl +++ b/structurizr-dsl/src/test/resources/dsl/deployment-groups-flat.dsl @@ -2,13 +2,13 @@ workspace { model { softwareSystem = softwareSystem "Software System" { - database = container "Database" - api = container "Service API" { + database = container "DB" + api = container "API" { -> database "Uses" } } - deploymentEnvironment "Example 1" { + deploymentEnvironment "WithoutDeploymentGroups" { deploymentNode "Server 1" { containerInstance api containerInstance database @@ -19,7 +19,7 @@ workspace { } } - deploymentEnvironment "Example 2" { + deploymentEnvironment "WithDeploymentGroups" { serviceInstance1 = deploymentGroup "Service Instance 1" serviceInstance2 = deploymentGroup "Service Instance 2" deploymentNode "Server 1" { @@ -33,16 +33,4 @@ workspace { } } - views { - deployment * "Example 1" { - include * - autolayout - } - - deployment * "Example 2" { - include * - autolayout - } - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-groups-hierarchical.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-groups-hierarchical.dsl new file mode 100644 index 000000000..202ea2a71 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/deployment-groups-hierarchical.dsl @@ -0,0 +1,51 @@ +workspace { + + !identifiers hierarchical + + model { + softwareSystem = softwareSystem "Software System" { + database = container "DB" + api = container "API" { + -> database "Uses" + } + } + + deploymentEnvironment "WithoutDeploymentGroups" { + deploymentNode "Server 1" { + containerInstance softwareSystem.api + containerInstance softwareSystem.database + } + deploymentNode "Server 2" { + containerInstance softwareSystem.api + containerInstance softwareSystem.database + } + } + + deploymentEnvironment "WithDeploymentGroups" { + serviceInstance1 = deploymentGroup "Service Instance 1" + serviceInstance2 = deploymentGroup "Service Instance 2" + deploymentNode "Server 1" { + containerInstance softwareSystem.api serviceInstance1 + containerInstance softwareSystem.database serviceInstance1 + } + deploymentNode "Server 2" { + containerInstance softwareSystem.api serviceInstance2 + containerInstance softwareSystem.database serviceInstance2 + } + } + + deploymentEnvironment "WithDeploymentGroupsAgain" { + serviceInstance1 = deploymentGroup "Service Instance 1" + serviceInstance2 = deploymentGroup "Service Instance 2" + deploymentNode "Server 1" { + containerInstance softwareSystem.api serviceInstance1 + containerInstance softwareSystem.database serviceInstance1 + } + deploymentNode "Server 2" { + containerInstance softwareSystem.api serviceInstance2 + containerInstance softwareSystem.database serviceInstance2 + } + } + } + +} \ No newline at end of file From fab1600c6cd186df70c6da39dd260983fc964e0b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 2 May 2025 15:49:17 +0100 Subject: [PATCH 344/418] . --- changelog.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 971f41c74..f523c14ee 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## v4.0.1 (unreleased) +## v4.1.0 (unreleased) - structurizr-client: Fixes https://github.com/structurizr/java/issues/413 (Cannot push to main branch, when branch feature is activated). - structurizr-dsl: Allows archetypes to be used via workspace extension. diff --git a/gradle.properties b/gradle.properties index 1f68eb29c..42ec5628d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=4.0.1 \ No newline at end of file +version=4.1.0 \ No newline at end of file From e328063b777d052c8682565dce25c395b594579b Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Wed, 28 May 2025 10:45:10 +0100 Subject: [PATCH 345/418] Updated to reflect release. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index f523c14ee..1d2028922 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## v4.1.0 (unreleased) +## v4.1.0 (28th May 2025) - structurizr-client: Fixes https://github.com/structurizr/java/issues/413 (Cannot push to main branch, when branch feature is activated). - structurizr-dsl: Allows archetypes to be used via workspace extension. From 1569be340f1d65be51f1b31cf2f4bb9eeb2b8adc Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 25 Jun 2025 09:09:57 +0100 Subject: [PATCH 346/418] Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). --- changelog.md | 4 ++ gradle.properties | 2 +- .../com/structurizr/view/ElementStyle.java | 21 +++++++++++ .../com/structurizr/view/IconPosition.java | 9 +++++ .../dsl/ElementStyleDslContext.java | 1 + .../structurizr/dsl/ElementStyleParser.java | 30 +++++++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 3 ++ .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../dsl/ElementStyleParserTests.java | 37 +++++++++++++++++++ 9 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 structurizr-core/src/main/java/com/structurizr/view/IconPosition.java diff --git a/changelog.md b/changelog.md index 1d2028922..59110d172 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## v4.2.0 (unreleased) + +- structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). + ## v4.1.0 (28th May 2025) - structurizr-client: Fixes https://github.com/structurizr/java/issues/413 (Cannot push to main branch, when branch feature is activated). diff --git a/gradle.properties b/gradle.properties index 42ec5628d..c99ca35d7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=4.1.0 \ No newline at end of file +version=4.2.0 \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java index 70299a5fe..16a83c76c 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java @@ -41,6 +41,9 @@ public final class ElementStyle extends AbstractStyle { @JsonInclude(value = JsonInclude.Include.NON_NULL) private String icon; + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private IconPosition iconPosition; + @JsonInclude(value = JsonInclude.Include.NON_NULL) private Border border; @@ -290,6 +293,24 @@ public ElementStyle icon(String icon) { return this; } + /** + * Gets the icon position to use when rendering the element. + * + * @return an IconPosition, or null if not specified + */ + public IconPosition getIconPosition() { + return iconPosition; + } + + public void setIconPosition(IconPosition iconPosition) { + this.iconPosition = iconPosition; + } + + public ElementStyle iconPosition(IconPosition iconPosition) { + setIconPosition(iconPosition); + return this; + } + /** * Gets the border used when rendering the element. * diff --git a/structurizr-core/src/main/java/com/structurizr/view/IconPosition.java b/structurizr-core/src/main/java/com/structurizr/view/IconPosition.java new file mode 100644 index 000000000..38ee1ff93 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/IconPosition.java @@ -0,0 +1,9 @@ +package com.structurizr.view; + +public enum IconPosition { + + Top, + Bottom, + Left + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java index 1ab73cbe3..f99834d07 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java @@ -27,6 +27,7 @@ protected String[] getPermittedTokens() { return new String[] { StructurizrDslTokens.ELEMENT_STYLE_SHAPE_TOKEN, StructurizrDslTokens.ELEMENT_STYLE_ICON_TOKEN, + StructurizrDslTokens.ELEMENT_STYLE_ICON_POSITION_TOKEN, StructurizrDslTokens.ELEMENT_STYLE_WIDTH_TOKEN, StructurizrDslTokens.ELEMENT_STYLE_HEIGHT_TOKEN, StructurizrDslTokens.ELEMENT_STYLE_BACKGROUND_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java index 039abab6c..c27f8eb9f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java @@ -5,6 +5,7 @@ import com.structurizr.util.StringUtils; import com.structurizr.view.Border; import com.structurizr.view.ElementStyle; +import com.structurizr.view.IconPosition; import com.structurizr.view.Shape; import java.io.File; @@ -318,4 +319,33 @@ void parseIcon(ElementStyleDslContext context, Tokens tokens, boolean restricted } } + void parseIconPosition(ElementStyleDslContext context, Tokens tokens) { + Map iconPositions = new HashMap<>(); + String iconPositionsAsString = ""; + for (IconPosition iconPosition : IconPosition.values()) { + iconPositions.put(iconPosition.toString().toLowerCase(), iconPosition); + iconPositionsAsString += iconPosition; + iconPositionsAsString += "|"; + } + iconPositionsAsString = iconPositionsAsString.substring(0, iconPositionsAsString.length()-1); + + ElementStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: iconPosition <" + iconPositionsAsString + ">"); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String iconPosition = tokens.get(1).toLowerCase(); + + if (iconPositions.containsKey(iconPosition)) { + style.setIconPosition(iconPositions.get(iconPosition)); + } else { + throw new RuntimeException("The icon position \"" + iconPosition + "\" is not valid"); + } + } else { + throw new RuntimeException("Expected: iconPosition <" + iconPositionsAsString + ">"); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 4664c0266..809a9e4d1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -834,6 +834,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (ELEMENT_STYLE_ICON_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { new ElementStyleParser().parseIcon(getContext(ElementStyleDslContext.class), tokens, restricted); + } else if (ELEMENT_STYLE_ICON_POSITION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { + new ElementStyleParser().parseIconPosition(getContext(ElementStyleDslContext.class), tokens); + } else if (RELATIONSHIP_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) { RelationshipStyle relationshipStyle = new RelationshipStyleParser().parseRelationshipStyle(getContext(), tokens.withoutContextStartToken()); startContext(new RelationshipStyleDslContext(relationshipStyle)); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 9b295eff0..67bc67f56 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -71,6 +71,7 @@ class StructurizrDslTokens { static final String ELEMENT_STYLE_COLOUR_TOKEN = "colour"; static final String ELEMENT_STYLE_COLOR_TOKEN = "color"; static final String ELEMENT_STYLE_ICON_TOKEN = "icon"; + static final String ELEMENT_STYLE_ICON_POSITION_TOKEN = "iconPosition"; static final String ELEMENT_STYLE_OPACITY_TOKEN = "opacity"; static final String ELEMENT_STYLE_BORDER_TOKEN = "border"; static final String ELEMENT_STYLE_FONT_SIZE_TOKEN = "fontSize"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java index 50086e8dc..a3519604e 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -2,6 +2,7 @@ import com.structurizr.view.Border; import com.structurizr.view.ElementStyle; +import com.structurizr.view.IconPosition; import com.structurizr.view.Shape; import org.junit.jupiter.api.Test; @@ -543,4 +544,40 @@ void test_parseIcon_SetsTheIconFromAFile() { assertTrue(elementStyle.getIcon().startsWith("data:image/png;base64,")); } + @Test + void test_parseIconPosition_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseIconPosition(elementStyleDslContext(), tokens("iconPosition", "top", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: iconPosition ", e.getMessage()); + } + } + + @Test + void test_parseIconPosition_ThrowsAnException_WhenTheShapeIsMissing() { + try { + parser.parseIconPosition(elementStyleDslContext(), tokens("iconPosition")); + fail(); + } catch (Exception e) { + assertEquals("Expected: iconPosition ", e.getMessage()); + } + } + + @Test + void test_parseIconPosition_ThrowsAnException_WhenTheShapeIsNotValid() { + try { + parser.parseIconPosition(elementStyleDslContext(), tokens("iconPosition", "right")); + fail(); + } catch (Exception e) { + assertEquals("The icon position \"right\" is not valid", e.getMessage()); + } + } + + @Test + void test_parseIconPosition_SetsTheIconPosition() { + parser.parseIconPosition(elementStyleDslContext(), tokens("iconPosition", "top")); + assertEquals(IconPosition.Top, elementStyle.getIconPosition()); + } + } \ No newline at end of file From 4df83011b480ed50933adb025201eda8023d0cc4 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 26 Jun 2025 15:20:26 +0100 Subject: [PATCH 347/418] Adds support for defining element and relationship styles for light and dark mode (re: #368). --- changelog.md | 1 + .../com/structurizr/view/AbstractStyle.java | 20 +++++++ .../com/structurizr/view/ColorScheme.java | 11 ++++ .../com/structurizr/view/ElementStyle.java | 5 ++ .../structurizr/view/RelationshipStyle.java | 5 ++ .../java/com/structurizr/view/Styles.java | 54 ++++++++++++++++--- .../com/structurizr/view/StylesTests.java | 44 ++++++++++++++- .../structurizr/dsl/ElementStyleParser.java | 11 ++-- .../dsl/RelationshipStyleParser.java | 10 ++-- .../structurizr/dsl/StructurizrDslParser.java | 10 +++- .../structurizr/dsl/StructurizrDslTokens.java | 2 + .../com/structurizr/dsl/StylesDslContext.java | 16 ++++++ .../java/com/structurizr/dsl/DslTests.java | 27 +++++++++- .../dsl/ElementStyleParserTests.java | 17 ++++-- .../dsl/RelationshipStyleParserTests.java | 17 ++++-- .../src/test/resources/dsl/color-schemes.dsl | 31 +++++++++++ .../src/test/resources/dsl/test.dsl | 21 ++++++++ 17 files changed, 267 insertions(+), 35 deletions(-) create mode 100644 structurizr-core/src/main/java/com/structurizr/view/ColorScheme.java create mode 100644 structurizr-dsl/src/test/resources/dsl/color-schemes.dsl diff --git a/changelog.md b/changelog.md index 59110d172..f0355de16 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## v4.2.0 (unreleased) - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). +- structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. ## v4.1.0 (28th May 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java index 2ad6bb291..39ad047be 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java @@ -8,8 +8,28 @@ public abstract class AbstractStyle implements PropertyHolder { + private ColorScheme colorScheme = null; + private Map properties = new HashMap<>(); + /** + * Gets the color scheme of this style. + * + * @return a ColorScheme, or null if not specified (i.e. applies to light and dark). + */ + public ColorScheme getColorScheme() { + return colorScheme; + } + + /** + * Sets the color scheme of this style. + * + * @param colorScheme a ColorScheme, or null if not specified (i.e. applies to light and dark). + */ + void setColorScheme(ColorScheme colorScheme) { + this.colorScheme = colorScheme; + } + /** * Gets the collection of name-value property pairs associated with this workspace, as a Map. * diff --git a/structurizr-core/src/main/java/com/structurizr/view/ColorScheme.java b/structurizr-core/src/main/java/com/structurizr/view/ColorScheme.java new file mode 100644 index 000000000..4548594bc --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ColorScheme.java @@ -0,0 +1,11 @@ +package com.structurizr.view; + +/** + * Represents light or dark mode color schemes. + */ +public enum ColorScheme { + + Light, + Dark + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java index 16a83c76c..90bd169c3 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java @@ -63,6 +63,11 @@ public final class ElementStyle extends AbstractStyle { this.tag = tag; } + ElementStyle(String tag, ColorScheme colorScheme) { + this.tag = tag; + setColorScheme(colorScheme); + } + public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize) { this(tag, width, height, background, color, fontSize, null); } diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java index d32e6ee30..3fbae753e 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java @@ -54,6 +54,11 @@ public final class RelationshipStyle extends AbstractStyle { this.tag = tag; } + RelationshipStyle(String tag, ColorScheme colorScheme) { + this.tag = tag; + setColorScheme(colorScheme); + } + public String getTag() { return tag; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/Styles.java b/structurizr-core/src/main/java/com/structurizr/view/Styles.java index 79780d94d..805ed188b 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Styles.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Styles.java @@ -29,8 +29,12 @@ public void add(ElementStyle elementStyle) { throw new IllegalArgumentException("A tag must be specified."); } - if (elements.stream().anyMatch(es -> es.getTag().equals(elementStyle.getTag()))) { - throw new IllegalArgumentException("An element style for the tag \"" + elementStyle.getTag() + "\" already exists."); + if (elements.stream().anyMatch(es -> es.getTag().equals(elementStyle.getTag()) && es.getColorScheme() == elementStyle.getColorScheme())) { + if (elementStyle.getColorScheme() == null) { + throw new IllegalArgumentException("An element style for the tag \"" + elementStyle.getTag() + "\" already exists."); + } else { + throw new IllegalArgumentException("An element style for the tag \"" + elementStyle.getTag() + "\" and color scheme " + elementStyle.getColorScheme() + " already exists."); + } } this.elements.add(elementStyle); @@ -38,7 +42,11 @@ public void add(ElementStyle elementStyle) { } public ElementStyle addElementStyle(String tag) { - ElementStyle elementStyle = new ElementStyle(tag); + return addElementStyle(tag, null); + } + + public ElementStyle addElementStyle(String tag, ColorScheme colorScheme) { + ElementStyle elementStyle = new ElementStyle(tag, colorScheme); add(elementStyle); return elementStyle; @@ -68,8 +76,12 @@ public void add(RelationshipStyle relationshipStyle) { throw new IllegalArgumentException("A tag must be specified."); } - if (relationships.stream().anyMatch(es -> es.getTag().equals(relationshipStyle.getTag()))) { - throw new IllegalArgumentException("A relationship style for the tag \"" + relationshipStyle.getTag() + "\" already exists."); + if (relationships.stream().anyMatch(rs -> rs.getTag().equals(relationshipStyle.getTag()) && rs.getColorScheme() == relationshipStyle.getColorScheme())) { + if (relationshipStyle.getColorScheme() == null) { + throw new IllegalArgumentException("A relationship style for the tag \"" + relationshipStyle.getTag() + "\" already exists."); + } else { + throw new IllegalArgumentException("A relationship style for the tag \"" + relationshipStyle.getTag() + "\" and color scheme " + relationshipStyle.getColorScheme() + " already exists."); + } } this.relationships.add(relationshipStyle); @@ -77,7 +89,11 @@ public void add(RelationshipStyle relationshipStyle) { } public RelationshipStyle addRelationshipStyle(String tag) { - RelationshipStyle relationshipStyle = new RelationshipStyle(tag); + return addRelationshipStyle(tag, null); + } + + public RelationshipStyle addRelationshipStyle(String tag, ColorScheme colorScheme) { + RelationshipStyle relationshipStyle = new RelationshipStyle(tag, colorScheme); add(relationshipStyle); return relationshipStyle; @@ -90,11 +106,22 @@ public RelationshipStyle addRelationshipStyle(String tag) { * @return an ElementStyle instance, or null if no element style has been defined in this workspace */ public ElementStyle getElementStyle(String tag) { + return getElementStyle(tag, null); + } + + /** + * Gets the element style that has been defined (in this workspace) for the given tag and color scheme. + * + * @param tag the tag (a String) + * @param colorScheme the ColorScheme (can be null) + * @return an ElementStyle instance, or null if no element style has been defined in this workspace + */ + public ElementStyle getElementStyle(String tag, ColorScheme colorScheme) { if (StringUtils.isNullOrEmpty(tag)) { throw new IllegalArgumentException("A tag must be specified."); } - return elements.stream().filter(es -> es.getTag().equals(tag)).findFirst().orElse(null); + return elements.stream().filter(es -> es.getTag().equals(tag) && es.getColorScheme() == colorScheme).findFirst().orElse(null); } /** @@ -141,11 +168,22 @@ public ElementStyle findElementStyle(String tag) { * @return an RelationshipStyle instance, or null if no relationship style has been defined in this workspace */ public RelationshipStyle getRelationshipStyle(String tag) { + return getRelationshipStyle(tag, null); + } + + /** + * Gets the relationship style that has been defined (in this workspace) for the given tag and color scheme. + * + * @param tag the tag (a String) + * @param colorScheme the ColorScheme (can be null) + * @return an RelationshipStyle instance, or null if no relationship style has been defined in this workspace + */ + public RelationshipStyle getRelationshipStyle(String tag, ColorScheme colorScheme) { if (StringUtils.isNullOrEmpty(tag)) { throw new IllegalArgumentException("A tag must be specified."); } - return relationships.stream().filter(rs -> rs.getTag().equals(tag)).findFirst().orElse(null); + return relationships.stream().filter(rs -> rs.getTag().equals(tag) && rs.getColorScheme() == colorScheme).findFirst().orElse(null); } /** diff --git a/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java index c9b6aa1fe..caf878673 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java @@ -10,7 +10,7 @@ public class StylesTests extends AbstractWorkspaceTestBase { - private Styles styles = new Styles(); + private final Styles styles = new Styles(); @Test void findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { @@ -263,6 +263,27 @@ void addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlread } } + @Test + void addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagAndColorSchemeExistsAlready() { + try { + styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Dark); + styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Dark); + + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element style for the tag \"Software System\" and color scheme Dark already exists.", iae.getMessage()); + } + } + + @Test + void addElementStyleByTag_WithDifferentColorSchemes() { + styles.addElementStyle(Tags.SOFTWARE_SYSTEM); + styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Dark); + styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Light); + + assertEquals(3, styles.getElements().size()); + } + @Test void addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { @@ -311,6 +332,27 @@ void addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsA } } + @Test + void addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagAndColorSchemeExistsAlready() { + try { + styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Light); + styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Light); + + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship style for the tag \"Relationship\" and color scheme Light already exists.", iae.getMessage()); + } + } + + @Test + void addRelationshipStyleByTag_WithDifferentColorSchemes() { + styles.addRelationshipStyle(Tags.RELATIONSHIP); + styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Dark); + styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Light); + + assertEquals(3, styles.getRelationships().size()); + } + @Test void addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { try { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java index c27f8eb9f..dd76e5804 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java @@ -3,10 +3,7 @@ import com.structurizr.Workspace; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; -import com.structurizr.view.Border; -import com.structurizr.view.ElementStyle; -import com.structurizr.view.IconPosition; -import com.structurizr.view.Shape; +import com.structurizr.view.*; import java.io.File; import java.io.IOException; @@ -17,7 +14,7 @@ final class ElementStyleParser extends AbstractParser { private static final int FIRST_PROPERTY_INDEX = 1; - ElementStyle parseElementStyle(DslContext context, Tokens tokens) { + ElementStyle parseElementStyle(StylesDslContext context, Tokens tokens) { if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { throw new RuntimeException("Too many tokens, expected: element {"); } else if (tokens.includes(FIRST_PROPERTY_INDEX)) { @@ -28,9 +25,9 @@ ElementStyle parseElementStyle(DslContext context, Tokens tokens) { } Workspace workspace = context.getWorkspace(); - ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle(tag); + ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle(tag, context.getColorScheme()); if (elementStyle == null) { - elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle(tag); + elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle(tag, context.getColorScheme()); } return elementStyle; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java index 6c260c3bf..dd8d28a72 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java @@ -2,9 +2,7 @@ import com.structurizr.Workspace; import com.structurizr.util.StringUtils; -import com.structurizr.view.LineStyle; -import com.structurizr.view.RelationshipStyle; -import com.structurizr.view.Routing; +import com.structurizr.view.*; import java.util.HashMap; import java.util.Map; @@ -13,7 +11,7 @@ final class RelationshipStyleParser extends AbstractParser { private static final int FIRST_PROPERTY_INDEX = 1; - RelationshipStyle parseRelationshipStyle(DslContext context, Tokens tokens) { + RelationshipStyle parseRelationshipStyle(StylesDslContext context, Tokens tokens) { if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { throw new RuntimeException("Too many tokens, expected: relationship {"); } @@ -26,9 +24,9 @@ RelationshipStyle parseRelationshipStyle(DslContext context, Tokens tokens) { } Workspace workspace = context.getWorkspace(); - RelationshipStyle relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle(tag); + RelationshipStyle relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle(tag, context.getColorScheme()); if (relationshipStyle == null) { - relationshipStyle = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(tag); + relationshipStyle = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(tag, context.getColorScheme()); } return relationshipStyle; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 809a9e4d1..636516db7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -791,8 +791,14 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (STYLES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { startContext(new StylesDslContext()); + } else if (LIGHT_COLOR_SCHEME_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) { + startContext(new StylesDslContext(ColorScheme.Light)); + + } else if (DARK_COLOR_SCHEME_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) { + startContext(new StylesDslContext(ColorScheme.Dark)); + } else if (ELEMENT_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) { - ElementStyle elementStyle = new ElementStyleParser().parseElementStyle(getContext(), tokens.withoutContextStartToken()); + ElementStyle elementStyle = new ElementStyleParser().parseElementStyle(getContext(StylesDslContext.class), tokens.withoutContextStartToken()); startContext(new ElementStyleDslContext(elementStyle, dslFile)); } else if (ELEMENT_STYLE_BACKGROUND_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { @@ -838,7 +844,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new ElementStyleParser().parseIconPosition(getContext(ElementStyleDslContext.class), tokens); } else if (RELATIONSHIP_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) { - RelationshipStyle relationshipStyle = new RelationshipStyleParser().parseRelationshipStyle(getContext(), tokens.withoutContextStartToken()); + RelationshipStyle relationshipStyle = new RelationshipStyleParser().parseRelationshipStyle(getContext(StylesDslContext.class), tokens.withoutContextStartToken()); startContext(new RelationshipStyleDslContext(relationshipStyle)); } else if (RELATIONSHIP_STYLE_THICKNESS_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 67bc67f56..7b38a7c11 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -60,6 +60,8 @@ class StructurizrDslTokens { static final String KROKI_TOKEN = "kroki"; static final String IMAGE_TOKEN = "image"; static final String STYLES_TOKEN = "styles"; + static final String LIGHT_COLOR_SCHEME_TOKEN = "light"; + static final String DARK_COLOR_SCHEME_TOKEN = "dark"; static final String BRANDING_TOKEN = "branding"; static final String BRANDING_LOGO_TOKEN = "logo"; static final String BRANDING_FONT_TOKEN = "font"; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java index a27234faf..a374a54df 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java @@ -1,7 +1,23 @@ package com.structurizr.dsl; +import com.structurizr.view.ColorScheme; + final class StylesDslContext extends DslContext { + private final ColorScheme colorScheme; + + StylesDslContext() { + colorScheme = null; + } + + StylesDslContext(ColorScheme colorScheme) { + this.colorScheme = colorScheme; + } + + ColorScheme getColorScheme() { + return colorScheme; + } + @Override protected String[] getPermittedTokens() { return new String[] { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 8761713fd..a613b53c7 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -4,7 +4,6 @@ import com.structurizr.documentation.Section; import com.structurizr.model.*; import com.structurizr.util.StringUtils; -import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -1712,4 +1711,30 @@ void test_deploymentGroups_Hierarchical() throws Exception { assertFalse(apiInstance2.hasEfferentRelationshipWith(dbInstance1)); } + @Test + void test_colorSchemes() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/color-schemes.dsl")); + + Workspace workspace = parser.getWorkspace(); + + ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle("Element"); + assertEquals(Shape.RoundedBox, elementStyle.getShape()); + + elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle("Element", ColorScheme.Light); + assertEquals("#000000", elementStyle.getColor()); + + elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle("Element", ColorScheme.Dark); + assertEquals("#ffffff", elementStyle.getColor()); + + RelationshipStyle relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle("Relationship"); + assertEquals(LineStyle.Solid, relationshipStyle.getStyle()); + + relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle("Relationship", ColorScheme.Light); + assertEquals("#000000", relationshipStyle.getColor()); + + relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle("Relationship", ColorScheme.Dark); + assertEquals("#ffffff", relationshipStyle.getColor()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java index a3519604e..c876abd0f 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -23,10 +23,17 @@ private ElementStyleDslContext elementStyleDslContext() { return context; } + private StylesDslContext stylesDslContext() { + StylesDslContext context = new StylesDslContext(); + context.setWorkspace(workspace); + + return context; + } + @Test void test_parseElementStyle_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parseElementStyle(context(), tokens("element", "tag", "extra")); + parser.parseElementStyle(stylesDslContext(), tokens("element", "tag", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: element {", e.getMessage()); @@ -36,7 +43,7 @@ void test_parseElementStyle_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseElementStyle_ThrowsAnException_WhenTheTagIsMissing() { try { - parser.parseElementStyle(context(), tokens("element")); + parser.parseElementStyle(stylesDslContext(), tokens("element")); fail(); } catch (Exception e) { assertEquals("Expected: element {", e.getMessage()); @@ -46,7 +53,7 @@ void test_parseElementStyle_ThrowsAnException_WhenTheTagIsMissing() { @Test void test_parseElementStyle_ThrowsAnException_WhenTheTagIsEmpty() { try { - parser.parseElementStyle(context(), tokens("element", "")); + parser.parseElementStyle(stylesDslContext(), tokens("element", "")); fail(); } catch (Exception e) { assertEquals("A tag must be specified", e.getMessage()); @@ -55,7 +62,7 @@ void test_parseElementStyle_ThrowsAnException_WhenTheTagIsEmpty() { @Test void test_parseElementStyle_CreatesAnElementStyle() { - parser.parseElementStyle(context(), tokens("element", "Element")); + parser.parseElementStyle(stylesDslContext(), tokens("element", "Element")); ElementStyle style = workspace.getViews().getConfiguration().getStyles().getElements().stream().filter(es -> "Element".equals(es.getTag())).findFirst().get(); assertNotNull(style); @@ -64,7 +71,7 @@ void test_parseElementStyle_CreatesAnElementStyle() { @Test void test_parseElementStyle_FindsAnExistingElementStyle() { ElementStyle style = workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag"); - assertSame(style, parser.parseElementStyle(context(), tokens("element", "Tag"))); + assertSame(style, parser.parseElementStyle(stylesDslContext(), tokens("element", "Tag"))); } @Test diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java index 8736c5acb..645d747b2 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java @@ -21,10 +21,17 @@ private RelationshipStyleDslContext relationshipStyleDslContext() { return context; } + private StylesDslContext stylesDslContext() { + StylesDslContext context = new StylesDslContext(); + context.setWorkspace(workspace); + + return context; + } + @Test void test_parseRelationshipStyle_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parseRelationshipStyle(context(), tokens("relationship", "tag", "extra")); + parser.parseRelationshipStyle(stylesDslContext(), tokens("relationship", "tag", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: relationship {", e.getMessage()); @@ -34,7 +41,7 @@ void test_parseRelationshipStyle_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseRelationshipStyle_ThrowsAnException_WhenTheTagIsMissing() { try { - parser.parseRelationshipStyle(context(), tokens("relationship")); + parser.parseRelationshipStyle(stylesDslContext(), tokens("relationship")); fail(); } catch (Exception e) { assertEquals("Expected: relationship {", e.getMessage()); @@ -44,7 +51,7 @@ void test_parseRelationshipStyle_ThrowsAnException_WhenTheTagIsMissing() { @Test void test_parseRelationshipStyle_ThrowsAnException_WhenTheTagIsEmpty() { try { - parser.parseRelationshipStyle(context(), tokens("relationship", "")); + parser.parseRelationshipStyle(stylesDslContext(), tokens("relationship", "")); fail(); } catch (Exception e) { assertEquals("A tag must be specified", e.getMessage()); @@ -53,7 +60,7 @@ void test_parseRelationshipStyle_ThrowsAnException_WhenTheTagIsEmpty() { @Test void test_parseRelationshipStyle_CreatesAnRelationshipStyle() { - parser.parseRelationshipStyle(context(), tokens("relationship", "Relationship")); + parser.parseRelationshipStyle(stylesDslContext(), tokens("relationship", "Relationship")); RelationshipStyle style = workspace.getViews().getConfiguration().getStyles().getRelationships().stream().filter(es -> "Relationship".equals(es.getTag())).findFirst().get(); assertNotNull(style); @@ -62,7 +69,7 @@ void test_parseRelationshipStyle_CreatesAnRelationshipStyle() { @Test void test_parseRelationshipStyle_FindsAnExistingRelationshipStyle() { RelationshipStyle style = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag"); - assertSame(style, parser.parseRelationshipStyle(context(), tokens("relationship", "Tag"))); + assertSame(style, parser.parseRelationshipStyle(stylesDslContext(), tokens("relationship", "Tag"))); } @Test diff --git a/structurizr-dsl/src/test/resources/dsl/color-schemes.dsl b/structurizr-dsl/src/test/resources/dsl/color-schemes.dsl new file mode 100644 index 000000000..aeeb22134 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/color-schemes.dsl @@ -0,0 +1,31 @@ +workspace { + + views { + styles { + element "Element" { + shape roundedbox + } + relationship "Relationship" { + style solid + } + + light { + element "Element" { + colour #000000 + } + relationship "Relationship" { + colour #000000 + } + } + + dark { + element "Element" { + colour #ffffff + } + relationship "Relationship" { + colour #ffffff + } + } + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index c6150affd..286853104 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -308,6 +308,7 @@ workspace "Name" "Description" { element "Element" { shape roundedbox icon logo.png + iconPosition left width 450 height 300 background #ffffff @@ -339,6 +340,26 @@ workspace "Name" "Description" { } } + light { + element "Element" { + background #ffffff + } + + relationship "Relationship" { + color #777777 + } + } + + dark { + element "Element" { + background #000000 + } + + relationship "Relationship" { + color #777777 + } + } + theme https://example.com/theme1 themes https://example.com/theme2 https://example.com/theme3 } From 48ed19c58cebac99366f9710a59be8b42ce6ad9c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 27 Jun 2025 14:03:10 +0100 Subject: [PATCH 348/418] Adds a `Bucket` shape. --- changelog.md | 1 + .../src/main/java/com/structurizr/view/Shape.java | 1 + .../java/com/structurizr/dsl/ElementStyleParserTests.java | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index f0355de16..e22ef0c29 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). - structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. +- structurizr-dsl: Adds a `Bucket` shape. ## v4.1.0 (28th May 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/view/Shape.java b/structurizr-core/src/main/java/com/structurizr/view/Shape.java index 33232c99f..4fcb6f8b3 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Shape.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Shape.java @@ -9,6 +9,7 @@ public enum Shape { Hexagon, Diamond, Cylinder, + Bucket, Pipe, Person, Robot, diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java index c876abd0f..294ad0b6e 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -80,7 +80,7 @@ void test_parseShape_ThrowsAnException_WhenThereAreTooManyTokens() { parser.parseShape(elementStyleDslContext(), tokens("shape", "shape", "extra")); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: shape ", e.getMessage()); + assertEquals("Too many tokens, expected: shape ", e.getMessage()); } } @@ -90,7 +90,7 @@ void test_parseShape_ThrowsAnException_WhenTheShapeIsMissing() { parser.parseShape(elementStyleDslContext(), tokens("shape")); fail(); } catch (Exception e) { - assertEquals("Expected: shape ", e.getMessage()); + assertEquals("Expected: shape ", e.getMessage()); } } From 90fd0905a14f91e340736a79aa2fc726b20477cf Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 27 Jul 2025 22:49:29 +0100 Subject: [PATCH 349/418] Adds a "Shell" shape. --- changelog.md | 1 + .../src/main/java/com/structurizr/view/Shape.java | 1 + .../java/com/structurizr/dsl/ElementStyleParserTests.java | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index e22ef0c29..4c7b5a7d8 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). - structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. - structurizr-dsl: Adds a `Bucket` shape. +- structurizr-dsl: Adds a `Shell` shape. ## v4.1.0 (28th May 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/view/Shape.java b/structurizr-core/src/main/java/com/structurizr/view/Shape.java index 4fcb6f8b3..2bddbd52d 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Shape.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Shape.java @@ -16,6 +16,7 @@ public enum Shape { Folder, WebBrowser, Window, + Shell, MobileDevicePortrait, MobileDeviceLandscape, Component diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java index 294ad0b6e..bc25ff100 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -80,7 +80,7 @@ void test_parseShape_ThrowsAnException_WhenThereAreTooManyTokens() { parser.parseShape(elementStyleDslContext(), tokens("shape", "shape", "extra")); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: shape ", e.getMessage()); + assertEquals("Too many tokens, expected: shape ", e.getMessage()); } } @@ -90,7 +90,7 @@ void test_parseShape_ThrowsAnException_WhenTheShapeIsMissing() { parser.parseShape(elementStyleDslContext(), tokens("shape")); fail(); } catch (Exception e) { - assertEquals("Expected: shape ", e.getMessage()); + assertEquals("Expected: shape ", e.getMessage()); } } From 7eacd41d9643c9064275f39a11dab03818f2a4d8 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 29 Jul 2025 10:03:47 +0100 Subject: [PATCH 350/418] Adds a "Terminal" shape. --- changelog.md | 1 + .../src/main/java/com/structurizr/view/Shape.java | 1 + .../java/com/structurizr/dsl/ElementStyleParserTests.java | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 4c7b5a7d8..6748a732e 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ - structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. - structurizr-dsl: Adds a `Bucket` shape. - structurizr-dsl: Adds a `Shell` shape. +- structurizr-dsl: Adds a `Terminal` shape. ## v4.1.0 (28th May 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/view/Shape.java b/structurizr-core/src/main/java/com/structurizr/view/Shape.java index 2bddbd52d..40b2c1b34 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Shape.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Shape.java @@ -16,6 +16,7 @@ public enum Shape { Folder, WebBrowser, Window, + Terminal, Shell, MobileDevicePortrait, MobileDeviceLandscape, diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java index bc25ff100..83e72cb77 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -80,7 +80,7 @@ void test_parseShape_ThrowsAnException_WhenThereAreTooManyTokens() { parser.parseShape(elementStyleDslContext(), tokens("shape", "shape", "extra")); fail(); } catch (Exception e) { - assertEquals("Too many tokens, expected: shape ", e.getMessage()); + assertEquals("Too many tokens, expected: shape ", e.getMessage()); } } @@ -90,7 +90,7 @@ void test_parseShape_ThrowsAnException_WhenTheShapeIsMissing() { parser.parseShape(elementStyleDslContext(), tokens("shape")); fail(); } catch (Exception e) { - assertEquals("Expected: shape ", e.getMessage()); + assertEquals("Expected: shape ", e.getMessage()); } } From 2546f308c6470db23045682bd01c822c23285f70 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 1 Aug 2025 12:22:33 +0100 Subject: [PATCH 351/418] Adds an 'instanceOf' keyword (an alternative for `softwareSystemInstance` and `containerInstance`). --- changelog.md | 1 + .../dsl/DeploymentNodeDslContext.java | 1 + .../com/structurizr/dsl/InstanceOfParser.java | 40 ++++++ .../structurizr/dsl/StructurizrDslParser.java | 15 +++ .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../dsl/ContainerInstanceParserTests.java | 2 +- .../dsl/InstanceOfParserTests.java | 115 ++++++++++++++++++ .../SoftwareSystemInstanceParserTests.java | 2 +- .../src/test/resources/dsl/test.dsl | 24 ++++ 9 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/InstanceOfParser.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/InstanceOfParserTests.java diff --git a/changelog.md b/changelog.md index 6748a732e..6f3d8f8a2 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ - structurizr-dsl: Adds a `Bucket` shape. - structurizr-dsl: Adds a `Shell` shape. - structurizr-dsl: Adds a `Terminal` shape. +- structurizr-dsl: Adds an 'instanceOf' keyword (an alternative for `softwareSystemInstance` and `containerInstance`). ## v4.1.0 (28th May 2025) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java index 0cfecda3d..a347f75c7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java @@ -38,6 +38,7 @@ protected String[] getPermittedTokens() { StructurizrDslTokens.GROUP_TOKEN, StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, + StructurizrDslTokens.INSTANCE_OF_TOKEN, StructurizrDslTokens.SOFTWARE_SYSTEM_INSTANCE_TOKEN, StructurizrDslTokens.CONTAINER_INSTANCE_TOKEN, StructurizrDslTokens.RELATIONSHIP_TOKEN, diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/InstanceOfParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InstanceOfParser.java new file mode 100644 index 000000000..2a0d4264c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InstanceOfParser.java @@ -0,0 +1,40 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +final class InstanceOfParser { + + private static final String GRAMMAR = "instanceOf [deploymentGroups] [tags]"; + + private static final int IDENTIFIER_INDEX = 1; + private static final int TAGS_INDEX = 3; + + StaticStructureElementInstance parse(DeploymentNodeDslContext context, Tokens tokens) { + // instanceOf [tags] + // instanceOf [deploymentGroup] [tags] + + if (tokens.hasMoreThan(TAGS_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String elementIdentifier = tokens.get(IDENTIFIER_INDEX); + + Element element = context.getElement(elementIdentifier); + if (element == null) { + throw new RuntimeException("The element \"" + elementIdentifier + "\" does not exist"); + } + + if (element instanceof SoftwareSystem) { + return new SoftwareSystemInstanceParser().parse(context, tokens); + } else if (element instanceof Container) { + return new ContainerInstanceParser().parse(context, tokens); + } else { + throw new RuntimeException("The element \"" + elementIdentifier + "\" must be a software system or a container"); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 636516db7..b68d40cee 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -916,6 +916,21 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, infrastructureNode); + } else if (INSTANCE_OF_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { + StaticStructureElementInstance instance = new InstanceOfParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); + + if (instance instanceof SoftwareSystemInstance) { + if (shouldStartContext(tokens)) { + startContext(new SoftwareSystemInstanceDslContext((SoftwareSystemInstance)instance)); + } + } else if (instance instanceof ContainerInstance) { + if (shouldStartContext(tokens)) { + startContext(new ContainerInstanceDslContext((ContainerInstance)instance)); + } + } + + registerIdentifier(identifier, instance); + } else if (SOFTWARE_SYSTEM_INSTANCE_TOKEN.equalsIgnoreCase(firstToken) && inContext(DeploymentNodeDslContext.class)) { SoftwareSystemInstance softwareSystemInstance = new SoftwareSystemInstanceParser().parse(getContext(DeploymentNodeDslContext.class), tokens.withoutContextStartToken()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 7b38a7c11..59f33b275 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -35,6 +35,7 @@ class StructurizrDslTokens { static final String DEPLOYMENT_GROUP_TOKEN = "deploymentGroup"; static final String DEPLOYMENT_NODE_TOKEN = "deploymentNode"; static final String INFRASTRUCTURE_NODE_TOKEN = "infrastructureNode"; + static final String INSTANCE_OF_TOKEN = "instanceOf"; static final String SOFTWARE_SYSTEM_INSTANCE_TOKEN = "softwareSystemInstance"; static final String CONTAINER_INSTANCE_TOKEN = "containerInstance"; static final String HEALTH_CHECK_TOKEN = "healthCheck"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java index ba62530c0..98dde6ece 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java @@ -7,7 +7,7 @@ class ContainerInstanceParserTests extends AbstractTests { - private ContainerInstanceParser parser = new ContainerInstanceParser(); + private final ContainerInstanceParser parser = new ContainerInstanceParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/InstanceOfParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/InstanceOfParserTests.java new file mode 100644 index 000000000..494e90fff --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/InstanceOfParserTests.java @@ -0,0 +1,115 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class InstanceOfParserTests extends AbstractTests { + + private final InstanceOfParser parser = new InstanceOfParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("instanceOf", "identifier", "deploymentGroups", "tags", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: instanceOf [deploymentGroups] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierIsNotSpecified() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("instanceOf")); + fail(); + } catch (Exception e) { + assertEquals("Expected: instanceOf [deploymentGroups] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementDoesNotExist() { + try { + parser.parse(new DeploymentNodeDslContext(null), tokens("instanceOf", "softwareSystem")); + fail(); + } catch (Exception e) { + assertEquals("The element \"softwareSystem\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotASoftwareSystem() { + DeploymentNodeDslContext context = new DeploymentNodeDslContext(null); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("instanceOf", "softwareSystem")); + fail(); + } catch (Exception e) { + assertEquals("The element \"softwareSystem\" must be a software system or a container", e.getMessage()); + } + } + + @Test + void test_parse_CreatesASoftwareSystemInstance() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwaresystem", softwareSystem); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("instanceOf", "softwareSystem")); + + assertEquals(3, model.getElements().size()); + assertEquals(1, deploymentNode.getSoftwareSystemInstances().size()); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.getSoftwareSystemInstances().iterator().next(); + assertSame(softwareSystem, softwareSystemInstance.getSoftwareSystem()); + assertEquals("Software System Instance", softwareSystemInstance.getTags()); + assertEquals("Live", softwareSystemInstance.getEnvironment()); + assertEquals(1, softwareSystemInstance.getDeploymentGroups().size()); + assertEquals("Default", softwareSystemInstance.getDeploymentGroups().iterator().next()); + } + + @Test + void test_parse_ThrowsAnException_WhenTheElementIsNotAContainer() { + DeploymentNodeDslContext context = new DeploymentNodeDslContext(null); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", model.addPerson("Name", "Description")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("instanceOf", "container")); + fail(); + } catch (Exception e) { + assertEquals("The element \"container\" must be a software system or a container", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAContainerInstance() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + DeploymentNodeDslContext context = new DeploymentNodeDslContext(deploymentNode); + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("instanceOf", "container")); + + assertEquals(4, model.getElements().size()); + assertEquals(1, deploymentNode.getContainerInstances().size()); + ContainerInstance containerInstance = deploymentNode.getContainerInstances().iterator().next(); + assertSame(container, containerInstance.getContainer()); + assertEquals("Container Instance", containerInstance.getTags()); + assertEquals("Live", containerInstance.getEnvironment()); + assertEquals(1, containerInstance.getDeploymentGroups().size()); + assertEquals("Default", containerInstance.getDeploymentGroups().iterator().next()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java index c25ad5e43..290c56a0b 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java @@ -7,7 +7,7 @@ class SoftwareSystemInstanceParserTests extends AbstractTests { - private SoftwareSystemInstanceParser parser = new SoftwareSystemInstanceParser(); + private final SoftwareSystemInstanceParser parser = new SoftwareSystemInstanceParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 286853104..d2de4ed88 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -109,6 +109,18 @@ workspace "Name" "Description" { healthCheck "Check 2" "https://example.com/health" 60 healthCheck "Check 2" "https://example.com/health" 120 1000 } + instanceOf softwareSystem { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + healthCheck "Check 1" "https://example.com/health" + healthCheck "Check 2" "https://example.com/health" 60 + healthCheck "Check 2" "https://example.com/health" 120 1000 + } } } @@ -138,6 +150,18 @@ workspace "Name" "Description" { healthCheck "Check 2" "https://example.com/health" 60 healthCheck "Check 2" "https://example.com/health" 120 1000 } + instanceOf webApplication { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + healthCheck "Check 1" "https://example.com/health" + healthCheck "Check 2" "https://example.com/health" 60 + healthCheck "Check 2" "https://example.com/health" 120 1000 + } } url "https://structurizr.com" From 2e9274f6f8be498bd30788c196d4271c804222a2 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 3 Aug 2025 08:47:46 +0100 Subject: [PATCH 352/418] Adds missing tests, fixes typo in message. --- .../com/structurizr/dsl/AbstractParser.java | 12 ------- .../structurizr/dsl/AbstractViewParser.java | 12 +++++++ .../dsl/AbstractViewParserTests.java | 34 +++++++++++++++++++ 3 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractViewParserTests.java diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java index 2e9ede6b2..5d41ec735 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java @@ -4,24 +4,12 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.io.entity.EntityUtils; -import java.util.regex.Pattern; - abstract class AbstractParser { private static final int HTTP_OK_STATUS = 200; - private static final Pattern VIEW_KEY_PATTERN = Pattern.compile("[\\w-]+"); - - void validateViewKey(String key) { - if (!VIEW_KEY_PATTERN.matcher(key).matches()) { - throw new RuntimeException("View keys can only contain the following characters: a-zA-0-9_-"); - } - } - String removeNonWordCharacters(String name) { return name.replaceAll("\\W", ""); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java index 8f66d3e9b..c47bfabe8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java @@ -1,4 +1,16 @@ package com.structurizr.dsl; +import java.util.regex.Pattern; + abstract class AbstractViewParser extends AbstractParser { + + private static final String PERMITTED_CHARACTERS_IN_VIEW_KEY = "a-zA-Z0-9_-"; + private static final Pattern VIEW_KEY_PATTERN = Pattern.compile("[" + PERMITTED_CHARACTERS_IN_VIEW_KEY + "]+"); + + void validateViewKey(String key) { + if (!VIEW_KEY_PATTERN.matcher(key).matches()) { + throw new RuntimeException("View keys can only contain the following characters: a-zA-Z0-9_-"); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractViewParserTests.java new file mode 100644 index 000000000..7322b1acc --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractViewParserTests.java @@ -0,0 +1,34 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class AbstractViewParserTests { + + private final AbstractViewParser parser = new SystemLandscapeViewParser(); + + @Test + void test_validateViewKey() { + parser.validateViewKey("key"); + parser.validateViewKey("key123"); + parser.validateViewKey("Key123"); + parser.validateViewKey("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"); + + try { + parser.validateViewKey("abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789"); + fail(); + } catch (Exception e) { + assertEquals("View keys can only contain the following characters: a-zA-Z0-9_-", e.getMessage()); + } + + try { + parser.validateViewKey("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789"); + fail(); + } catch (Exception e) { + assertEquals("View keys can only contain the following characters: a-zA-Z0-9_-", e.getMessage()); + } + } + +} \ No newline at end of file From b7b76b54be7b1ddd06c0b93553f141ea699a6767 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 3 Aug 2025 09:24:05 +0100 Subject: [PATCH 353/418] Adds the ability to remove a relationship from the workspace. --- .../main/java/com/structurizr/Workspace.java | 30 +++++++++++++++++++ .../java/com/structurizr/model/Model.java | 13 ++++++-- .../java/com/structurizr/view/ModelView.java | 16 ++++++++++ .../java/com/structurizr/WorkspaceTests.java | 28 +++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/structurizr-core/src/main/java/com/structurizr/Workspace.java b/structurizr-core/src/main/java/com/structurizr/Workspace.java index 98ab55e46..46e2b7fdd 100644 --- a/structurizr-core/src/main/java/com/structurizr/Workspace.java +++ b/structurizr-core/src/main/java/com/structurizr/Workspace.java @@ -310,6 +310,36 @@ void remove(DeploymentNode deploymentNode) { } } + /** + * Removes a relationship from the workspace. + * + * @param relationship the Relationship to remove + */ + public void remove(Relationship relationship) { + if (relationship == null) { + throw new IllegalArgumentException("A relationship must be specified."); + } + + // remove the relationship from views + for (View view : viewSet.getViews()) { + if (view instanceof ModelView) { + ModelView modelView = (ModelView)view; + if (modelView.isRelationshipInView(relationship)) { + modelView.remove(relationship); + } + } + } + + // now remove the relationship itself + try { + Method method = Model.class.getDeclaredMethod("remove", Relationship.class); + method.setAccessible(true); + method.invoke(model, relationship); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private boolean isElementAssociatedWithAnyViews(Element element) { boolean result = false; diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 57d947b2d..95964fa88 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -1171,8 +1171,7 @@ private void removeElement(Element element) { // remove any relationships to/from the element for (Relationship relationship : getRelationships()) { if (relationship.getSource() == element || relationship.getDestination() == element) { - removeRelationshipFromInternalStructures(relationship); - relationship.getSource().remove(relationship); + remove(relationship); } } @@ -1180,4 +1179,14 @@ private void removeElement(Element element) { elements.remove(element); } + /** + * Removes a relationship from the model. + * + * @param relationship the Relationship to remove + */ + void remove(Relationship relationship) { + removeRelationshipFromInternalStructures(relationship); + relationship.getSource().remove(relationship); + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java index 5ced49fed..90f0a5bdc 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ModelView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java @@ -272,10 +272,26 @@ protected RelationshipView addRelationship(Relationship relationship) { return null; } + /** + * Determines whether the specified element exists in this view. + * + * @param element the Element to look for + * @return true if the element exists in the view, false otherwise + */ public boolean isElementInView(Element element) { return this.elementViews.stream().anyMatch(ev -> ev.getElement().equals(element)); } + /** + * Determines whether the specified relationship exists in this view. + * + * @param relationship the Relationship to look for + * @return true if the relationship exists in the view, false otherwise + */ + public boolean isRelationshipInView(Relationship relationship) { + return this.relationshipViews.stream().anyMatch(rv -> rv.getRelationship().equals(relationship)); + } + /** * Removes a relationship from this view. * diff --git a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java index c22b582be..ad55331ad 100644 --- a/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java @@ -300,4 +300,32 @@ void trim_WhenTheDestinationOfAnElementIsRemoved() { assertEquals(0, a.getRelationships().size()); } + @Test + void removeRelationship_ThrowsAnException_WhenNoRelationshipIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + try { + workspace.remove((Relationship)null); + fail(); + } catch (Exception e) { + assertEquals("A relationship must be specified.", e.getMessage()); + } + } + + @Test + void removeRelationship() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + workspace.remove(relationship); + + assertEquals(0, a.getRelationships().size()); + assertFalse(a.hasEfferentRelationshipWith(b)); + assertFalse(view.isRelationshipInView(relationship)); + } + } \ No newline at end of file From d8467b34782c2748338229e5802b6515761c3d00 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 20 Aug 2025 13:02:43 +0100 Subject: [PATCH 354/418] Relationships to/from software system/container instances can be now defined by using the software system/container identifier. --- .../dsl/AbstractRelationshipParser.java | 12 ++ .../dsl/ExplicitRelationshipParser.java | 59 +++++++- .../dsl/ImplicitRelationshipParser.java | 45 +++++- .../structurizr/dsl/StructurizrDslParser.java | 34 +++-- .../dsl/ExplicitRelationshipParserTests.java | 134 +++++++++++++++++- .../dsl/ImplicitRelationshipParserTests.java | 65 +++++++++ 6 files changed, 326 insertions(+), 23 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java index d8ffb28c6..c9b5adfdc 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java @@ -2,6 +2,10 @@ import com.structurizr.model.*; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + abstract class AbstractRelationshipParser extends AbstractParser { protected Relationship createRelationship(Element sourceElement, String description, String technology, String[] tags, Element destinationElement) { @@ -34,4 +38,12 @@ protected Relationship createRelationship(Element sourceElement, String descript return relationship; } + protected Set findSoftwareSystemInstances(SoftwareSystem softwareSystem, String deploymentEnvironment) { + return softwareSystem.getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).map(e -> (SoftwareSystemInstance) e).filter(ssi -> ssi.getSoftwareSystem().equals(softwareSystem) && ssi.getEnvironment().equals(deploymentEnvironment)).collect(Collectors.toSet()); + } + + protected Set findContainerInstances(Container container, String deploymentEnvironment) { + return container.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance) e).filter(ci -> ci.getContainer().equals(container) && ci.getEnvironment().equals(deploymentEnvironment)).collect(Collectors.toSet()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index 38e225b22..affc90fb1 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -1,7 +1,6 @@ package com.structurizr.dsl; -import com.structurizr.model.Element; -import com.structurizr.model.Relationship; +import com.structurizr.model.*; import javax.lang.model.util.Elements; import java.util.*; @@ -16,9 +15,11 @@ final class ExplicitRelationshipParser extends AbstractRelationshipParser { private final static int TECHNOLOGY_INDEX = 4; private final static int TAGS_INDEX = 5; - Relationship parse(DslContext context, Tokens tokens, Archetype archetype) { + Set parse(DslContext context, Tokens tokens, Archetype archetype) { // -> [description] [technology] [tags] + Set relationships = new HashSet<>(); + if (tokens.hasMoreThan(TAGS_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); } @@ -54,11 +55,55 @@ Relationship parse(DslContext context, Tokens tokens, Archetype archetype) { tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - Relationship relationship = createRelationship(sourceElement, description, technology, tags.toArray(new String[0]), destinationElement); - relationship.addProperties(archetype.getProperties()); - relationship.addPerspectives(archetype.getPerspectives()); + Set sourceElements = new HashSet<>(); + Set destinationElements = new HashSet<>(); + + if (context instanceof DeploymentEnvironmentDslContext || context instanceof DeploymentNodeDslContext) { + String deploymentEnvironment; + if (context instanceof DeploymentEnvironmentDslContext) { + deploymentEnvironment = ((DeploymentEnvironmentDslContext)context).getEnvironment().getName(); + } else { + deploymentEnvironment = ((DeploymentNodeDslContext)context).getDeploymentNode().getEnvironment(); + } + + if (sourceElement instanceof SoftwareSystem) { + // find the software system instances in the deployment environment + sourceElements = findSoftwareSystemInstances((SoftwareSystem)sourceElement, deploymentEnvironment); + } else if (sourceElement instanceof Container) { + // find the container instances in the deployment environment + sourceElements = findContainerInstances((Container)sourceElement, deploymentEnvironment); + } else { + sourceElements.add(sourceElement); + } + + if (destinationElement instanceof SoftwareSystem) { + // find the software system instances in the deployment environment + destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment); + } else if (destinationElement instanceof Container) { + // find the container instances in the deployment environment + destinationElements = findContainerInstances((Container)destinationElement, deploymentEnvironment); + } else { + destinationElements.add(destinationElement); + } + + for (Element se : sourceElements) { + for (Element de : destinationElements) { + Relationship relationship = createRelationship(se, description, technology, tags.toArray(new String[0]), de); + relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); + + relationships.add(relationship); + } + } + } else { + Relationship relationship = createRelationship(sourceElement, description, technology, tags.toArray(new String[0]), destinationElement); + relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); - return relationship; + relationships.add(relationship); + } + + return relationships; } Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index 7c10b7015..e97e8cb8d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -1,7 +1,9 @@ package com.structurizr.dsl; +import com.structurizr.model.Container; import com.structurizr.model.Element; import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; import java.util.*; @@ -14,9 +16,11 @@ final class ImplicitRelationshipParser extends AbstractRelationshipParser { private final static int TECHNOLOGY_INDEX = 3; private final static int TAGS_INDEX = 4; - Relationship parse(ElementDslContext context, Tokens tokens, Archetype archetype) { + Set parse(ElementDslContext context, Tokens tokens, Archetype archetype) { // -> [description] [technology] [tags] + Set relationships = new HashSet<>(); + if (tokens.hasMoreThan(TAGS_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); } @@ -49,11 +53,42 @@ Relationship parse(ElementDslContext context, Tokens tokens, Archetype archetype tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); } - Relationship relationship = createRelationship(sourceElement, description, technology, tags.toArray(new String[0]), destinationElement); - relationship.addProperties(archetype.getProperties()); - relationship.addPerspectives(archetype.getPerspectives()); + Set sourceElements = new HashSet<>(); + Set destinationElements = new HashSet<>(); + + if (context instanceof InfrastructureNodeDslContext) { + String deploymentEnvironment = ((InfrastructureNodeDslContext)context).getInfrastructureNode().getEnvironment(); + + sourceElements.add(sourceElement); + + if (destinationElement instanceof SoftwareSystem) { + // find the software system instances in the deployment environment + destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment); + } else if (destinationElement instanceof Container) { + // find the container instances in the deployment environment + destinationElements = findContainerInstances((Container)destinationElement, deploymentEnvironment); + } else { + destinationElements.add(destinationElement); + } + + for (Element se : sourceElements) { + for (Element de : destinationElements) { + Relationship relationship = createRelationship(se, description, technology, tags.toArray(new String[0]), de); + relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); + + relationships.add(relationship); + } + } + } else { + Relationship relationship = createRelationship(sourceElement, description, technology, tags.toArray(new String[0]), destinationElement); + relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); - return relationship; + relationships.add(relationship); + } + + return relationships; } Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index b68d40cee..322b25b67 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -327,25 +327,39 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn // explicit without archetype: a -> b // explicit with archetype: a --https-> b Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); - Relationship relationship = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken(), archetype); + Set relationships = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken(), archetype); - if (shouldStartContext(tokens)) { - startContext(new RelationshipDslContext(relationship)); - } + if (relationships.size() == 1) { + Relationship relationship = relationships.iterator().next(); + registerIdentifier(identifier, relationship); - registerIdentifier(identifier, relationship); + if (shouldStartContext(tokens)) { + startContext(new RelationshipDslContext(relationship)); + } + } else { + if (shouldStartContext(tokens)) { + startContext(new RelationshipsDslContext(getContext(), relationships)); + } + } } else if (tokens.size() >= 2 && isRelationshipKeywordOrArchetype(tokens.get(0)) && inContext(ElementDslContext.class)) { // implicit without archetype: -> this // implicit with archetype: --https-> this Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); - Relationship relationship = new ImplicitRelationshipParser().parse(getContext(ElementDslContext.class), tokens.withoutContextStartToken(), archetype); + Set relationships = new ImplicitRelationshipParser().parse(getContext(ElementDslContext.class), tokens.withoutContextStartToken(), archetype); - if (shouldStartContext(tokens)) { - startContext(new RelationshipDslContext(relationship)); - } + if (relationships.size() == 1) { + Relationship relationship = relationships.iterator().next(); + registerIdentifier(identifier, relationship); - registerIdentifier(identifier, relationship); + if (shouldStartContext(tokens)) { + startContext(new RelationshipDslContext(relationship)); + } + } else { + if (shouldStartContext(tokens)) { + startContext(new RelationshipsDslContext(getContext(), relationships)); + } + } } else if (tokens.size() > 2 && isRelationshipKeywordOrArchetype(tokens.get(1)) && inContext(ElementsDslContext.class)) { Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java index 1cd8f213d..a1f540ea4 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java @@ -3,11 +3,13 @@ import com.structurizr.model.*; import org.junit.jupiter.api.Test; +import java.util.Set; + import static org.junit.jupiter.api.Assertions.*; class ExplicitRelationshipParserTests extends AbstractTests { - private ExplicitRelationshipParser parser = new ExplicitRelationshipParser(); + private final ExplicitRelationshipParser parser = new ExplicitRelationshipParser(); private Archetype archetype = new Archetype("name", "type"); @Test @@ -259,4 +261,134 @@ void test_parse_AddsTheRelationship_WithADestinationOfThis() { assertEquals("Relationship", r.getTags()); } + @Test + void test_parse_AddsTheRelationshipToAllSoftwareSystemInstancesInTheDeploymentEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + DeploymentNode devDeploymentNode = model.addDeploymentNode("dev", "Deployment Node", "Description", "Technology"); + devDeploymentNode.addInfrastructureNode("Infrastructure Node"); + devDeploymentNode.add(softwareSystem); + devDeploymentNode.add(softwareSystem); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode liveInfrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance liveSoftwareSystemInstance1 = liveDeploymentNode.add(softwareSystem); + SoftwareSystemInstance liveSoftwareSystemInstance2 = liveDeploymentNode.add(softwareSystem); + + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("live"); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwareSystem", softwareSystem); + elements.register("liveInfrastructureNode", liveInfrastructureNode); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + Set relationships = parser.parse(context, tokens("liveInfrastructureNode", "->", "softwareSystem"), archetype); + + assertEquals(2, relationships.size()); + assertEquals(2, model.getRelationships().size()); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveSoftwareSystemInstance1)); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveSoftwareSystemInstance2)); + } + + @Test + void test_parse_AddsTheRelationshipFromAllSoftwareSystemInstancesInTheDeploymentEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + DeploymentNode devDeploymentNode = model.addDeploymentNode("dev", "Deployment Node", "Description", "Technology"); + devDeploymentNode.addInfrastructureNode("Infrastructure Node"); + devDeploymentNode.add(softwareSystem); + devDeploymentNode.add(softwareSystem); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode liveInfrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance liveSoftwareSystemInstance1 = liveDeploymentNode.add(softwareSystem); + SoftwareSystemInstance liveSoftwareSystemInstance2 = liveDeploymentNode.add(softwareSystem); + + DeploymentEnvironmentDslContext context = new DeploymentEnvironmentDslContext("live"); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwareSystem", softwareSystem); + elements.register("liveInfrastructureNode", liveInfrastructureNode); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + Set relationships = parser.parse(context, tokens("softwareSystem", "->", "liveInfrastructureNode"), archetype); + + assertEquals(2, relationships.size()); + assertEquals(2, model.getRelationships().size()); + assertTrue(liveSoftwareSystemInstance1.hasEfferentRelationshipWith(liveInfrastructureNode)); + assertTrue(liveSoftwareSystemInstance2.hasEfferentRelationshipWith(liveInfrastructureNode)); + } + + @Test + void test_parse_AddsTheRelationshipToAllContainerInstancesInTheDeploymentEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + + DeploymentNode devDeploymentNode = model.addDeploymentNode("dev", "Deployment Node", "Description", "Technology"); + devDeploymentNode.addInfrastructureNode("Infrastructure Node"); + devDeploymentNode.add(container); + devDeploymentNode.add(container); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode liveInfrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + ContainerInstance liveContainerInstance1 = liveDeploymentNode.add(container); + ContainerInstance liveContainerInstance2 = liveDeploymentNode.add(container); + + DeploymentNodeDslContext context = new DeploymentNodeDslContext(liveDeploymentNode); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + elements.register("liveInfrastructureNode", liveInfrastructureNode); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + Set relationships = parser.parse(context, tokens("liveInfrastructureNode", "->", "container"), archetype); + + assertEquals(2, relationships.size()); + assertEquals(2, model.getRelationships().size()); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveContainerInstance1)); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveContainerInstance2)); + } + + @Test + void test_parse_AddsTheRelationshipFromAllContainerInstancesInTheDeploymentEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + + DeploymentNode devDeploymentNode = model.addDeploymentNode("dev", "Deployment Node", "Description", "Technology"); + devDeploymentNode.addInfrastructureNode("Infrastructure Node"); + devDeploymentNode.add(container); + devDeploymentNode.add(container); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode liveInfrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + ContainerInstance liveContainerInstance1 = liveDeploymentNode.add(container); + ContainerInstance liveContainerInstance2 = liveDeploymentNode.add(container); + + DeploymentNodeDslContext context = new DeploymentNodeDslContext(liveDeploymentNode); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + elements.register("liveInfrastructureNode", liveInfrastructureNode); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + Set relationships = parser.parse(context, tokens("container", "->", "liveInfrastructureNode"), archetype); + + assertEquals(2, relationships.size()); + assertEquals(2, model.getRelationships().size()); + assertTrue(liveContainerInstance1.hasEfferentRelationshipWith(liveInfrastructureNode)); + assertTrue(liveContainerInstance2.hasEfferentRelationshipWith(liveInfrastructureNode)); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java index fe6b84a61..706454d33 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java @@ -3,6 +3,8 @@ import com.structurizr.model.*; import org.junit.jupiter.api.Test; +import java.util.Set; + import static org.junit.jupiter.api.Assertions.*; class ImplicitRelationshipParserTests extends AbstractTests { @@ -205,4 +207,67 @@ void test_parse_AddsTheRelationshipAndImplicitRelationshipsWithADescriptionAndTe assertEquals("", r.getTags()); } + @Test + void test_parse_AddsTheRelationshipToAllSoftwareSystemInstancesInTheDeploymentEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + + DeploymentNode devDeploymentNode = model.addDeploymentNode("dev", "Deployment Node", "Description", "Technology"); + devDeploymentNode.addInfrastructureNode("Infrastructure Node"); + devDeploymentNode.add(softwareSystem); + devDeploymentNode.add(softwareSystem); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode liveInfrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance liveSoftwareSystemInstance1 = liveDeploymentNode.add(softwareSystem); + SoftwareSystemInstance liveSoftwareSystemInstance2 = liveDeploymentNode.add(softwareSystem); + + InfrastructureNodeDslContext context = new InfrastructureNodeDslContext(liveInfrastructureNode); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("softwareSystem", softwareSystem); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + Set relationships = parser.parse(context, tokens("->", "softwareSystem"), archetype); + + assertEquals(2, relationships.size()); + assertEquals(2, model.getRelationships().size()); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveSoftwareSystemInstance1)); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveSoftwareSystemInstance2)); + } + + @Test + void test_parse_AddsTheRelationshipToAllContainerInstancesInTheDeploymentEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + + DeploymentNode devDeploymentNode = model.addDeploymentNode("dev", "Deployment Node", "Description", "Technology"); + devDeploymentNode.addInfrastructureNode("Infrastructure Node"); + devDeploymentNode.add(container); + devDeploymentNode.add(container); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode liveInfrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + ContainerInstance liveContainerInstance1 = liveDeploymentNode.add(container); + ContainerInstance liveContainerInstance2 = liveDeploymentNode.add(container); + + InfrastructureNodeDslContext context = new InfrastructureNodeDslContext(liveInfrastructureNode); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("container", container); + context.setIdentifierRegister(elements); + + assertEquals(0, model.getRelationships().size()); + + Set relationships = parser.parse(context, tokens("->", "container"), archetype); + + assertEquals(2, relationships.size()); + assertEquals(2, model.getRelationships().size()); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveContainerInstance1)); + assertTrue(liveInfrastructureNode.hasEfferentRelationshipWith(liveContainerInstance2)); + } + } \ No newline at end of file From d76d18c08daf2e87589c67502d51761468a72bac Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 20 Aug 2025 13:15:12 +0100 Subject: [PATCH 355/418] Fixes #435. --- changelog.md | 2 ++ .../structurizr/dsl/StructurizrDslParser.java | 2 +- .../test/java/com/structurizr/dsl/DslTests.java | 16 ++++++++++++++++ .../archetypes-for-implicit-relationships.dsl | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/archetypes-for-implicit-relationships.dsl diff --git a/changelog.md b/changelog.md index 6f3d8f8a2..ca8a70096 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,8 @@ - structurizr-dsl: Adds a `Shell` shape. - structurizr-dsl: Adds a `Terminal` shape. - structurizr-dsl: Adds an 'instanceOf' keyword (an alternative for `softwareSystemInstance` and `containerInstance`). +- structurizr-dsl: Relationships to/from software system/container instances can be now defined by using the software system/container identifier. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships). ## v4.1.0 (28th May 2025) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 322b25b67..2a686828c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -345,7 +345,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (tokens.size() >= 2 && isRelationshipKeywordOrArchetype(tokens.get(0)) && inContext(ElementDslContext.class)) { // implicit without archetype: -> this // implicit with archetype: --https-> this - Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); + Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(0)); Set relationships = new ImplicitRelationshipParser().parse(getContext(ElementDslContext.class), tokens.withoutContextStartToken(), archetype); if (relationships.size() == 1) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index a613b53c7..9bcc2beeb 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1563,6 +1563,22 @@ void test_archetypesForExtension() throws Exception { assertTrue(r.hasTag("HTTPS")); } + @Test + void test_archetypesForImplicitRelationships() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/archetypes-for-implicit-relationships.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + + SoftwareSystem a = workspace.getModel().getSoftwareSystemWithName("A"); + SoftwareSystem b = workspace.getModel().getSoftwareSystemWithName("B"); + + Relationship r = b.getEfferentRelationshipWith(a); + assertEquals("Makes API calls to", r.getDescription()); + assertEquals("HTTPS", r.getTechnology()); + assertTrue(r.hasTag("HTTPS")); + } + @Test void test_textBlock() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); diff --git a/structurizr-dsl/src/test/resources/dsl/archetypes-for-implicit-relationships.dsl b/structurizr-dsl/src/test/resources/dsl/archetypes-for-implicit-relationships.dsl new file mode 100644 index 000000000..8a642f3bd --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-implicit-relationships.dsl @@ -0,0 +1,15 @@ +workspace { + model { + archetypes { + https = -> { + technology "HTTPS" + tag "HTTPS" + } + } + + a = softwareSystem "A" + b = softwareSystem "B" { + --https-> a "Makes API calls to" + } + } +} \ No newline at end of file From c527e093d51c85044fccfd47536b52c9cb8f7be9 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 20 Aug 2025 17:43:50 +0100 Subject: [PATCH 356/418] Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. --- changelog.md | 1 + .../dsl/AbstractRelationshipParser.java | 4 +- .../dsl/DeploymentEnvironmentDslContext.java | 2 +- .../dsl/ExplicitRelationshipParser.java | 16 +- .../dsl/ImplicitRelationshipParser.java | 4 +- ...shipInDeploymentEnvironmentDslContext.java | 26 ++ .../structurizr/dsl/NoRelationshipParser.java | 98 ++++++ .../structurizr/dsl/StructurizrDslParser.java | 13 + .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../java/com/structurizr/dsl/DslTests.java | 49 +++ .../dsl/ExplicitRelationshipParserTests.java | 54 +++ .../dsl/NoRelationshipParserTests.java | 331 ++++++++++++++++++ .../test/resources/dsl/no-relationship.dsl | 88 +++++ 13 files changed, 678 insertions(+), 9 deletions(-) create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipInDeploymentEnvironmentDslContext.java create mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipParser.java create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/NoRelationshipParserTests.java create mode 100644 structurizr-dsl/src/test/resources/dsl/no-relationship.dsl diff --git a/changelog.md b/changelog.md index ca8a70096..1eadaa858 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ - structurizr-dsl: Adds an 'instanceOf' keyword (an alternative for `softwareSystemInstance` and `containerInstance`). - structurizr-dsl: Relationships to/from software system/container instances can be now defined by using the software system/container identifier. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships). +- structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. ## v4.1.0 (28th May 2025) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java index c9b5adfdc..556ac6a7e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java @@ -38,11 +38,11 @@ protected Relationship createRelationship(Element sourceElement, String descript return relationship; } - protected Set findSoftwareSystemInstances(SoftwareSystem softwareSystem, String deploymentEnvironment) { + protected Set findSoftwareSystemInstances(SoftwareSystem softwareSystem, String deploymentEnvironment) { return softwareSystem.getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).map(e -> (SoftwareSystemInstance) e).filter(ssi -> ssi.getSoftwareSystem().equals(softwareSystem) && ssi.getEnvironment().equals(deploymentEnvironment)).collect(Collectors.toSet()); } - protected Set findContainerInstances(Container container, String deploymentEnvironment) { + protected Set findContainerInstances(Container container, String deploymentEnvironment) { return container.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance) e).filter(ci -> ci.getContainer().equals(container) && ci.getEnvironment().equals(deploymentEnvironment)).collect(Collectors.toSet()); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java index 4cc53b0fc..ff54ccfe8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java @@ -1,6 +1,6 @@ package com.structurizr.dsl; -final class DeploymentEnvironmentDslContext extends DslContext implements GroupableDslContext { +class DeploymentEnvironmentDslContext extends DslContext implements GroupableDslContext { private final DeploymentEnvironment environment; private final ElementGroup group; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java index affc90fb1..2158d596d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -43,11 +43,19 @@ Set parse(DslContext context, Tokens tokens, Archetype archetype) String description = archetype.getDescription(); if (tokens.includes(DESCRIPTION_INDEX)) { description = tokens.get(DESCRIPTION_INDEX); + } else { + if (context instanceof NoRelationshipInDeploymentEnvironmentDslContext) { + description = ((NoRelationshipInDeploymentEnvironmentDslContext)context).getRelationship().getDescription(); + } } String technology = archetype.getTechnology(); if (tokens.includes(TECHNOLOGY_INDEX)) { technology = tokens.get(TECHNOLOGY_INDEX); + } else { + if (context instanceof NoRelationshipInDeploymentEnvironmentDslContext) { + technology = ((NoRelationshipInDeploymentEnvironmentDslContext)context).getRelationship().getTechnology(); + } } List tags = new ArrayList<>(archetype.getTags()); @@ -68,20 +76,20 @@ Set parse(DslContext context, Tokens tokens, Archetype archetype) if (sourceElement instanceof SoftwareSystem) { // find the software system instances in the deployment environment - sourceElements = findSoftwareSystemInstances((SoftwareSystem)sourceElement, deploymentEnvironment); + sourceElements.addAll(findSoftwareSystemInstances((SoftwareSystem)sourceElement, deploymentEnvironment)); } else if (sourceElement instanceof Container) { // find the container instances in the deployment environment - sourceElements = findContainerInstances((Container)sourceElement, deploymentEnvironment); + sourceElements.addAll(findContainerInstances((Container)sourceElement, deploymentEnvironment)); } else { sourceElements.add(sourceElement); } if (destinationElement instanceof SoftwareSystem) { // find the software system instances in the deployment environment - destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment); + destinationElements.addAll(findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment)); } else if (destinationElement instanceof Container) { // find the container instances in the deployment environment - destinationElements = findContainerInstances((Container)destinationElement, deploymentEnvironment); + destinationElements.addAll(findContainerInstances((Container)destinationElement, deploymentEnvironment)); } else { destinationElements.add(destinationElement); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java index e97e8cb8d..6d761096a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -63,10 +63,10 @@ Set parse(ElementDslContext context, Tokens tokens, Archetype arch if (destinationElement instanceof SoftwareSystem) { // find the software system instances in the deployment environment - destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment); + destinationElements.addAll(findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment)); } else if (destinationElement instanceof Container) { // find the container instances in the deployment environment - destinationElements = findContainerInstances((Container)destinationElement, deploymentEnvironment); + destinationElements.addAll(findContainerInstances((Container)destinationElement, deploymentEnvironment)); } else { destinationElements.add(destinationElement); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipInDeploymentEnvironmentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipInDeploymentEnvironmentDslContext.java new file mode 100644 index 000000000..c694fe977 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipInDeploymentEnvironmentDslContext.java @@ -0,0 +1,26 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Relationship; + +final class NoRelationshipInDeploymentEnvironmentDslContext extends DeploymentEnvironmentDslContext { + + private final Relationship relationship; + + NoRelationshipInDeploymentEnvironmentDslContext(DeploymentEnvironmentDslContext parent, Relationship relationship) { + super(parent.getEnvironment().getName(), parent.getGroup()); + + this.relationship = relationship; + } + + Relationship getRelationship() { + return relationship; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipParser.java new file mode 100644 index 000000000..989e4ca93 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/NoRelationshipParser.java @@ -0,0 +1,98 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +import java.util.*; + +final class NoRelationshipParser extends AbstractRelationshipParser { + + private static final String GRAMMAR = " -/> [description]"; + + private static final int SOURCE_IDENTIFIER_INDEX = 0; + private static final int DESTINATION_IDENTIFIER_INDEX = 2; + private static final int DESCRIPTION_IDENTIFIER_INDEX = 3; + + Set parse(DeploymentEnvironmentDslContext context, Tokens tokens) { + // -/> [description] + + Set relationships = new HashSet<>(); + + if (tokens.hasMoreThan(DESCRIPTION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Not enough tokens, expected: " + GRAMMAR); + } + + String sourceId = tokens.get(SOURCE_IDENTIFIER_INDEX); + Element sourceElement = context.getElement(sourceId); + Set sourceElements = new HashSet<>(); + + if (sourceElement == null) { + throw new RuntimeException("The source element \"" + sourceId + "\" does not exist"); + } else if (sourceElement instanceof SoftwareSystem) { + sourceElements = findSoftwareSystemInstances((SoftwareSystem)sourceElement, context.getEnvironment().getName()); + } else if (sourceElement instanceof Container) { + sourceElements = findContainerInstances((Container)sourceElement, context.getEnvironment().getName()); + } else if (sourceElement instanceof StaticStructureElementInstance) { + sourceElements.add((StaticStructureElementInstance)sourceElement); + } else { + throw new RuntimeException("The source element \"" + sourceId + "\" is not valid - expecting a software system, software system instance, container, or container instance"); + } + + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + Element destinationElement = context.getElement(destinationId); + Set destinationElements = new HashSet<>(); + + if (destinationElement == null) { + throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); + } else if (destinationElement instanceof SoftwareSystem) { + destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, context.getEnvironment().getName()); + } else if (destinationElement instanceof Container) { + destinationElements = findContainerInstances((Container)destinationElement, context.getEnvironment().getName()); + } else if (destinationElement instanceof StaticStructureElementInstance) { + destinationElements.add((StaticStructureElementInstance)destinationElement); + } else { + throw new RuntimeException("The destination element \"" + destinationId + "\" is not valid - expecting a software system, software system instance, container, or container instance"); + } + + String description = null; + + if (tokens.includes(DESCRIPTION_IDENTIFIER_INDEX)) { + description = tokens.get(DESCRIPTION_IDENTIFIER_INDEX); + } + + int count = 0; + for (Element se : sourceElements) { + for (Element de : destinationElements) { + Relationship relationship; + + do { + if (description != null) { + relationship = se.getEfferentRelationshipWith(de, description); + } else { + relationship = se.getEfferentRelationshipWith(de); + } + + if (relationship != null && relationship.getLinkedRelationshipId() != null) { + context.getWorkspace().remove(relationship); + relationships.add(relationship); + count++; + } + } while (relationship != null); + } + } + + if (count == 0) { + if (description != null) { + throw new RuntimeException("A relationship between \"" + sourceId + "\" and \"" + destinationId + "\" with description \"" + description + "\" does not exist"); + } else { + throw new RuntimeException("A relationship between \"" + sourceId + "\" and \"" + destinationId + "\" does not exist"); + } + } + + return relationships; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 2a686828c..b2ce23e56 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -323,6 +323,19 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (inContext(ExternalScriptDslContext.class)) { new ScriptParser().parseParameter(getContext(ExternalScriptDslContext.class), tokens); + } else if (tokens.size() >= 4 && tokens.get(1).equals(NO_RELATIONSHIP_TOKEN) && shouldStartContext(tokens) && inContext(DeploymentEnvironmentDslContext.class)) { + // source -/> destination { + // or + // source -/> destination "description" { + + // remove source -> destination (between instances) in the deployment model + Set relationships = new NoRelationshipParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens.withoutContextStartToken()); + + // find the static element -> static element relationship that the removed relationships were based upon + Relationship relationship = workspace.getModel().getRelationship(relationships.iterator().next().getLinkedRelationshipId()); + + startContext(new NoRelationshipInDeploymentEnvironmentDslContext(getContext(DeploymentEnvironmentDslContext.class), relationship)); + } else if (tokens.size() > 2 && isRelationshipKeywordOrArchetype(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) { // explicit without archetype: a -> b // explicit with archetype: a --https-> b diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 59f33b275..a35b8828f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -11,6 +11,7 @@ class StructurizrDslTokens { static final String PERSON_TOKEN = "person"; static final String SOFTWARE_SYSTEM_TOKEN = "softwareSystem"; static final String RELATIONSHIP_TOKEN = "->"; + static final String NO_RELATIONSHIP_TOKEN = "-/>"; static final String CONTAINER_TOKEN = "container"; static final String COMPONENT_TOKEN = "component"; static final String GROUP_TOKEN = "group"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 9bcc2beeb..b0c1b64f8 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1753,4 +1753,53 @@ void test_colorSchemes() throws Exception { assertEquals("#ffffff", relationshipStyle.getColor()); } + @Test + void test_noRelationship() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/no-relationship.dsl")); + + Workspace workspace = parser.getWorkspace(); + + Container ui = (Container)parser.getIdentifiersRegister().getElement("ss.ui"); + Container backend = (Container)parser.getIdentifiersRegister().getElement("ss.backend"); + + // environment One: ui -> backend + ContainerInstance uiInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("One") && ci.getContainer().equals(ui)).findFirst().get(); + ContainerInstance backendInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("One") && ci.getContainer().equals(backend)).findFirst().get(); + + assertNotNull(uiInstance); + assertNotNull(backendInstance); + assertTrue(uiInstance.hasEfferentRelationshipWith(backendInstance)); + + // environment Two: ui -> load balancer -> backend + uiInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Two") && ci.getContainer().equals(ui)).findFirst().get(); + backendInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Two") && ci.getContainer().equals(backend)).findFirst().get(); + InfrastructureNode loadBalancer = workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).map(e -> (InfrastructureNode)e).filter(ci -> ci.getEnvironment().equals("Two")).findFirst().get(); + + assertNotNull(uiInstance); + assertNotNull(backendInstance); + assertFalse(uiInstance.hasEfferentRelationshipWith(backendInstance)); + + assertEquals("Makes API requests to", uiInstance.getEfferentRelationshipWith(loadBalancer).getDescription()); + assertEquals("JSON/HTTPS", uiInstance.getEfferentRelationshipWith(loadBalancer).getTechnology()); + + assertEquals("Forwards API requests to", loadBalancer.getEfferentRelationshipWith(backendInstance).getDescription()); + assertEquals("", loadBalancer.getEfferentRelationshipWith(backendInstance).getTechnology()); + + // environment Three: ui -> load balancer -> backend + uiInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Three") && ci.getContainer().equals(ui)).findFirst().get(); + backendInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Three") && ci.getContainer().equals(backend)).findFirst().get(); + loadBalancer = workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).map(e -> (InfrastructureNode)e).filter(ci -> ci.getEnvironment().equals("Three")).findFirst().get(); + + assertNotNull(uiInstance); + assertNotNull(backendInstance); + assertFalse(uiInstance.hasEfferentRelationshipWith(backendInstance)); + + assertEquals("Makes API requests to", uiInstance.getEfferentRelationshipWith(loadBalancer).getDescription()); + assertEquals("JSON/HTTPS", uiInstance.getEfferentRelationshipWith(loadBalancer).getTechnology()); + + assertEquals("Forwards API requests to", loadBalancer.getEfferentRelationshipWith(backendInstance).getDescription()); + assertEquals("", loadBalancer.getEfferentRelationshipWith(backendInstance).getTechnology()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java index a1f540ea4..ac4b7cead 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java @@ -391,4 +391,58 @@ void test_parse_AddsTheRelationshipFromAllContainerInstancesInTheDeploymentEnvir assertTrue(liveContainerInstance2.hasEfferentRelationshipWith(liveInfrastructureNode)); } + @Test + void test_parse_AddsAViaRelationshipUsingTheDescriptionAndTechnologyOfTheRemovedRelationship() { + SoftwareSystem ss = model.addSoftwareSystem("SS"); + Container a = ss.addContainer("A"); + Container b = ss.addContainer("B"); + Relationship relationship = a.uses(b, "Makes API calls using", "JSON/HTTPS"); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + ContainerInstance aInstance = liveDeploymentNode.add(a); + ContainerInstance bInstance = liveDeploymentNode.add(b); + + NoRelationshipInDeploymentEnvironmentDslContext context = new NoRelationshipInDeploymentEnvironmentDslContext(new DeploymentEnvironmentDslContext("live"), relationship); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", aInstance); + elements.register("infrastructureNode", infrastructureNode); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("a", "->", "infrastructureNode"), archetype); + + Relationship aInstanceToInfrastructureNode = aInstance.getEfferentRelationshipWith(infrastructureNode); + assertEquals("Makes API calls using", aInstanceToInfrastructureNode.getDescription()); + assertEquals("JSON/HTTPS", aInstanceToInfrastructureNode.getTechnology()); + } + + @Test + void test_parse_AddsAViaRelationshipUOverridingTheDescriptionAndTechnologyOfTheRemovedRelationship() { + SoftwareSystem ss = model.addSoftwareSystem("SS"); + Container a = ss.addContainer("A"); + Container b = ss.addContainer("B"); + Relationship relationship = a.uses(b, "Makes API calls using", "JSON/HTTPS"); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("live", "Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = liveDeploymentNode.addInfrastructureNode("Infrastructure Node"); + ContainerInstance aInstance = liveDeploymentNode.add(a); + ContainerInstance bInstance = liveDeploymentNode.add(b); + + NoRelationshipInDeploymentEnvironmentDslContext context = new NoRelationshipInDeploymentEnvironmentDslContext(new DeploymentEnvironmentDslContext("live"), relationship); + context.setWorkspace(workspace); + + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", aInstance); + elements.register("infrastructureNode", infrastructureNode); + context.setIdentifierRegister(elements); + + parser.parse(context, tokens("a", "->", "infrastructureNode", "New description", "New technology"), archetype); + + Relationship aInstanceToInfrastructureNode = aInstance.getEfferentRelationshipWith(infrastructureNode); + assertEquals("New description", aInstanceToInfrastructureNode.getDescription()); + assertEquals("New technology", aInstanceToInfrastructureNode.getTechnology()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/NoRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/NoRelationshipParserTests.java new file mode 100644 index 000000000..58fcb32fd --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/NoRelationshipParserTests.java @@ -0,0 +1,331 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class NoRelationshipParserTests extends AbstractTests { + + private static final String DEPLOYMENT_ENVIRONMENT = "live"; + + private final NoRelationshipParser parser = new NoRelationshipParser(); + private DeploymentEnvironmentDslContext context; + + @BeforeEach + void setUp() { + context = new DeploymentEnvironmentDslContext(DEPLOYMENT_ENVIRONMENT); + context.setWorkspace(workspace); + } + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(null, tokens("source", "-/>", "destination", "description", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: -/> [description]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { + try { + parser.parse(null, tokens("source", "-/>")); + fail(); + } catch (Exception e) { + assertEquals("Not enough tokens, expected: -/> [description]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheSourceElementIsNotDefined() { + try { + parser.parse(context, tokens("a", "-/>", "b")); + fail(); + } catch (Exception e) { + assertEquals("The source element \"a\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheSourceElementIsNotAStaticStructureElementInstance() { + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", model.addPerson("User")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("a", "->", "b")); + fail(); + } catch (Exception e) { + assertEquals("The source element \"a\" is not valid - expecting a software system, software system instance, container, or container instance", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", model.addSoftwareSystem("A")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("a", "-/>", "b")); + fail(); + } catch (Exception e) { + assertEquals("The destination element \"b\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotAStaticStructureElementInstance() { + IdentifiersRegister elements = new IdentifiersRegister(); + elements.register("a", model.addSoftwareSystem("A")); + elements.register("b", model.addPerson("User")); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("a", "->", "b")); + fail(); + } catch (Exception e) { + assertEquals("The destination element \"b\" is not valid - expecting a software system, software system instance, container, or container instance", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenARelationshipDoesNotExist() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + deploymentNode.add(a); + deploymentNode.add(b); + + elements.register("a", a); + elements.register("b", b); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("a", "-/>", "b")); + fail(); + } catch (Exception e) { + assertEquals("A relationship between \"a\" and \"b\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenARelationshipWithDescriptionDoesNotExist() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + deploymentNode.add(a); + deploymentNode.add(b); + + elements.register("a", a); + elements.register("b", b); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("a", "-/>", "b", "Description")); + fail(); + } catch (Exception e) { + assertEquals("A relationship between \"a\" and \"b\" with description \"Description\" does not exist", e.getMessage()); + } + } + + @Test + void test_parse_RemovesAllRelationshipsBetweenSoftwareSystemInstances_WhenUsingSoftwareSystemInstanceIdentifiersAndNoDescription() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + a.uses(b, "Description 1", "Technology"); + a.uses(b, "Description 2", "Technology"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + SoftwareSystemInstance aInstance = deploymentNode.add(a); + SoftwareSystemInstance bInstance = deploymentNode.add(b); + Relationship relationship1 = aInstance.getEfferentRelationshipWith(bInstance, "Description 1"); + Relationship relationship2 = aInstance.getEfferentRelationshipWith(bInstance, "Description 2"); + + elements.register("aInstance", aInstance); + elements.register("bInstance", bInstance); + context.setIdentifierRegister(elements); + + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + + Set relationshipsRemoved = parser.parse(context, tokens("aInstance", "-/>", "bInstance")); + + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + assertEquals(2, relationshipsRemoved.size()); + assertTrue(relationshipsRemoved.contains(relationship1)); + assertTrue(relationshipsRemoved.contains(relationship2)); + } + + @Test + void test_parse_RemovesAllRelationshipsBetweenSoftwareSystemInstances_WhenUsingSoftwareSystemIdentifiersAndNoDescription() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + a.uses(b, "Description 1", "Technology"); + a.uses(b, "Description 2", "Technology"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + SoftwareSystemInstance aInstance = deploymentNode.add(a); + SoftwareSystemInstance bInstance = deploymentNode.add(b); + Relationship relationship1 = aInstance.getEfferentRelationshipWith(bInstance, "Description 1"); + Relationship relationship2 = aInstance.getEfferentRelationshipWith(bInstance, "Description 2"); + + elements.register("a", a); + elements.register("b", b); + context.setIdentifierRegister(elements); + + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + + Set relationshipsRemoved = parser.parse(context, tokens("a", "-/>", "b")); + + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + assertEquals(2, relationshipsRemoved.size()); + assertTrue(relationshipsRemoved.contains(relationship1)); + assertTrue(relationshipsRemoved.contains(relationship2)); + } + + @Test + void test_parse_RemovesAllRelationshipsBetweenContainerInstances_WhenUsingContainerIdentifiersAndNoDescription() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem ss = model.addSoftwareSystem("A"); + Container a = ss.addContainer("A"); + Container b = ss.addContainer("B"); + a.uses(b, "Description 1", "Technology"); + a.uses(b, "Description 2", "Technology"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + ContainerInstance aInstance = deploymentNode.add(a); + ContainerInstance bInstance = deploymentNode.add(b); + Relationship relationship1 = aInstance.getEfferentRelationshipWith(bInstance, "Description 1"); + Relationship relationship2 = aInstance.getEfferentRelationshipWith(bInstance, "Description 2"); + + elements.register("a", a); + elements.register("b", b); + context.setIdentifierRegister(elements); + + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + + Set relationshipsRemoved = parser.parse(context, tokens("a", "-/>", "b")); + + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + assertEquals(2, relationshipsRemoved.size()); + assertTrue(relationshipsRemoved.contains(relationship1)); + assertTrue(relationshipsRemoved.contains(relationship2)); + } + + @Test + void test_parse_RemovesTheRelationshipBetweenSoftwareSystemInstances_WhenUsingSoftwareSystemInstanceIdentifiersAndADescription() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + a.uses(b, "Description 1", "Technology"); + a.uses(b, "Description 2", "Technology"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + SoftwareSystemInstance aInstance = deploymentNode.add(a); + SoftwareSystemInstance bInstance = deploymentNode.add(b); + Relationship relationship1 = aInstance.getEfferentRelationshipWith(bInstance, "Description 1"); + Relationship relationship2 = aInstance.getEfferentRelationshipWith(bInstance, "Description 2"); + + elements.register("aInstance", aInstance); + elements.register("bInstance", bInstance); + context.setIdentifierRegister(elements); + + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + + Set relationshipsRemoved = parser.parse(context, tokens("aInstance", "-/>", "bInstance", "Description 1")); + + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + assertEquals(1, relationshipsRemoved.size()); + assertTrue(relationshipsRemoved.contains(relationship1)); + assertFalse(relationshipsRemoved.contains(relationship2)); + } + + @Test + void test_parse_RemovesTheRelationshipBetweenSoftwareSystemInstances_WhenUsingSoftwareSystemIdentifiersAndADescription() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + a.uses(b, "Description 1", "Technology"); + a.uses(b, "Description 2", "Technology"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + SoftwareSystemInstance aInstance = deploymentNode.add(a); + SoftwareSystemInstance bInstance = deploymentNode.add(b); + Relationship relationship1 = aInstance.getEfferentRelationshipWith(bInstance, "Description 1"); + Relationship relationship2 = aInstance.getEfferentRelationshipWith(bInstance, "Description 2"); + + elements.register("a", a); + elements.register("b", b); + context.setIdentifierRegister(elements); + + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + + Set relationshipsRemoved = parser.parse(context, tokens("a", "-/>", "b", "Description 1")); + + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + assertEquals(1, relationshipsRemoved.size()); + assertTrue(relationshipsRemoved.contains(relationship1)); + assertFalse(relationshipsRemoved.contains(relationship2)); + } + + @Test + void test_parse_RemovesTheRelationshipBetweenContainerInstances_WhenUsingContainerIdentifiersAndADescription() { + IdentifiersRegister elements = new IdentifiersRegister(); + + SoftwareSystem ss = model.addSoftwareSystem("A"); + Container a = ss.addContainer("A"); + Container b = ss.addContainer("B"); + a.uses(b, "Description 1", "Technology"); + a.uses(b, "Description 2", "Technology"); + + DeploymentNode deploymentNode = model.addDeploymentNode(DEPLOYMENT_ENVIRONMENT, "Deployment Node", "Description", "Technology"); + ContainerInstance aInstance = deploymentNode.add(a); + ContainerInstance bInstance = deploymentNode.add(b); + Relationship relationship1 = aInstance.getEfferentRelationshipWith(bInstance, "Description 1"); + Relationship relationship2 = aInstance.getEfferentRelationshipWith(bInstance, "Description 2"); + + elements.register("a", a); + elements.register("b", b); + context.setIdentifierRegister(elements); + + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + + Set relationshipsRemoved = parser.parse(context, tokens("a", "-/>", "b", "Description 1")); + + assertFalse(aInstance.hasEfferentRelationshipWith(bInstance, "Description 1")); + assertTrue(aInstance.hasEfferentRelationshipWith(bInstance, "Description 2")); + assertEquals(1, relationshipsRemoved.size()); + assertTrue(relationshipsRemoved.contains(relationship1)); + assertFalse(relationshipsRemoved.contains(relationship2)); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/no-relationship.dsl b/structurizr-dsl/src/test/resources/dsl/no-relationship.dsl new file mode 100644 index 000000000..fdf96ed8a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/no-relationship.dsl @@ -0,0 +1,88 @@ +workspace { + + !identifiers hierarchical + + model { + ss = softwareSystem "Software System" { + ui = container "UI" "Description" "JavaScript and React" + backend = container "Backend" "Description" "Spring Boot" + + ui -> backend "Makes API requests to" "JSON/HTTPS" + } + + // ui -> backend + one = deploymentEnvironment "One" { + deploymentNode "Developer's Computer" { + deploymentNode "Web Browser" { + instanceOf ss.ui + } + instanceOf ss.backend + } + } + + // ui -> loadbalancer -> backend + // configured via container identifiers + two = deploymentEnvironment "Two" { + deploymentNode "User's Computer" { + deploymentNode "Web Browser" { + instanceOf ss.ui + } + } + dc = deploymentNode "Data Center" { + loadBalancer = infrastructureNode "Load Balancer" + deploymentNode "Server" { + instanceOf ss.backend + } + } + + ss.ui -/> ss.backend { + ss.ui -> dc.loadBalancer + dc.loadBalancer -> ss.backend "Forwards API requests to" "" + } + } + + // ui -> loadbalancer -> backend + // configured via container instance identifiers + three = deploymentEnvironment "Three" { + computer = deploymentNode "User's Computer" { + webbrowser = deploymentNode "Web Browser" { + ui = instanceOf ss.ui + } + } + datacenter = deploymentNode "Data Center" { + loadbalancer = infrastructureNode "Load Balancer" + server = deploymentNode "Server" { + backend = instanceOf ss.backend + } + } + + computer.webbrowser.ui -/> datacenter.server.backend { + computer.webbrowser.ui -> datacenter.loadbalancer + datacenter.loadbalancer -> datacenter.server.backend "Forwards API requests to" "" + } + } + } + + views { + container ss { + include * + autolayout lr + } + + deployment ss one { + include * + autolayout lr + } + + deployment ss two { + include * + autolayout lr + } + + deployment ss three { + include * + autolayout lr + } + } + +} \ No newline at end of file From 324bb7f6ddcf469aa570b0d30901731ea486cc82 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 26 Aug 2025 15:07:24 +0100 Subject: [PATCH 357/418] Fixes #437. --- changelog.md | 1 + .../main/java/com/structurizr/component/ComponentFinder.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 1eadaa858..f2741e277 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## v4.2.0 (unreleased) +- structurizr-java: Fixes https://github.com/structurizr/java/issues/437 (Make ComponentFinder.run() not fail on empty Set). - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). - structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. - structurizr-dsl: Adds a `Bucket` shape. diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index c73e0228b..387b66711 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -61,7 +61,7 @@ public Set run() { for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { Set set = componentFinderStrategy.run(typeRepository); if (set.isEmpty()) { - throw new RuntimeException("No components were found by " + componentFinderStrategy); + log.debug("No components were found by " + componentFinderStrategy); } discoveredComponents.addAll(set); } From aa6c734a603b668ccec397b18b15ce3ed2ac18a5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 28 Aug 2025 08:25:17 +0100 Subject: [PATCH 358/418] Adds support for a `jump` property on relationship styles. --- changelog.md | 1 + .../structurizr/view/RelationshipStyle.java | 17 ++++++++ .../structurizr/view/RelationshipView.java | 22 ++++++++++ .../dsl/RelationshipStyleParser.java | 23 +++++++++++ .../structurizr/dsl/StructurizrDslParser.java | 3 ++ .../structurizr/dsl/StructurizrDslTokens.java | 1 + .../dsl/RelationshipStyleParserTests.java | 41 ++++++++++++++++++- 7 files changed, 107 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index f2741e277..6f6955154 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ - structurizr-dsl: Relationships to/from software system/container instances can be now defined by using the software system/container identifier. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships). - structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. +- structurizr-dsl: Adds support for a `jump` property on relationship styles. ## v4.1.0 (28th May 2025) diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java index 3fbae753e..be966314e 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java @@ -39,6 +39,10 @@ public final class RelationshipStyle extends AbstractStyle { @JsonInclude(value = JsonInclude.Include.NON_NULL) private Routing routing; + /** whether the line should jump over others */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Boolean jump; + /** the position of the annotation along the line; 0 (start) to 100 (end) */ @JsonInclude(value = JsonInclude.Include.NON_NULL) private Integer position; @@ -142,6 +146,19 @@ public RelationshipStyle routing(Routing routing) { return this; } + public Boolean getJump() { + return jump; + } + + public void setJump(Boolean jump) { + this.jump = jump; + } + + public RelationshipStyle jump(boolean jump) { + setJump(jump); + return this; + } + public Integer getFontSize() { return fontSize; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java index 9bee72b15..b9305de3e 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java @@ -29,6 +29,9 @@ public final class RelationshipView implements PropertyHolder, Comparable + RelationshipStyle style = context.getStyle(); + + if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { + throw new RuntimeException("Too many tokens, expected: jump "); + } + + if (tokens.includes(FIRST_PROPERTY_INDEX)) { + String jump = tokens.get(1); + + if ("true".equalsIgnoreCase(jump)) { + style.setJump(true); + } else if ("false".equalsIgnoreCase(jump)) { + style.setJump(false); + } else { + throw new RuntimeException("Jump must be true or false"); + } + } else { + throw new RuntimeException("Expected: jump "); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index b2ce23e56..58fcc3e08 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -901,6 +901,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (RELATIONSHIP_STYLE_ROUTING_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { new RelationshipStyleParser().parseRouting(getContext(RelationshipStyleDslContext.class), tokens); + } else if (RELATIONSHIP_STYLE_JUMP_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) { + new RelationshipStyleParser().parseJump(getContext(RelationshipStyleDslContext.class), tokens); + } else if (DEPLOYMENT_ENVIRONMENT_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { String environment = new DeploymentEnvironmentParser().parse(tokens.withoutContextStartToken()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index a35b8828f..9ddcd5958 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -90,6 +90,7 @@ class StructurizrDslTokens { static final String RELATIONSHIP_STYLE_DASHED_TOKEN = "dashed"; static final String RELATIONSHIP_STYLE_OPACITY_TOKEN = "opacity"; static final String RELATIONSHIP_STYLE_ROUTING_TOKEN = "routing"; + static final String RELATIONSHIP_STYLE_JUMP_TOKEN = "jump"; static final String RELATIONSHIP_STYLE_LINE_STYLE_TOKEN = "style"; static final String RELATIONSHIP_STYLE_FONT_SIZE_TOKEN = "fontSize"; static final String RELATIONSHIP_STYLE_WIDTH_TOKEN = "width"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java index 645d747b2..d59d7081d 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.view.Border; import com.structurizr.view.LineStyle; import com.structurizr.view.RelationshipStyle; import com.structurizr.view.Routing; @@ -396,4 +395,44 @@ void test_parseRouting_SetsTheRouting() { assertEquals(Routing.Curved, relationshipStyle.getRouting()); } + @Test + void test_parseJump_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseJump(relationshipStyleDslContext(), tokens("jump", "boolean", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: jump ", e.getMessage()); + } + } + + @Test + void test_parseJump_ThrowsAnException_WhenTheValueIsMissing() { + try { + parser.parseJump(relationshipStyleDslContext(), tokens("jump")); + fail(); + } catch (Exception e) { + assertEquals("Expected: jump ", e.getMessage()); + } + } + + @Test + void test_parseJump_ThrowsAnException_WhenTheValueIsNotValid() { + try { + parser.parseJump(relationshipStyleDslContext(), tokens("jump", "abc")); + fail(); + } catch (Exception e) { + assertEquals("Jump must be true or false", e.getMessage()); + } + } + + @Test + void test_parseJump_SetsTheJump() { + RelationshipStyleDslContext context = relationshipStyleDslContext(); + parser.parseJump(context, tokens("jump", "false")); + assertEquals(false, relationshipStyle.getJump()); + + parser.parseJump(context, tokens("jump", "true")); + assertEquals(true, relationshipStyle.getJump()); + } + } \ No newline at end of file From 768ea227d07fab8a92f57eed70b645356420bb0b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 30 Aug 2025 09:54:49 +0100 Subject: [PATCH 359/418] Inspections can be disabled via a workspace property. --- changelog.md | 1 + .../com/structurizr/inspection/DefaultInspector.java | 8 +++++--- .../inspection/DefaultInspectorTests.java | 12 ++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 6f6955154..5b0d7e937 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships). - structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. - structurizr-dsl: Adds support for a `jump` property on relationship styles. +- structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). ## v4.1.0 (28th May 2025) diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java index daa269cad..c038a2369 100644 --- a/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java @@ -15,9 +15,11 @@ public class DefaultInspector extends Inspector { public DefaultInspector(Workspace workspace) { super(workspace); - runWorkspaceInspections(); - runModelInspections(); - runViewInspections(); + if (!"false".equalsIgnoreCase(workspace.getProperties().get("structurizr.inspection"))) { + runWorkspaceInspections(); + runModelInspections(); + runViewInspections(); + } } private void runWorkspaceInspections() { diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java index 457e66325..562a9affd 100644 --- a/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java @@ -36,6 +36,18 @@ void test_EmptyWorkspace() { assertEquals("This workspace has no views.", violation.getMessage()); } + @Test + void test_EmptyWorkspace_WhenInspectionsAreDisabled() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.addProperty("structurizr.inspection", "false"); + + DefaultInspector inspector = new DefaultInspector(workspace); + List violations = inspector.getViolations(); + + assertEquals(0, inspector.getNumberOfInspections()); + assertEquals(0, violations.size()); + } + @Test void test() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); From 3710689b7a31353e627c6817f534152fea810235 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 30 Aug 2025 09:56:07 +0100 Subject: [PATCH 360/418] Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. --- changelog.md | 1 + .../structurizr/inspection/DefaultInspector.java | 13 +++++++++++++ .../inspection/DefaultInspectorTests.java | 13 ++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 5b0d7e937..7e820fd14 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,7 @@ - structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. - structurizr-dsl: Adds support for a `jump` property on relationship styles. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). +- structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. ## v4.1.0 (28th May 2025) diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java index c038a2369..e691825ab 100644 --- a/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java @@ -10,8 +10,15 @@ import com.structurizr.model.*; import com.structurizr.view.*; +import java.util.List; + public class DefaultInspector extends Inspector { + private static final String INSPECTION_SUMMARY_NUMBER_OF_ERROR = "structurizr.inspection.error"; + private static final String INSPECTION_SUMMARY_NUMBER_OF_WARNING = "structurizr.inspection.warning"; + private static final String INSPECTION_SUMMARY_NUMBER_OF_INFO = "structurizr.inspection.info"; + private static final String INSPECTION_SUMMARY_NUMBER_OF_IGNORE = "structurizr.inspection.ignore"; + public DefaultInspector(Workspace workspace) { super(workspace); @@ -19,6 +26,12 @@ public DefaultInspector(Workspace workspace) { runWorkspaceInspections(); runModelInspections(); runViewInspections(); + + List violations = getViolations(); + workspace.addProperty(INSPECTION_SUMMARY_NUMBER_OF_ERROR, "" + violations.stream().filter(r -> r.getSeverity() == Severity.ERROR).count()); + workspace.addProperty(INSPECTION_SUMMARY_NUMBER_OF_WARNING, "" + violations.stream().filter(r -> r.getSeverity() == Severity.WARNING).count()); + workspace.addProperty(INSPECTION_SUMMARY_NUMBER_OF_INFO, "" + violations.stream().filter(r -> r.getSeverity() == Severity.INFO).count()); + workspace.addProperty(INSPECTION_SUMMARY_NUMBER_OF_IGNORE, "" + violations.stream().filter(r -> r.getSeverity() == Severity.IGNORE).count()); } } diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java index 562a9affd..619f12d22 100644 --- a/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java @@ -9,12 +9,14 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; public class DefaultInspectorTests { @Test void test_EmptyWorkspace() { - DefaultInspector inspector = new DefaultInspector(new Workspace("Name", "Description")); + Workspace workspace = new Workspace("Name", "Description"); + DefaultInspector inspector = new DefaultInspector(workspace); List violations = inspector.getViolations(); assertEquals(9, inspector.getNumberOfInspections()); @@ -34,6 +36,11 @@ void test_EmptyWorkspace() { assertEquals(Severity.ERROR, violation.getSeverity()); assertEquals("views.empty", violation.getType()); assertEquals("This workspace has no views.", violation.getMessage()); + + assertEquals("3", workspace.getProperties().get("structurizr.inspection.error")); + assertEquals("0", workspace.getProperties().get("structurizr.inspection.warning")); + assertEquals("0", workspace.getProperties().get("structurizr.inspection.info")); + assertEquals("0", workspace.getProperties().get("structurizr.inspection.ignore")); } @Test @@ -46,6 +53,10 @@ void test_EmptyWorkspace_WhenInspectionsAreDisabled() { assertEquals(0, inspector.getNumberOfInspections()); assertEquals(0, violations.size()); + assertNull(workspace.getProperties().get("structurizr.inspection.error")); + assertNull(workspace.getProperties().get("structurizr.inspection.warning")); + assertNull(workspace.getProperties().get("structurizr.inspection.info")); + assertNull(workspace.getProperties().get("structurizr.inspection.ignore")); } @Test From 3b737651be44b52ec6f8327aecf05a8f8d05bf3c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 3 Sep 2025 09:13:13 +0100 Subject: [PATCH 361/418] structurizr-inspection: Fixes `model.deploymentnode.technology` (it was checking the description property rather than technology). --- changelog.md | 1 + .../inspection/model/DeploymentNodeTechnologyInspection.java | 2 +- .../model/DeploymentNodeTechnologyInspectionTests.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 7e820fd14..2eabf5e2d 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,7 @@ - structurizr-dsl: Adds support for a `jump` property on relationship styles. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. +- structurizr-inspection: Fixes `model.deploymentnode.technology` (it was checking the description property rather than technology). ## v4.1.0 (28th May 2025) diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspection.java index 189293528..8919c90df 100644 --- a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspection.java +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspection.java @@ -13,7 +13,7 @@ public DeploymentNodeTechnologyInspection(Inspector inspector) { @Override protected Violation inspect(DeploymentNode deploymentNode) { - if (StringUtils.isNullOrEmpty(deploymentNode.getDescription())) { + if (StringUtils.isNullOrEmpty(deploymentNode.getTechnology())) { return violation("The " + terminologyFor(deploymentNode).toLowerCase() + " \"" + nameOf(deploymentNode) + "\" is missing a technology."); } diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspectionTests.java index 2b46c8f75..dc7844b8e 100644 --- a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspectionTests.java +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspectionTests.java @@ -17,7 +17,7 @@ @Test public void run_WithoutDescription() { Workspace workspace = new Workspace("Name", "Description"); - DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name", "Description", ""); Violation violation = new DeploymentNodeTechnologyInspection(new DefaultInspector(workspace)).run(deploymentNode); Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); From 30c61e5a19f1dabe639c9462a361704c2650450e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 3 Sep 2025 09:13:33 +0100 Subject: [PATCH 362/418] Adds `archetypes` to the set of permitted tokens. --- .../src/main/java/com/structurizr/dsl/ModelDslContext.java | 1 + 1 file changed, 1 insertion(+) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java index 795354332..56460da9f 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java @@ -24,6 +24,7 @@ public ElementGroup getGroup() { @Override protected String[] getPermittedTokens() { return new String[] { + StructurizrDslTokens.ARCHETYPES_TOKEN, StructurizrDslTokens.IDENTIFIERS_TOKEN, StructurizrDslTokens.GROUP_TOKEN, StructurizrDslTokens.PERSON_TOKEN, From 8f8a4597c6161aac83e23f1bc2a324ad2a41a34a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 4 Sep 2025 12:34:59 +0100 Subject: [PATCH 363/418] structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace. --- changelog.md | 1 + .../java/com/structurizr/util/ImageUtils.java | 33 ++++++++++++ structurizr-import/build.gradle | 15 +++++- .../diagrams/kroki/KrokiImporter.java | 18 +++++-- .../diagrams/mermaid/MermaidImporter.java | 16 +++++- .../diagrams/plantuml/PlantUMLImporter.java | 16 +++++- .../diagrams/kroki/KrokiImporterTests.java | 51 ++++++++++++++++--- .../mermaid/MermaidImporterTests.java | 39 ++++++++++++++ .../plantuml/PlantUMLImporterTests.java | 29 +++++++++++ 9 files changed, 204 insertions(+), 14 deletions(-) diff --git a/changelog.md b/changelog.md index 2eabf5e2d..43d0c76ea 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships). - structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. - structurizr-dsl: Adds support for a `jump` property on relationship styles. +- structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. - structurizr-inspection: Fixes `model.deploymentnode.technology` (it was checking the description property rather than technology). diff --git a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java index 21cc4f25a..bed31efc7 100644 --- a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java +++ b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java @@ -8,6 +8,9 @@ import java.io.IOException; import java.net.URL; import java.net.URLConnection; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.file.Files; import java.util.Base64; @@ -124,6 +127,36 @@ public static String getImageAsDataUri(File file) throws IOException { return DATA_URI_PREFIX + contentType + ";base64," + base64Content; } + public static String getSvgAsDataUri(@Nonnull URL url) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(url.toURI()) + .header("accept", CONTENT_TYPE_IMAGE_SVG) + .build(); + HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + String svg = response.body(); + + return DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_SVG + ";base64," + Base64.getEncoder().encodeToString(svg.getBytes()); + } + + public static String getPngAsDataUri(@Nonnull URL url) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(url.toURI()) + .header("accept", CONTENT_TYPE_IMAGE_PNG) + .build(); + HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + byte[] png = response.body(); + + return DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_PNG + ";base64," + Base64.getEncoder().encodeToString(png); + } + public static void validateImage(String imageDescriptor) { if (StringUtils.isNullOrEmpty(imageDescriptor)) { return; diff --git a/structurizr-import/build.gradle b/structurizr-import/build.gradle index 7b5c3b32f..d1f719fe1 100644 --- a/structurizr-import/build.gradle +++ b/structurizr-import/build.gradle @@ -4,4 +4,17 @@ dependencies { } -description = 'Utilities to import diagrams and documentation into a Structurizr workspace' \ No newline at end of file +description = 'Utilities to import diagrams and documentation into a Structurizr workspace' + +test { + useJUnitPlatform { + excludeTags "IntegrationTest" + } +} + +tasks.register("integrationTest", Test) { + useJUnitPlatform { + includeTags "IntegrationTest" + } + mustRunAfter check +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java index 6c01587e4..5277bd99b 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java @@ -1,17 +1,20 @@ package com.structurizr.importer.diagrams.kroki; import com.structurizr.importer.diagrams.AbstractDiagramImporter; +import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; import com.structurizr.view.ImageView; import java.io.File; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; public class KrokiImporter extends AbstractDiagramImporter { - private static final String KROKI_URL_PROPERTY = "kroki.url"; - private static final String KROKI_FORMAT_PROPERTY = "kroki.format"; + public static final String KROKI_URL_PROPERTY = "kroki.url"; + public static final String KROKI_FORMAT_PROPERTY = "kroki.format"; + public static final String KROKI_INLINE_PROPERTY = "kroki.inline"; public void importDiagram(ImageView view, String format, File file) throws Exception { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); @@ -38,7 +41,16 @@ public void importDiagram(ImageView view, String format, String content) throws String encodedDiagram = new KrokiEncoder().encode(content); String url = String.format("%s/%s/%s/%s", krokiServer, format, imageFormat, encodedDiagram); - view.setContent(url); + String inline = getViewOrViewSetProperty(view, KROKI_INLINE_PROPERTY); + if ("true".equals(inline)) { + if (imageFormat.equals(SVG_FORMAT)) { + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + } else { + view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + } + } else { + view.setContent(url); + } view.setContentType(CONTENT_TYPES_BY_FORMAT.get(imageFormat)); } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java index c7fa23b44..2be38290c 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java @@ -1,10 +1,12 @@ package com.structurizr.importer.diagrams.mermaid; import com.structurizr.importer.diagrams.AbstractDiagramImporter; +import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; import com.structurizr.view.ImageView; import java.io.File; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -13,6 +15,7 @@ public class MermaidImporter extends AbstractDiagramImporter { public static final String MERMAID_URL_PROPERTY = "mermaid.url"; public static final String MERMAID_FORMAT_PROPERTY = "mermaid.format"; public static final String MERMAID_COMPRESS_PROPERTY = "mermaid.compress"; + public static final String MERMAID_INLINE_PROPERTY = "mermaid.inline"; public void importDiagram(ImageView view, File file) throws Exception { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); @@ -21,7 +24,7 @@ public void importDiagram(ImageView view, File file) throws Exception { importDiagram(view, content); } - public void importDiagram(ImageView view, String content) { + public void importDiagram(ImageView view, String content) throws Exception { String mermaidServer = getViewOrViewSetProperty(view, MERMAID_URL_PROPERTY); if (StringUtils.isNullOrEmpty(mermaidServer)) { throw new IllegalArgumentException("Please define a view/viewset property named " + MERMAID_URL_PROPERTY + " to specify your Mermaid server"); @@ -49,7 +52,16 @@ public void importDiagram(ImageView view, String content) { url = String.format("%s/svg/%s", mermaidServer, encodedMermaid); } - view.setContent(url); + String inline = getViewOrViewSetProperty(view, MERMAID_INLINE_PROPERTY); + if ("true".equals(inline)) { + if (format.equals(SVG_FORMAT)) { + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + } else { + view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + } + } else { + view.setContent(url); + } view.setContentType(CONTENT_TYPES_BY_FORMAT.get(format)); } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java index 3ed7651e0..24176d02b 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -1,10 +1,13 @@ package com.structurizr.importer.diagrams.plantuml; import com.structurizr.importer.diagrams.AbstractDiagramImporter; +import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; +import com.structurizr.util.Url; import com.structurizr.view.ImageView; import java.io.File; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -12,6 +15,7 @@ public class PlantUMLImporter extends AbstractDiagramImporter { public static final String PLANTUML_URL_PROPERTY = "plantuml.url"; public static final String PLANTUML_FORMAT_PROPERTY = "plantuml.format"; + public static final String PLANTUML_INLINE_PROPERTY = "plantuml.inline"; private static final String TITLE_STRING = "title "; private static final String NEWLINE = "\n"; @@ -39,7 +43,17 @@ public void importDiagram(ImageView view, String content) throws Exception { String encodedPlantUML = new PlantUMLEncoder().encode(content); String url = String.format("%s/%s/%s", plantUMLServer, format, encodedPlantUML); - view.setContent(url); + + String inline = getViewOrViewSetProperty(view, PLANTUML_INLINE_PROPERTY); + if ("true".equals(inline)) { + if (format.equals(SVG_FORMAT)) { + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + } else { + view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + } + } else { + view.setContent(url); + } view.setContentType(CONTENT_TYPES_BY_FORMAT.get(format)); String[] lines = content.split(NEWLINE); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java index cd3b0c6a8..cfcb4d461 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java @@ -2,6 +2,7 @@ import com.structurizr.Workspace; import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -13,7 +14,7 @@ public class KrokiImporterTests { @Test public void importDiagram() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("kroki.url", "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); ImageView view = workspace.getViews().createImageView("key"); new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); @@ -28,8 +29,8 @@ public void importDiagram() throws Exception { @Test public void importDiagram_AsPNG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("kroki.url", "https://kroki.io"); - workspace.getViews().getConfiguration().addProperty("kroki.format", "png"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_FORMAT_PROPERTY, "png"); ImageView view = workspace.getViews().createImageView("key"); new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); @@ -41,11 +42,29 @@ public void importDiagram_AsPNG() throws Exception { assertEquals("image/png", view.getContentType()); } + @Test + @Tag("IntegrationTest") + public void importDiagram_AsInlinePNG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_FORMAT_PROPERTY, "png"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_INLINE_PROPERTY, "true"); + ImageView view = workspace.getViews().createImageView("key"); + + new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("diagram.dot", view.getTitle()); + assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + @Test public void importDiagram_AsSVG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("kroki.url", "https://kroki.io"); - workspace.getViews().getConfiguration().addProperty("kroki.format", "svg"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_FORMAT_PROPERTY, "svg"); ImageView view = workspace.getViews().createImageView("key"); new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); @@ -57,6 +76,24 @@ public void importDiagram_AsSVG() throws Exception { assertEquals("image/svg+xml", view.getContentType()); } + @Test + @Tag("IntegrationTest") + public void importDiagram_AsInlineSVG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_FORMAT_PROPERTY, "svg"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_INLINE_PROPERTY, "true"); + ImageView view = workspace.getViews().createImageView("key"); + + new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("diagram.dot", view.getTitle()); + assertEquals("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIKICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8IS0tIEdlbmVyYXRlZCBieSBncmFwaHZpeiB2ZXJzaW9uIDkuMC4wICgyMDIzMDkxMS4xODI3KQogLS0+CjwhLS0gVGl0bGU6IEcgUGFnZXM6IDEgLS0+Cjxzdmcgd2lkdGg9Ijg5cHQiIGhlaWdodD0iMTE2cHQiCiB2aWV3Qm94PSIwLjAwIDAuMDAgODkuMzcgMTE2LjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGcgaWQ9ImdyYXBoMCIgY2xhc3M9ImdyYXBoIiB0cmFuc2Zvcm09InNjYWxlKDEgMSkgcm90YXRlKDApIHRyYW5zbGF0ZSg0IDExMikiPgo8dGl0bGU+RzwvdGl0bGU+Cjxwb2x5Z29uIGZpbGw9IndoaXRlIiBzdHJva2U9Im5vbmUiIHBvaW50cz0iLTQsNCAtNCwtMTEyIDg1LjM3LC0xMTIgODUuMzcsNCAtNCw0Ii8+CjwhLS0gSGVsbG8gLS0+CjxnIGlkPSJub2RlMSIgY2xhc3M9Im5vZGUiPgo8dGl0bGU+SGVsbG88L3RpdGxlPgo8ZWxsaXBzZSBmaWxsPSJub25lIiBzdHJva2U9ImJsYWNrIiBjeD0iNDAuNjkiIGN5PSItOTAiIHJ4PSIzNy41MyIgcnk9IjE4Ii8+Cjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjQwLjY5IiB5PSItODUuMzMiIGZvbnQtZmFtaWx5PSJUaW1lcyxzZXJpZiIgZm9udC1zaXplPSIxNC4wMCI+SGVsbG88L3RleHQ+CjwvZz4KPCEtLSBXb3JsZCAtLT4KPGcgaWQ9Im5vZGUyIiBjbGFzcz0ibm9kZSI+Cjx0aXRsZT5Xb3JsZDwvdGl0bGU+CjxlbGxpcHNlIGZpbGw9Im5vbmUiIHN0cm9rZT0iYmxhY2siIGN4PSI0MC42OSIgY3k9Ii0xOCIgcng9IjQwLjY5IiByeT0iMTgiLz4KPHRleHQgdGV4dC1hbmNob3I9Im1pZGRsZSIgeD0iNDAuNjkiIHk9Ii0xMy4zMiIgZm9udC1mYW1pbHk9IlRpbWVzLHNlcmlmIiBmb250LXNpemU9IjE0LjAwIj5Xb3JsZDwvdGV4dD4KPC9nPgo8IS0tIEhlbGxvJiM0NTsmZ3Q7V29ybGQgLS0+CjxnIGlkPSJlZGdlMSIgY2xhc3M9ImVkZ2UiPgo8dGl0bGU+SGVsbG8mIzQ1OyZndDtXb3JsZDwvdGl0bGU+CjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iYmxhY2siIGQ9Ik00MC42OSwtNzEuN0M0MC42OSwtNjQuNDEgNDAuNjksLTU1LjczIDQwLjY5LC00Ny41NCIvPgo8cG9seWdvbiBmaWxsPSJibGFjayIgc3Ryb2tlPSJibGFjayIgcG9pbnRzPSI0NC4xOSwtNDcuNjIgNDAuNjksLTM3LjYyIDM3LjE5LC00Ny42MiA0NC4xOSwtNDcuNjIiLz4KPC9nPgo8L2c+Cjwvc3ZnPgo=", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); + } + @Test public void importDiagram_WhenTheKrokiUrlIsNotDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); @@ -73,8 +110,8 @@ public void importDiagram_WhenTheKrokiUrlIsNotDefined() throws Exception { @Test public void importDiagram_WhenAnInvalidFormatIsSpecified() throws Exception { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().getConfiguration().addProperty("kroki.url", "https://mermaid.ink"); - workspace.getViews().getConfiguration().addProperty("kroki.format", "jpg"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_FORMAT_PROPERTY, "jpg"); ImageView view = workspace.getViews().createImageView("key"); try { diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java index 5293f6c8f..bf8762878 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java @@ -2,6 +2,7 @@ import com.structurizr.Workspace; import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -43,6 +44,25 @@ public void importDiagram_AsPNG() throws Exception { assertEquals("image/png", view.getContentType()); } + @Test + @Tag("IntegrationTest") + public void importDiagram_AsInlinePNG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_FORMAT_PROPERTY, "png"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_COMPRESS_PROPERTY, "true"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_INLINE_PROPERTY, "true"); + ImageView view = workspace.getViews().createImageView("key"); + + new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("flowchart.mmd", view.getTitle()); + assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + @Test public void importDiagram_AsSVG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); @@ -60,6 +80,25 @@ public void importDiagram_AsSVG() throws Exception { assertEquals("image/svg+xml", view.getContentType()); } + @Test + @Tag("IntegrationTest") + public void importDiagram_AsInlineSVG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_FORMAT_PROPERTY, "svg"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_COMPRESS_PROPERTY, "false"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_INLINE_PROPERTY, "true"); + ImageView view = workspace.getViews().createImageView("key"); + + new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("flowchart.mmd", view.getTitle()); + assertEquals("data:image/svg+xml;base64,PHN2ZyBpZD0ibWVybWFpZC1zdmciIHdpZHRoPSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJmbG93Y2hhcnQiIHN0eWxlPSJtYXgtd2lkdGg6IDQ0MXB4OyIgdmlld0JveD0iMCAwIDQ0MSA1MTcuMTU2MjUiIHJvbGU9ImdyYXBoaWNzLWRvY3VtZW50IGRvY3VtZW50IiBhcmlhLXJvbGVkZXNjcmlwdGlvbj0iZmxvd2NoYXJ0LXYyIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHN0eWxlIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIj5AaW1wb3J0IHVybCgiaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9hamF4L2xpYnMvZm9udC1hd2Vzb21lLzYuNy4yL2Nzcy9hbGwubWluLmNzcyIpOzwvc3R5bGU+PHN0eWxlPiNtZXJtYWlkLXN2Z3tmb250LWZhbWlseToidHJlYnVjaGV0IG1zIix2ZXJkYW5hLGFyaWFsLHNhbnMtc2VyaWY7Zm9udC1zaXplOjE2cHg7ZmlsbDojMzMzO31Aa2V5ZnJhbWVzIGVkZ2UtYW5pbWF0aW9uLWZyYW1le2Zyb217c3Ryb2tlLWRhc2hvZmZzZXQ6MDt9fUBrZXlmcmFtZXMgZGFzaHt0b3tzdHJva2UtZGFzaG9mZnNldDowO319I21lcm1haWQtc3ZnIC5lZGdlLWFuaW1hdGlvbi1zbG93e3N0cm9rZS1kYXNoYXJyYXk6OSw1IWltcG9ydGFudDtzdHJva2UtZGFzaG9mZnNldDo5MDA7YW5pbWF0aW9uOmRhc2ggNTBzIGxpbmVhciBpbmZpbml0ZTtzdHJva2UtbGluZWNhcDpyb3VuZDt9I21lcm1haWQtc3ZnIC5lZGdlLWFuaW1hdGlvbi1mYXN0e3N0cm9rZS1kYXNoYXJyYXk6OSw1IWltcG9ydGFudDtzdHJva2UtZGFzaG9mZnNldDo5MDA7YW5pbWF0aW9uOmRhc2ggMjBzIGxpbmVhciBpbmZpbml0ZTtzdHJva2UtbGluZWNhcDpyb3VuZDt9I21lcm1haWQtc3ZnIC5lcnJvci1pY29ue2ZpbGw6IzU1MjIyMjt9I21lcm1haWQtc3ZnIC5lcnJvci10ZXh0e2ZpbGw6IzU1MjIyMjtzdHJva2U6IzU1MjIyMjt9I21lcm1haWQtc3ZnIC5lZGdlLXRoaWNrbmVzcy1ub3JtYWx7c3Ryb2tlLXdpZHRoOjFweDt9I21lcm1haWQtc3ZnIC5lZGdlLXRoaWNrbmVzcy10aGlja3tzdHJva2Utd2lkdGg6My41cHg7fSNtZXJtYWlkLXN2ZyAuZWRnZS1wYXR0ZXJuLXNvbGlke3N0cm9rZS1kYXNoYXJyYXk6MDt9I21lcm1haWQtc3ZnIC5lZGdlLXRoaWNrbmVzcy1pbnZpc2libGV7c3Ryb2tlLXdpZHRoOjA7ZmlsbDpub25lO30jbWVybWFpZC1zdmcgLmVkZ2UtcGF0dGVybi1kYXNoZWR7c3Ryb2tlLWRhc2hhcnJheTozO30jbWVybWFpZC1zdmcgLmVkZ2UtcGF0dGVybi1kb3R0ZWR7c3Ryb2tlLWRhc2hhcnJheToyO30jbWVybWFpZC1zdmcgLm1hcmtlcntmaWxsOiMzMzMzMzM7c3Ryb2tlOiMzMzMzMzM7fSNtZXJtYWlkLXN2ZyAubWFya2VyLmNyb3Nze3N0cm9rZTojMzMzMzMzO30jbWVybWFpZC1zdmcgc3Zne2ZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDt9I21lcm1haWQtc3ZnIHB7bWFyZ2luOjA7fSNtZXJtYWlkLXN2ZyAubGFiZWx7Zm9udC1mYW1pbHk6InRyZWJ1Y2hldCBtcyIsdmVyZGFuYSxhcmlhbCxzYW5zLXNlcmlmO2NvbG9yOiMzMzM7fSNtZXJtYWlkLXN2ZyAuY2x1c3Rlci1sYWJlbCB0ZXh0e2ZpbGw6IzMzMzt9I21lcm1haWQtc3ZnIC5jbHVzdGVyLWxhYmVsIHNwYW57Y29sb3I6IzMzMzt9I21lcm1haWQtc3ZnIC5jbHVzdGVyLWxhYmVsIHNwYW4gcHtiYWNrZ3JvdW5kLWNvbG9yOnRyYW5zcGFyZW50O30jbWVybWFpZC1zdmcgLmxhYmVsIHRleHQsI21lcm1haWQtc3ZnIHNwYW57ZmlsbDojMzMzO2NvbG9yOiMzMzM7fSNtZXJtYWlkLXN2ZyAubm9kZSByZWN0LCNtZXJtYWlkLXN2ZyAubm9kZSBjaXJjbGUsI21lcm1haWQtc3ZnIC5ub2RlIGVsbGlwc2UsI21lcm1haWQtc3ZnIC5ub2RlIHBvbHlnb24sI21lcm1haWQtc3ZnIC5ub2RlIHBhdGh7ZmlsbDojRUNFQ0ZGO3N0cm9rZTojOTM3MERCO3N0cm9rZS13aWR0aDoxcHg7fSNtZXJtYWlkLXN2ZyAucm91Z2gtbm9kZSAubGFiZWwgdGV4dCwjbWVybWFpZC1zdmcgLm5vZGUgLmxhYmVsIHRleHQsI21lcm1haWQtc3ZnIC5pbWFnZS1zaGFwZSAubGFiZWwsI21lcm1haWQtc3ZnIC5pY29uLXNoYXBlIC5sYWJlbHt0ZXh0LWFuY2hvcjptaWRkbGU7fSNtZXJtYWlkLXN2ZyAubm9kZSAua2F0ZXggcGF0aHtmaWxsOiMwMDA7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjFweDt9I21lcm1haWQtc3ZnIC5yb3VnaC1ub2RlIC5sYWJlbCwjbWVybWFpZC1zdmcgLm5vZGUgLmxhYmVsLCNtZXJtYWlkLXN2ZyAuaW1hZ2Utc2hhcGUgLmxhYmVsLCNtZXJtYWlkLXN2ZyAuaWNvbi1zaGFwZSAubGFiZWx7dGV4dC1hbGlnbjpjZW50ZXI7fSNtZXJtYWlkLXN2ZyAubm9kZS5jbGlja2FibGV7Y3Vyc29yOnBvaW50ZXI7fSNtZXJtYWlkLXN2ZyAucm9vdCAuYW5jaG9yIHBhdGh7ZmlsbDojMzMzMzMzIWltcG9ydGFudDtzdHJva2Utd2lkdGg6MDtzdHJva2U6IzMzMzMzMzt9I21lcm1haWQtc3ZnIC5hcnJvd2hlYWRQYXRoe2ZpbGw6IzMzMzMzMzt9I21lcm1haWQtc3ZnIC5lZGdlUGF0aCAucGF0aHtzdHJva2U6IzMzMzMzMztzdHJva2Utd2lkdGg6Mi4wcHg7fSNtZXJtYWlkLXN2ZyAuZmxvd2NoYXJ0LWxpbmt7c3Ryb2tlOiMzMzMzMzM7ZmlsbDpub25lO30jbWVybWFpZC1zdmcgLmVkZ2VMYWJlbHtiYWNrZ3JvdW5kLWNvbG9yOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7dGV4dC1hbGlnbjpjZW50ZXI7fSNtZXJtYWlkLXN2ZyAuZWRnZUxhYmVsIHB7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO30jbWVybWFpZC1zdmcgLmVkZ2VMYWJlbCByZWN0e29wYWNpdHk6MC41O2JhY2tncm91bmQtY29sb3I6cmdiYSgyMzIsMjMyLDIzMiwgMC44KTtmaWxsOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7fSNtZXJtYWlkLXN2ZyAubGFiZWxCa2d7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwgMjMyLCAyMzIsIDAuNSk7fSNtZXJtYWlkLXN2ZyAuY2x1c3RlciByZWN0e2ZpbGw6I2ZmZmZkZTtzdHJva2U6I2FhYWEzMztzdHJva2Utd2lkdGg6MXB4O30jbWVybWFpZC1zdmcgLmNsdXN0ZXIgdGV4dHtmaWxsOiMzMzM7fSNtZXJtYWlkLXN2ZyAuY2x1c3RlciBzcGFue2NvbG9yOiMzMzM7fSNtZXJtYWlkLXN2ZyBkaXYubWVybWFpZFRvb2x0aXB7cG9zaXRpb246YWJzb2x1dGU7dGV4dC1hbGlnbjpjZW50ZXI7bWF4LXdpZHRoOjIwMHB4O3BhZGRpbmc6MnB4O2ZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjtmb250LXNpemU6MTJweDtiYWNrZ3JvdW5kOmhzbCg4MCwgMTAwJSwgOTYuMjc0NTA5ODAzOSUpO2JvcmRlcjoxcHggc29saWQgI2FhYWEzMztib3JkZXItcmFkaXVzOjJweDtwb2ludGVyLWV2ZW50czpub25lO3otaW5kZXg6MTAwO30jbWVybWFpZC1zdmcgLmZsb3djaGFydFRpdGxlVGV4dHt0ZXh0LWFuY2hvcjptaWRkbGU7Zm9udC1zaXplOjE4cHg7ZmlsbDojMzMzO30jbWVybWFpZC1zdmcgcmVjdC50ZXh0e2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6MDt9I21lcm1haWQtc3ZnIC5pY29uLXNoYXBlLCNtZXJtYWlkLXN2ZyAuaW1hZ2Utc2hhcGV7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO3RleHQtYWxpZ246Y2VudGVyO30jbWVybWFpZC1zdmcgLmljb24tc2hhcGUgcCwjbWVybWFpZC1zdmcgLmltYWdlLXNoYXBlIHB7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO3BhZGRpbmc6MnB4O30jbWVybWFpZC1zdmcgLmljb24tc2hhcGUgcmVjdCwjbWVybWFpZC1zdmcgLmltYWdlLXNoYXBlIHJlY3R7b3BhY2l0eTowLjU7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO2ZpbGw6cmdiYSgyMzIsMjMyLDIzMiwgMC44KTt9I21lcm1haWQtc3ZnIC5sYWJlbC1pY29ue2Rpc3BsYXk6aW5saW5lLWJsb2NrO2hlaWdodDoxZW07b3ZlcmZsb3c6dmlzaWJsZTt2ZXJ0aWNhbC1hbGlnbjotMC4xMjVlbTt9I21lcm1haWQtc3ZnIC5ub2RlIC5sYWJlbC1pY29uIHBhdGh7ZmlsbDpjdXJyZW50Q29sb3I7c3Ryb2tlOnJldmVydDtzdHJva2Utd2lkdGg6cmV2ZXJ0O30jbWVybWFpZC1zdmcgOnJvb3R7LS1tZXJtYWlkLWZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT48Zz48bWFya2VyIGlkPSJtZXJtYWlkLXN2Z19mbG93Y2hhcnQtdjItcG9pbnRFbmQiIGNsYXNzPSJtYXJrZXIgZmxvd2NoYXJ0LXYyIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjUiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjgiIG1hcmtlckhlaWdodD0iOCIgb3JpZW50PSJhdXRvIj48cGF0aCBkPSJNIDAgMCBMIDEwIDUgTCAwIDEwIHoiIGNsYXNzPSJhcnJvd01hcmtlclBhdGgiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7IHN0cm9rZS1kYXNoYXJyYXk6IDEsIDA7Ii8+PC9tYXJrZXI+PG1hcmtlciBpZD0ibWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50U3RhcnQiIGNsYXNzPSJtYXJrZXIgZmxvd2NoYXJ0LXYyIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjQuNSIgcmVmWT0iNSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iOCIgbWFya2VySGVpZ2h0PSI4IiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0gMCA1IEwgMTAgMTAgTCAxMCAwIHoiIGNsYXNzPSJhcnJvd01hcmtlclBhdGgiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7IHN0cm9rZS1kYXNoYXJyYXk6IDEsIDA7Ii8+PC9tYXJrZXI+PG1hcmtlciBpZD0ibWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLWNpcmNsZUVuZCIgY2xhc3M9Im1hcmtlciBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iMTEiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjExIiBtYXJrZXJIZWlnaHQ9IjExIiBvcmllbnQ9ImF1dG8iPjxjaXJjbGUgY3g9IjUiIGN5PSI1IiByPSI1IiBjbGFzcz0iYXJyb3dNYXJrZXJQYXRoIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyBzdHJva2UtZGFzaGFycmF5OiAxLCAwOyIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9Im1lcm1haWQtc3ZnX2Zsb3djaGFydC12Mi1jaXJjbGVTdGFydCIgY2xhc3M9Im1hcmtlciBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iLTEiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjExIiBtYXJrZXJIZWlnaHQ9IjExIiBvcmllbnQ9ImF1dG8iPjxjaXJjbGUgY3g9IjUiIGN5PSI1IiByPSI1IiBjbGFzcz0iYXJyb3dNYXJrZXJQYXRoIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyBzdHJva2UtZGFzaGFycmF5OiAxLCAwOyIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9Im1lcm1haWQtc3ZnX2Zsb3djaGFydC12Mi1jcm9zc0VuZCIgY2xhc3M9Im1hcmtlciBjcm9zcyBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMSAxMSIgcmVmWD0iMTIiIHJlZlk9IjUuMiIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTEiIG1hcmtlckhlaWdodD0iMTEiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAxLDEgbCA5LDkgTSAxMCwxIGwgLTksOSIgY2xhc3M9ImFycm93TWFya2VyUGF0aCIgc3R5bGU9InN0cm9rZS13aWR0aDogMjsgc3Ryb2tlLWRhc2hhcnJheTogMSwgMDsiLz48L21hcmtlcj48bWFya2VyIGlkPSJtZXJtYWlkLXN2Z19mbG93Y2hhcnQtdjItY3Jvc3NTdGFydCIgY2xhc3M9Im1hcmtlciBjcm9zcyBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMSAxMSIgcmVmWD0iLTEiIHJlZlk9IjUuMiIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTEiIG1hcmtlckhlaWdodD0iMTEiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAxLDEgbCA5LDkgTSAxMCwxIGwgLTksOSIgY2xhc3M9ImFycm93TWFya2VyUGF0aCIgc3R5bGU9InN0cm9rZS13aWR0aDogMjsgc3Ryb2tlLWRhc2hhcnJheTogMSwgMDsiLz48L21hcmtlcj48ZyBjbGFzcz0icm9vdCI+PGcgY2xhc3M9ImNsdXN0ZXJzIi8+PGcgY2xhc3M9ImVkZ2VQYXRocyI+PHBhdGggZD0iTTIyMS44NTIsNjJMMjIxLjg1Miw2OC4xNjdDMjIxLjg1Miw3NC4zMzMsMjIxLjg1Miw4Ni42NjcsMjIxLjkyNiw5OC40MTdDMjIyLDExMC4xNjcsMjIyLjE0OSwxMjEuMzM0LDIyMi4yMjQsMTI2LjkxN0wyMjIuMjk4LDEzMi41IiBpZD0iTF9BX0JfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjxwYXRoIGQ9Ik0yMjIuMzUyLDE5MC41TDIyMi4yNjgsMTk0LjU4M0MyMjIuMTg1LDE5OC42NjcsMjIyLjAxOCwyMDYuODMzLDIyMi4wMDUsMjE0LjVDMjIxLjk5MiwyMjIuMTY3LDIyMi4xMzMsMjI5LjMzNCwyMjIuMjAzLDIzMi45MTdMMjIyLjI3MywyMzYuNTAxIiBpZD0iTF9CX0NfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjxwYXRoIGQ9Ik0xODAuMjE1LDMzOS41MTlMMTYwLjU5LDM1Mi42MjVDMTQwLjk2NiwzNjUuNzMyLDEwMS43MTcsMzkxLjk0NCw4Mi4wOTMsNDEwLjU1QzYyLjQ2OSw0MjkuMTU2LDYyLjQ2OSw0NDAuMTU2LDYyLjQ2OSw0NDUuNjU2TDYyLjQ2OSw0NTEuMTU2IiBpZD0iTF9DX0RfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjxwYXRoIGQ9Ik0yMjIuMzUyLDM4MS42NTZMMjIyLjI2OCwzODcuNzRDMjIyLjE4NSwzOTMuODIzLDIyMi4wMTgsNDA1Ljk5LDIyMS45MzUsNDE3LjU3M0MyMjEuODUyLDQyOS4xNTYsMjIxLjg1Miw0NDAuMTU2LDIyMS44NTIsNDQ1LjY1NkwyMjEuODUyLDQ1MS4xNTYiIGlkPSJMX0NfRV8wIiBjbGFzcz0iIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZWRnZS10aGlja25lc3Mtbm9ybWFsIGVkZ2UtcGF0dGVybi1zb2xpZCBmbG93Y2hhcnQtbGluayIgc3R5bGU9IiIgbWFya2VyLWVuZD0idXJsKCNtZXJtYWlkLXN2Z19mbG93Y2hhcnQtdjItcG9pbnRFbmQpIi8+PHBhdGggZD0iTTI2NC4zNDQsMzM5LjY2NEwyODMuNiwzNTIuNzQ2QzMwMi44NTcsMzY1LjgyOCwzNDEuMzcsMzkxLjk5MiwzNjAuNjI2LDQxMC41NzRDMzc5Ljg4Myw0MjkuMTU2LDM3OS44ODMsNDQwLjE1NiwzNzkuODgzLDQ0NS42NTZMMzc5Ljg4Myw0NTEuMTU2IiBpZD0iTF9DX0ZfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjwvZz48ZyBjbGFzcz0iZWRnZUxhYmVscyI+PGcgY2xhc3M9ImVkZ2VMYWJlbCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIxLjg1MTU2MjUsIDk5KSI+PGcgY2xhc3M9ImxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzkuMTMyODEyNSwgLTEyKSI+PGZvcmVpZ25PYmplY3Qgd2lkdGg9Ijc4LjI2NTYyNSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgY2xhc3M9ImxhYmVsQmtnIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9ImVkZ2VMYWJlbCAiPjxwPkdldCBtb25leTwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9ImVkZ2VMYWJlbCI+PGcgY2xhc3M9ImxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLCAwKSI+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjAiIGhlaWdodD0iMCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgY2xhc3M9ImxhYmVsQmtnIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9ImVkZ2VMYWJlbCAiPjwvc3Bhbj48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9nPjwvZz48ZyBjbGFzcz0iZWRnZUxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Mi40Njg3NSwgNDE4LjE1NjI1KSI+PGcgY2xhc3M9ImxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTUuMTI1LCAtMTIpIj48Zm9yZWlnbk9iamVjdCB3aWR0aD0iMzAuMjUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIGNsYXNzPSJsYWJlbEJrZyIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJlZGdlTGFiZWwgIj48cD5PbmU8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJlZGdlTGFiZWwiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMS44NTE1NjI1LCA0MTguMTU2MjUpIj48ZyBjbGFzcz0ibGFiZWwiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNC42NzE4NzUsIC0xMikiPjxmb3JlaWduT2JqZWN0IHdpZHRoPSIyOS4zNDM3NSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgY2xhc3M9ImxhYmVsQmtnIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9ImVkZ2VMYWJlbCAiPjxwPlR3bzwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9ImVkZ2VMYWJlbCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzc5Ljg4MjgxMjUsIDQxOC4xNTYyNSkiPjxnIGNsYXNzPSJsYWJlbCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIwLjg5ODQzNzUsIC0xMikiPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI0MS43OTY4NzUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIGNsYXNzPSJsYWJlbEJrZyIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJlZGdlTGFiZWwgIj48cD5UaHJlZTwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PC9nPjxnIGNsYXNzPSJub2RlcyI+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCAgIiBpZD0iZmxvd2NoYXJ0LUEtMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIxLjg1MTU2MjUsIDM1KSI+PHJlY3QgY2xhc3M9ImJhc2ljIGxhYmVsLWNvbnRhaW5lciIgc3R5bGU9IiIgeD0iLTY2LjAwNzgxMjUiIHk9Ii0yNyIgd2lkdGg9IjEzMi4wMTU2MjUiIGhlaWdodD0iNTQiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zNi4wMDc4MTI1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjcyLjAxNTYyNSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJub2RlTGFiZWwgIj48cD5DaHJpc3RtYXM8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJub2RlIGRlZmF1bHQgICIgaWQ9ImZsb3djaGFydC1CLTEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMS44NTE1NjI1LCAxNjMpIj48ZyBjbGFzcz0iYmFzaWMgbGFiZWwtY29udGFpbmVyIG91dGVyLXBhdGgiPjxwYXRoIGQ9Ik0tNTUuMzY3MTg3NSAtMjcgQy0zMC4yMDQ5NTY4MTg5MTIyMzcgLTI3LCAtNS4wNDI3MjYxMzc4MjQ0NzQgLTI3LCA1NS4zNjcxODc1IC0yNyBDNTUuMzY3MTg3NSAtMjcsIDU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNyBDNTUuNDgxOTY0MTg0NzQ3NjkgLTI2Ljk5NTI1MjgwMTIwMzY4LCA1NS41OTY3NDA4Njk0OTUzOCAtMjYuOTkwNTA1NjAyNDA3MzY0LCA1NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEM1NS45MzU4OTE1NDUzNjU0NSAtMjYuOTYzNTAxMTAzMDk4MDYsIDU2LjA5MTY5ODg2MzM2OTIzIC0yNi45NDQwNzk3NDExNjI3NzQsIDU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQzU2LjMwNTQ4MjkxMDI0NDczIC0yNi45MDc2MjU5NDAyMDMzNzgsIDU2LjQyMDgwNTM2OTA4NTc4NCAtMjYuODgzNDQ1MzYzMzkzMTQzLCA1Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNzIyNDc4MjM5MzM3MjQgLTI2LjgwODkzNDc3NDkwMDkyNiwgNTYuODUwMzQxNTQyOTcwNSAtMjYuNzcwODY4MjIwMTA1MTk2LCA1Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEM1Ny4wNjg1Njk5NzEzMDQ2NzYgLTI2LjY5ODY5NTMzODMxMjEzLCA1Ny4xNDY0NTUwOTY1ODU5MyAtMjYuNjY4MzA0NDY4MTIxMDg2LCA1Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuNDk5MzQ0NDA2MTA4MjIgLTI2LjUxODQwMzI3MTc0NDQzLCA1Ny42MjMwMjQxODg5NTE2MDQgLTI2LjQ1NzkzOTkxMDIxMzU3NSwgNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQzU3Ljg0MjcxNTMwMjI0MzkyIC0yNi4zNDAyODk4MDY0NzMzMTUsIDU3LjkzODUwNjEzOTMwMjQ3NiAtMjYuMjgzMjEwODU2OTE0MTg0LCA1OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMjA3MjQ3MDcwNTc5MiAtMjYuMTEwNjM2MjI3NzQyNDA0LCA1OC4zMTI1NjU4NTA1NDYyNyAtMjYuMDM1NDQwMDY0MTcyMTY0LCA1OC40MzgyNTEwNjM0NDgzNCAtMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjU1NDEyNjAwMDEwNzc2NSAtMjUuODQ3NTYxNDgxODM0NTMsIDU4LjY3MDAwMDkzNjc2NzE5IC0yNS43NDk0MjA0MTY2ODcwODQsIDU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IEM1OC44NTUwMzgwMjg2MDczNiAtMjUuNTc3MTc2ODgyODg3MDAyLCA1OC45NTY0ODA2OTkwODYwMTUgLTI1LjQ3NTczNDIxMjQwODM0OCwgNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBDNTkuMTAwMjg1MzM2Njc2NjggLTI1LjMyMjA4NTQ3MDU2ODU2NywgNTkuMTU0NzYzNjE5OTg3NyAtMjUuMjU3NzYzMDgzMDA4NDMsIDU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBDNTkuMzY1NDk3Nzc5ODYwNTkgLTI0Ljk5NzM4MTg0OTEwODgyNywgNTkuNDE4MTA1NTEyNzM5MjIgLTI0LjkyMzcwMDEzNDc2OTMxLCA1OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzNiBDNTkuNjM2MTkyMjU2NjkxNzg1IC0yNC41OTUxNTk1NTE2MjA0NDQsIDU5LjcxOTM2NDYyMjA3MDkzNCAtMjQuNDU1NTc4MzEyNjI4NzU2LCA1OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzcgQzU5LjgyMTI3NTk1MTk2NDQ3IC0yNC4yNjM3MTQ5NzI3MjI0NSwgNTkuODc3OTk1NjQ3ODk2NTA1IC0yNC4xNDc2OTI5ODAyNTk1MzUsIDU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQzU5Ljk4NjIxNTc4NTA3MDAyIC0yMy45MDU1NTE2MzA3Nzc0LCA2MC4wMjYzNzc0MzY4NjQ3NiAtMjMuODAyNjI2MTM4Mjg5OTUyLCA2MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMTMyNzU0ODU4NTU5ODIgLTIzLjUwMDk1OTMyMzAzNjcyNCwgNjAuMTY5MjM2MDA4NjE2NDY1IC0yMy4zNzg0MjEzMDAwNTAwMzIsIDYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQzYwLjIzMzE0MDkyMjIwMzk3IC0yMy4xMzcwNDA3NjE5MTcwMiwgNjAuMjUyMDkzMDE0NzExMyAtMjMuMDQ2NjU0MDg4MTMwMDQ3LCA2MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgQzYwLjMxNjkwMjMyNTk2MjgxIC0yMi42NzkzMDQwNTc0OTkzOSwgNjAuMzM0ODEwNjM0OTEyIC0yMi41MzU2MzUxNjM1OTUxMSwgNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzU0NDU4NTI3OTA5ODkgLTIyLjMwNzc1ODE3MDU0MDc1OCwgNjAuMzU4ODA3MDkwNzg2NDQgLTIyLjIwMjYxOTYxMzcxOTg1NCwgNjAuMzY3MTg3NSAtMjIgQzYwLjM2NzE4NzUgLTIyLCA2MC4zNjcxODc1IC0yMiwgNjAuMzY3MTg3NSAtMjIgQzYwLjM2NzE4NzUgLTYuNjA5NzgyODc3MTEyOTA4LCA2MC4zNjcxODc1IDguNzgwNDM0MjQ1Nzc0MTg1LCA2MC4zNjcxODc1IDIyIEM2MC4zNjcxODc1IDIyLCA2MC4zNjcxODc1IDIyLCA2MC4zNjcxODc1IDIyIEM2MC4zNjMxMDk0MTQzMTA1NjQgMjIuMDk4NTk5MDIxMzY2Njg1LCA2MC4zNTkwMzEzMjg2MjExMzYgMjIuMTk3MTk4MDQyNzMzMzczLCA2MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgQzYwLjMzODgxNDQxMzMzMjkxIDIyLjUwMzUxNDk2NjI3MjA3NSwgNjAuMzI3NTE4ODYxNjMyNDg0IDIyLjU5NDEzMzIwNTE4MjQ4NywgNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQzYwLjI2NTY3MzY2NDU5NjI3IDIyLjk4MTg4NDk5ODg4NTk5LCA2MC4yMzIzNTMzMTIxNzg5NCAyMy4xNDA3OTcwNDYzNjgzMSwgNjAuMjE0MTg4ODI5Njk2NjUgMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4xODQ1MDc0NDQxNTI1NDQgMjMuMzI3MTI1NDUyMzA3OTk2LCA2MC4xNTQ4MjYwNTg2MDg0NCAyMy40MjY4MjM0Njg5MTIsIDYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQzYwLjA0MjY3MTUwOTM2NTI5IDIzLjc2MDg2ODAwOTI4Mzg0LCA1OS45ODkwNjkzMTAyMjc0MSAyMy44OTgyMzg2NzI1NDQyNjcsIDU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuOTAyMDYyMjEyOTQ3NjU1IDI0LjA5ODQ2NDAzNDU5ODA2LCA1OS44NTgwNzAyOTI2MjAwMiAyNC4xODg0NTA5NDU5MzEyNzYsIDU5Ljc2NDU1NjI1NjAzMjQ1IDI0LjM3OTczNjk2NTE4NTM2NiBDNTkuNjg1NjQ4MDMxNTg5MTUgMjQuNTEyMTYyMDUyMzQyODg3LCA1OS42MDY3Mzk4MDcxNDU4NSAyNC42NDQ1ODcxMzk1MDA0MDUsIDU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQzU5LjQ1OTc1Njc2ODgzODQyNSAyNC44NjUzNjM5MjA5NDk0OCwgNTkuMzY2NDkzNjQ2MzY0MjA1IDI0Ljk5NTk4NzA1MTI4NjgyNSwgNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQzU5LjIzNzczNjUyMDE2NTk0IDI1LjE1OTc5NzE2NDg4NjgxLCA1OS4xNjI1ODI5OTMzNDk5MSAyNS4yNDg1MzA3NjYzMjUyODUsIDU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBDNTguOTMzMTA4MjAxNDY3NzkgMjUuNDk5MTA2NzEwMDI2NTc0LCA1OC44MjA0MDkzNDk1Njk5MiAyNS42MTE4MDU1NjE5MjQ0NCwgNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBDNTguNjUxOTM0NzA3MDgxMDU0IDI1Ljc2NDcyMTczMjcwNjg0NiwgNTguNTUwMjc0MDU2MDMzNCAyNS44NTA4MjM5MTIwNDgwNCwgNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjM2MjY5NzIzMjc3NTEgMjUuOTk5NjQ2OTQ1MjMxMTcsIDU4LjI4NzE0MzQwMjEwMTg1NCAyNi4wNTM1OTEzNDM0ODAzNywgNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMDAxNzI2ODI4NjczODIgMjYuMjQ1NTM5NTAyODQxODIyLCA1Ny45MDE1MjUzNjY3MzU1IDI2LjMwNTI0NjYxNDM3MSwgNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBDNTcuNjU5NzUyNzU1OTA3NzI1IDI2LjQzOTk4NDQwNzkzNzI2LCA1Ny41NzI1ODEwNDY2MzAwOSAyNi40ODI2MDAwNTk4NDIwNzQsIDU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgQzU3LjI4NzYxMjIwOTIyMDkzIDI2LjYxMzIyNDc5MTg1MjQ2MywgNTcuMTk5NTU5Nzk1MTc3MDIgMjYuNjQ3NTgyOTUwNDI5NjM3LCA1Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQzU2Ljg5MDc2NzQ0NzIzMTc1IDI2Ljc1ODgzMjkwNjk1NDM1LCA1Ni43OTA4NTAwNDg0NDAwODUgMjYuNzg4NTc5NjA1NDA1NTI3LCA1Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIEM1Ni40OTAzMjk4MTI5NjQwNSAyNi44Njg4Njc2MjA0OTQ4MTgsIDU2LjM4NjA0NDY5MDIyNDEgMjYuODkwNzMzOTExMjkyOTg3LCA1Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQzU2LjAyOTUyMzkyMjY4NTQ3IDI2Ljk1MTgyOTgzOTQzNjY4NywgNTUuODY4ODg3MzkzOTY3MjggMjYuOTcxODUzMTYxODU5NzYyLCA1NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgQzU1LjY1MTg4NTcyODY5NTI2IDI2Ljk4ODIyNDc5NDE1OTY1NiwgNTUuNTIzNjg3MjMwMDI4ODYgMjYuOTkzNTI3MTIzMjg1OTcsIDU1LjM2NzE4NzUgMjcgQzU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcgQzI4LjU1MDIzMDcyMDQwMjE2NSAyNywgMS43MzMyNzM5NDA4MDQzMzEgMjcsIC01NS4zNjcxODc1IDI3IEMtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcsIC01NS4zNjcxODc1IDI3IEMtNTUuNDkwNDAxNzQxOTI5Nzk0IDI2Ljk5NDkwMzgyMTI1NzIxOCwgLTU1LjYxMzYxNTk4Mzg1OTU4IDI2Ljk4OTgwNzY0MjUxNDQzLCAtNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuODcwMDc2NjQ2NjAxMTM0IDI2Ljk3MTcwNDkyMTY3MzU0NSwgLTU1Ljk2MDA2OTA2NTg0MDYgMjYuOTYwNDg3Mzc4MzEzNzQ0LCAtNTYuMTkwMTYwNDUxNDAzNjcgMjYuOTMxODA2NTE3MDEzNjEyIEMtNTYuMzA0MDA5NDM2MDQ0NDk1IDI2LjkwNzkzNDg5NTI1MzcxNywgLTU2LjQxNzg1ODQyMDY4NTMyIDI2Ljg4NDA2MzI3MzQ5MzgyMywgLTU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgQy01Ni42ODIyNTYyMjI4OTgwOCAyNi44MjA5MDkzODgwMTcxOTIsIC01Ni43Njk4OTc1MTAwOTIxNyAyNi43OTQ4MTc0NDYzMzc3MywgLTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDLTU3LjE0MDk1ODA2MzM4MzEyIDI2LjY3MDQ0OTQxNzEzMjg0MywgLTU3LjI5MTIzMTI4MDc0MjgxNCAyNi42MTE4MTI2MjU3NjI1MTQsIC01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNDUxOTQ5NzMxMjY5NDUgMjYuNTQxNTczMTE2NDMxNTIyLCAtNTcuNTI4MjM0ODM5Mjc0MDU2IDI2LjUwNDI3OTU5OTU4Nzc1NCwgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgQy01Ny44NDc2ODIwMzkzMTI2NyAyNi4zMzczMzAyNzM1NjQ2NzgsIC01Ny45NDg0Mzk2MTM0Mzk5OCAyNi4yNzcyOTE3OTEwOTY5MDcsIC01OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTguMjI4NDIzMjg3ODk3MzEgMjYuMDk1NTE2Njk5MTg1ODcsIC01OC4zNTQ5MTgyODUxODI0ODUgMjYuMDA1MjAxMDA3MDU5MDkyLCAtNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQy01OC41NjIxNTI4MDA2NzQ1NDYgMjUuODQwNzYzMTI4NTQwMzYzLCAtNTguNjg2MDU0NTM3OTAwNzUgMjUuNzM1ODIzNzEwMDk4NzU3LCAtNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IEMtNTguODE2MzM1MTU3ODUxMjg1IDI1LjYxNTg3OTc1MzY0MzA4MiwgLTU4Ljg3OTA3NDk1NzU3Mzg1NiAyNS41NTMxMzk5NTM5MjA1MDcsIC01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQy01OS4xMzc0NDE0MTkyMjEwOCAyNS4yNzgyMTUzNjY4MDY0MjgsIC01OS4yMjkwNzU3ODUwNzY1IDI1LjE3MDAyMjg3NTQ4NDE1LCAtNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4zNzkwNzc1MDY0MTUxIDI0Ljk3ODM2MjI1ODY3ODkwMywgLTU5LjQ0NTI2NDk2NTg0ODI0IDI0Ljg4NTY2MDk1MzkwOTQ2NSwgLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQy01OS42MTg1NzgzNDUyNTk3NiAyNC42MjQ3MTk1MDkwNjUwMTMsIC01OS42ODQxMzY3OTkyMDY4OCAyNC41MTQ2OTgyMjc1MTc4OTQsIC01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBDLTU5LjgxMzYxMTQ1NzU2NzY3IDI0LjI3OTM5Mjk0NjU5OTUyLCAtNTkuODYyNjY2NjU5MTAyOSAyNC4xNzkwNDg5MjgwMTM2NjgsIC01OS45NDYwNTQxMzMyNzUyOCAyNC4wMDg0NzcxMjMyNjQ4NSBDLTU5Ljk5MDQ3OTQzNTIyMDk2IDIzLjg5NDYyNDgzMTkwNDExNCwgLTYwLjAzNDkwNDczNzE2NjY1IDIzLjc4MDc3MjU0MDU0MzM4LCAtNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBDLTYwLjEyNDc3MzE2MTY2NzAxNSAyMy41Mjc3NjkzNzAzMDIzNzMsIC02MC4xNTMyNzI2MTQ4MzA4NSAyMy40MzIwNDEzOTQ1ODEzMjYsIC02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4yMzY3MDE1MjUyNjU3IDIzLjEyMDA1OTQ2NzI0OTkyNCwgLTYwLjI1OTIxNDIyMDgzNDc1IDIzLjAxMjY5MTQ5ODc5NTg1LCAtNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQy02MC4zMTEyNjI3MjE5NDc4NyAyMi43MjQ1NDc2MTg2NDA5MSwgLTYwLjMyMzUzMTQyNjg4MjEzIDIyLjYyNjEyMjI4NTg3ODE0NywgLTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBDLTYwLjM1NjM5NjM0OTE4NDI0IDIyLjI2MDkwNTk3MTc5MjQyMiwgLTYwLjM2MjY4MjczMzMzNTEzNCAyMi4xMDg5MTUyMTYyMjMxNzgsIC02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSAyMiwgLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSA5LjcyMDAzMjY0MjAyNzc2MiwgLTYwLjM2NzE4NzUgLTIuNTU5OTM0NzE1OTQ0NDc1NCwgLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzY3MTg3NSAtMjIsIC02MC4zNjcxODc1IC0yMiwgLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYyODU3NDk5OTA1MzQgLTIyLjEwNDY4OTc1MDEzMzc5NCwgLTYwLjM1ODUyNzQ5OTgxMDY4NCAtMjIuMjA5Mzc5NTAwMjY3NTg0LCAtNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IEMtNjAuMzM1ODAwMTMxODUyODYgLTIyLjUyNzY5Njk1MjgyMjgxNSwgLTYwLjMyMTQ5MDI5ODY3MjM2NiAtMjIuNjQyNDk3MTc4MjgzOTc1LCAtNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMjgxMzQyNDkxMTA3NzM2IC0yMi45MDcxNTY5Mzg2NDgzNzQsIC02MC4yNjM2OTA5NjUyMDE4NSAtMjIuOTkxMzQwOTI1ODkzMDgsIC02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEMtNjAuMTY4MDA3MzI5NjMwMjg1IC0yMy4zODI1NDgzNjAwMTQzNzIsIC02MC4xMjE4MjU4Mjk1NjM5MiAtMjMuNTM3NjY5Mjg0MzI0NzQ2LCAtNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgQy02MC4wNTQ2NTAzNjUzNTQ0NSAtMjMuNzMwMTY4ODMyNDA2ODIsIC02MC4wMTMwMjcwMjIyMDU3MSAtMjMuODM2ODQwMzE4NzkwMjI3LCAtNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBDLTU5Ljg3NTQxNjk3NjMwOTc2IC0yNC4xNTI5Njc3MzcyOTkxMzYsIC01OS44MDQ3Nzk4MTkzNDQyMjYgLTI0LjI5NzQ1ODM1MTMzMzQyLCAtNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM2NiBDLTU5LjcwMDYyMTA5NTI1Nzk5IC0yNC40ODcwMzQwMDkyODU5LCAtNTkuNjM2Njg1OTM0NDgzNTQgLTI0LjU5NDMzMTA1MzM4NjQzLCAtNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzMgQy01OS40OTM3MDc1NjU0MzEwMiAtMjQuODE3ODEyODcxNzE2NzI1LCAtNTkuNDM0Mzk1MjM5NTQ5NCAtMjQuOTAwODg0OTUyODIxMzIsIC01OS4zMTI4OTAwNDY5ODE5NyAtMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4yMjI0MTUyNTI0OTI2MSAtMjUuMTc3ODg2OTUxNDQ1Nzk2LCAtNTkuMTMxOTQwNDU4MDAzMjYgLTI1LjI4NDcxMDMzOTQ0MzI1NCwgLTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgQy01OC45NTQ4NjY5NzI1Njg4NDQgLTI1LjQ3NzM0NzkzODkyNTUyLCAtNTguODYzOTI2ODkxNzcyMDMgLTI1LjU2ODI4ODAxOTcyMjMzLCAtNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgQy01OC42ODU3ODExMzMyMTU0OSAtMjUuNzM2MDU1MjcyMDU1MTI1LCAtNTguNjE3OTY2OTA4MzAyMjggLTI1Ljc5MzQ5MDk5MDc0NDU5OCwgLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgQy01OC4zNjQ4NDkyMDg0NjE2MzUgLTI1Ljk5ODExMDQ2NDEyMzI5MywgLTU4LjI5MTQ0NzM1MzQ3NDkyIC0yNi4wNTA1MTgzODEyNjQ2MiwgLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTcuOTc1NTMyNDU5MzkwMDIgLTI2LjI2MTE0Nzk1OTAyNjA5NywgLTU3Ljg0OTEzNjYyODE2NzkwNSAtMjYuMzM2NDYzNTI2NzM5NTQ2LCAtNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MDkzMjgwNzM5MTc1NSAtMjYuNDY0NjM1NTMyODU5Njg1LCAtNTcuNDcxNzMxNjgyNjQ5NzQgLTI2LjUzMTkwMjMwOTY4NjkyLCAtNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQy01Ny4yNzQzNzA3ODI1MDY2OSAtMjYuNjE4MzkxNjEyNTg5NTg3LCAtNTcuMTczMDc2OTQxNzQ4NTQgLTI2LjY1NzkxNjU5MTkwMzg4NCwgLTU2Ljk5MDY4NDg0NjAyMzQyIC0yNi43MjkwODYyMDg1MDMxNzMgQy01Ni44ODU5NjU3MDA2Njk2MTUgLTI2Ljc2MDI2MjQ0ODg0MzQ1MiwgLTU2Ljc4MTI0NjU1NTMxNTgyIC0yNi43OTE0Mzg2ODkxODM3MywgLTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNDkwMjQ4NTEyNjIwOTIgLTI2Ljg2ODg4NDY2NzM4NDEzNywgLTU2LjM4NTg4MjA4OTUzNzg2IC0yNi44OTA3NjgwMDUwNzE2MTcsIC01Ni4xOTAxNjA0NTE0MDM2NyAtMjYuOTMxODA2NTE3MDEzNjEyIEMtNTYuMDg2MzE4NTkyNjY3NjEgLTI2Ljk0NDc1MDM5MTIwNjMzLCAtNTUuOTgyNDc2NzMzOTMxNTU2IC0yNi45NTc2OTQyNjUzOTkwNTIsIC01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuNjk1MzIyNTMxNTM1NjIgLTI2Ljk4NjQyODIzNDcwNDk4LCAtNTUuNjEwNTYwODM1NzA5NTg1IC0yNi45ODk5MzQwMDQzNzY2MTgsIC01NS4zNjcxODc1IC0yNyBDLTU1LjM2NzE4NzUgLTI3LCAtNTUuMzY3MTg3NSAtMjcsIC01NS4zNjcxODc1IC0yNyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjAiIGZpbGw9IiNFQ0VDRkYiIHN0eWxlPSIiLz48cGF0aCBkPSJNLTU1LjM2NzE4NzUgLTI3IEMtMjAuMjQyNzk3ODM4MTEyNzA4IC0yNywgMTQuODgxNTkxODIzNzc0NTg1IC0yNywgNTUuMzY3MTg3NSAtMjcgTS01NS4zNjcxODc1IC0yNyBDLTEyLjEwNTEzNzc2Mzc0NzU2NCAtMjcsIDMxLjE1NjkxMTk3MjUwNDg3MiAtMjcsIDU1LjM2NzE4NzUgLTI3IE01NS4zNjcxODc1IC0yNyBDNTUuMzY3MTg3NSAtMjcsIDU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNyBNNTUuMzY3MTg3NSAtMjcgQzU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNywgNTUuMzY3MTg3NSAtMjcgTTU1LjM2NzE4NzUgLTI3IEM1NS40Njg1MTUyMzY1NTI1NCAtMjYuOTk1ODA5MDUzODE1NjUsIDU1LjU2OTg0Mjk3MzEwNTA4IC0yNi45OTE2MTgxMDc2MzEyOTcsIDU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgTTU1LjM2NzE4NzUgLTI3IEM1NS40ODYyNzY3MzcyNDMzMSAtMjYuOTk1MDc0NDMyNzEzMDY4LCA1NS42MDUzNjU5NzQ0ODY2MiAtMjYuOTkwMTQ4ODY1NDI2MTMyLCA1NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IE01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEM1NS44NjU4ODcxNDgzNDkwNCAtMjYuOTcyMjI3MTQyMDg2MDYyLCA1NS45NTE2OTAwNjkzMzY0MSAtMjYuOTYxNTMxODE5MTM4NzgsIDU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgTTU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgQzU1LjkxNjIwMzA1NzQ0NDk3NCAtMjYuOTY1OTU1MjcwNTU3OTc3LCA1Ni4wNTIzMjE4ODc1MjgyOSAtMjYuOTQ4OTg4MDc2MDgyNjEsIDU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgTTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQzU2LjMzMTIyODgxNDEwMDc3NiAtMjYuOTAyMjI3NTkxODUwNDUsIDU2LjQ3MjI5NzE3Njc5Nzg4IC0yNi44NzI2NDg2NjY2ODcyODcsIDU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni4xOTAxNjA0NTE0MDM2NyAtMjYuOTMxODA2NTE3MDEzNjEyIEM1Ni4yNzkzNjMxMzA3MTE4MiAtMjYuOTEzMTAyNjgyMDQxOTUsIDU2LjM2ODU2NTgxMDAxOTk3IC0yNi44OTQzOTg4NDcwNzAyODUsIDU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNzE1NTI0NTQwNjYzNTQgLTI2LjgxMTAwNDk4MDY5MTU5NiwgNTYuODM2NDM0MTQ1NjIzMDcgLTI2Ljc3NTAwODYzMTY4NjUzNiwgNTYuOTkwNjg0ODQ2MDIzNDIgLTI2LjcyOTA4NjIwODUwMzE3MyBNNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgQzU2LjcxNDI3NDc0MDkzMDA4IC0yNi44MTEzNzcwNjIxOTMzNjUsIDU2LjgzMzkzNDU0NjE1NjE2NSAtMjYuNzc1NzUyNzk0NjkwMDc4LCA1Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIE01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEM1Ny4xMzU2MTAyMTcwNjk4NSAtMjYuNjcyNTM2MTUzMjM5NjU1LCA1Ny4yODA1MzU1ODgxMTYyOSAtMjYuNjE1OTg2MDk3OTc2MTQsIDU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEM1Ny4xNDAxNTYyNTQzNzQxNyAtMjYuNjcwNzYyMjgzOTc5LCA1Ny4yODk2Mjc2NjI3MjQ5MSAtMjYuNjEyNDM4MzU5NDU0ODIsIDU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE01Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuNDc0MzUyNDc4NzY0MTggLTI2LjUzMDYyMTA4MDUzNzU1NywgNTcuNTczMDQwMzM0MjYzNTI0IC0yNi40ODIzNzU1Mjc3OTk4MjUsIDU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IE01Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuNDg3Mjg4NjYyNjM2NiAtMjYuNTI0Mjk2OTY1NTk4NzU0LCA1Ny41OTg5MTI3MDIwMDgzNTYgLTI2LjQ2OTcyNzI5NzkyMjIyMywgNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgTTU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IEM1Ny44MzkwOTM1MDM0MzY3IC0yNi4zNDI0NDc5MzAxMjg3NTYsIDU3LjkzMTI2MjU0MTY4ODAzIC0yNi4yODc1MjcxMDQyMjUwNjcsIDU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IE01Ny43NDY5MjQ0NjUxODUzNjYgLTI2LjM5NzM2ODc1NjAzMjQ0NiBDNTcuODI5OTI3ODU5OTM0MDMgLTI2LjM0NzkwOTQ2ODE5NjgwNCwgNTcuOTEyOTMxMjU0NjgyNzA0IC0yNi4yOTg0NTAxODAzNjExNjUsIDU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IE01OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMTczNjUzMzA4ODYyNTUgLTI2LjEzNDYyMTcxMjU4OTIyLCA1OC4yNDUzNzgzMjcxMTI5NiAtMjYuMDgzNDExMDMzODY1Nzk0LCA1OC40MzgyNTEwNjM0NDgzNCAtMjUuOTQ1NzAyNTQ2OTgxOTcgTTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEM1OC4xOTE4OTA1Njc1MDcwMSAtMjYuMTIxNjAwNTYwMTkzMzE1LCA1OC4yODE4NTI4NDQ0MDE4OSAtMjYuMDU3MzY4NzI5MDczOTksIDU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NyBNNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk3IEM1OC41MjYwOTM4MjY2NzgzNyAtMjUuODcxMzAzNTIxNjQxMTU2LCA1OC42MTM5MzY1ODk5MDg0MDUgLTI1Ljc5NjkwNDQ5NjMwMDM0LCA1OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBNNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk3IEM1OC41MDk4MTA0MjQ2Mjc2IC0yNS44ODUwOTQ4NTk2NzgxNiwgNTguNTgxMzY5Nzg1ODA2ODY0IC0yNS44MjQ0ODcxNzIzNzQzNDcsIDU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IE01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBDNTguODQzNDgyODQ4MjU2NDcgLTI1LjU4ODczMjA2MzIzNzg5NSwgNTguOTMzMzcwMzM4Mzg0MjMgLTI1LjQ5ODg0NDU3MzExMDEzLCA1OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IE01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBDNTguODcwMDQxMjA3OTAxOTIgLTI1LjU2MjE3MzcwMzU5MjQ0MywgNTguOTg2NDg3MDU3Njc1MTM0IC0yNS40NDU3Mjc4NTM4MTkyMzIsIDU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgTTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgQzU5LjEzMjAyMzI2ODgzMjY3NSAtMjUuMjg0NjEyNTY0ODgxNzEyLCA1OS4yMTgyMzk0ODQyOTk2OTQgLTI1LjE4MjgxNzI3MTYzNDcyLCA1OS4zMTI4OTAwNDY5ODE5NyAtMjUuMDcxMDYzNTYzNDQ4MzQgTTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgQzU5LjExMTE5MDI0MTMxNjEyIC0yNS4zMDkyMTAwNzM5OTA0NDIsIDU5LjE3NjU3MzQyOTI2NjU5IC0yNS4yMzIwMTIyODk4NTIxNzUsIDU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBNNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4zNjY3MjA5MTA1NTE5OSAtMjQuOTk1NjY4NzQ3OTg3MTQzLCA1OS40MjA1NTE3NzQxMjIwMSAtMjQuOTIwMjczOTMyNTI1OTQ0LCA1OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzNiBNNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4zOTA3NTQwMDY5MjY1NiAtMjQuOTYyMDA4MzAyMTc2ODI3LCA1OS40Njg2MTc5NjY4NzExMzYgLTI0Ljg1Mjk1MzA0MDkwNTMxMiwgNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzYgTTU5LjU1MzAxOTg5MTMxMjY0NCAtMjQuNzM0NzQwNzkwNjEyMTM2IEM1OS42MjMxMzIzMzUwOTU3MSAtMjQuNjE3MDc2OTI4MDEzNDkzLCA1OS42OTMyNDQ3Nzg4Nzg3OCAtMjQuNDk5NDEzMDY1NDE0ODUzLCA1OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzcgTTU5LjU1MzAxOTg5MTMxMjY0NCAtMjQuNzM0NzQwNzkwNjEyMTM2IEM1OS42MDA5NjkxMzYzMTM3NjUgLTI0LjY1NDI3MTU3NDcxOTc3LCA1OS42NDg5MTgzODEzMTQ4OSAtMjQuNTczODAyMzU4ODI3NDA0LCA1OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzcgTTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNyBDNTkuODMzNzk2NTI5MzcwNTQgLTI0LjIzODEwMzcyMjI4MTcxLCA1OS45MDMwMzY4MDI3MDg2MzQgLTI0LjA5NjQ3MDQ3OTM3ODA1NiwgNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBNNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM3IEM1OS44MTI4NTE0MTA5OTIxNiAtMjQuMjgwOTQ3NjQ2NzE5MjQ1LCA1OS44NjExNDY1NjU5NTE4NyAtMjQuMTgyMTU4MzI4MjUzMTIsIDU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgTTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQzU5Ljk5Nzk5OTg5MjYzNjU2IC0yMy44NzUzNTE1NTEzMzMyMjUsIDYwLjA0OTk0NTY1MTk5NzgyNCAtMjMuNzQyMjI1OTc5NDAxNiwgNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgTTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQzU5Ljk5MjQ0NTA2MDg3MDQ3IC0yMy44ODk1ODczNjUwOTIzNTMsIDYwLjAzODgzNTk4ODQ2NTY2NCAtMjMuNzcwNjk3NjA2OTE5ODYsIDYwLjA5NjI3MzcwODUwMzE3NiAtMjMuNjIzNDk3MzQ2MDIzNDE3IE02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMTIzNjI3MTk1OTcxNDE2IC0yMy41MzE2MTg2MDEyMjEwOTgsIDYwLjE1MDk4MDY4MzQzOTY2IC0yMy40Mzk3Mzk4NTY0MTg3OCwgNjAuMjE0MTg4ODI5Njk2NjUgLTIzLjIyNzQyNzQzNTcwMzk5NCBNNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgQzYwLjEzNjgzMTc1MjQ0MDk3NiAtMjMuNDg3MjY1Mjc3ODgzMzI4LCA2MC4xNzczODk3OTYzNzg3OCAtMjMuMzUxMDMzMjA5NzQzMjQsIDYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgTTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQzYwLjIzNjE2MTUzMjk3Nzk0NSAtMjMuMTIyNjM0ODA4NTYwNzQ3LCA2MC4yNTgxMzQyMzYyNTkyNCAtMjMuMDE3ODQyMTgxNDE3NSwgNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IE02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4yMzE0OTkzMTQ3MTY3OCAtMjMuMTQ0ODY5OTQ3MDI1NDQsIDYwLjI0ODgwOTc5OTczNjkgLTIzLjA2MjMxMjQ1ODM0Njg4OCwgNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IE02MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgQzYwLjMxNjY3MzgwODg0NDE4IC0yMi42ODExMzczMjk1MTY1NzcsIDYwLjMzNDM1MzYwMDY3NDc0NiAtMjIuNTM5MzAxNzA3NjI5NDgsIDYwLjM1MDEwOTk2NTAzMzM1IC0yMi40MTI4OTY3MjczNjE2NjIgTTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDNjAuMzEwMzg3NTU5MjA5NiAtMjIuNzMxNTY4NTg2NTU4NjgsIDYwLjMyMTc4MTEwMTQwNTU4NCAtMjIuNjQwMTY0MjIxNzEzNjkzLCA2MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYyIE02MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYyIEM2MC4zNTY3MzA5NDkwMDU5IC0yMi4yNTI4MTYwOTM5NzI5MDcsIDYwLjM2MzM1MTkzMjk3ODQ0NiAtMjIuMDkyNzM1NDYwNTg0MTQ4LCA2MC4zNjcxODc1IC0yMiBNNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzUzOTgyMzQwMjUyODcgLTIyLjMxOTI3MTMyNjYwMDk0LCA2MC4zNTc4NTQ3MTU0NzI0IC0yMi4yMjU2NDU5MjU4NDAyMiwgNjAuMzY3MTg3NSAtMjIgTTYwLjM2NzE4NzUgLTIyIEM2MC4zNjcxODc1IC0yMiwgNjAuMzY3MTg3NSAtMjIsIDYwLjM2NzE4NzUgLTIyIE02MC4zNjcxODc1IC0yMiBDNjAuMzY3MTg3NSAtMjIsIDYwLjM2NzE4NzUgLTIyLCA2MC4zNjcxODc1IC0yMiBNNjAuMzY3MTg3NSAtMjIgQzYwLjM2NzE4NzUgLTEwLjM4OTE3MTUxNDIyNDUzMiwgNjAuMzY3MTg3NSAxLjIyMTY1Njk3MTU1MDkzNTEsIDYwLjM2NzE4NzUgMjIgTTYwLjM2NzE4NzUgLTIyIEM2MC4zNjcxODc1IC0xMC45NTU2NjIwMjIwMzk5OTcsIDYwLjM2NzE4NzUgMC4wODg2NzU5NTU5MjAwMDY1NCwgNjAuMzY3MTg3NSAyMiBNNjAuMzY3MTg3NSAyMiBDNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiBNNjAuMzY3MTg3NSAyMiBDNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiBNNjAuMzY3MTg3NSAyMiBDNjAuMzYyOTg3NDI5NzQ2ODEgMjIuMTAxNTQ4MzM2MTk4MjEzLCA2MC4zNTg3ODczNTk0OTM2MjYgMjIuMjAzMDk2NjcyMzk2NDMsIDYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNNjAuMzY3MTg3NSAyMiBDNjAuMzYzMTk3NDcwMDg2NTA0IDIyLjA5NjQ3MDAyODk5Mjc1LCA2MC4zNTkyMDc0NDAxNzMwMSAyMi4xOTI5NDAwNTc5ODU1MDYsIDYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIEM2MC4zMzAwMTYwODE0MTExOSAyMi41NzQwOTkzMzEyNzE3NCwgNjAuMzA5OTIyMTk3Nzg5MDM1IDIyLjczNTMwMTkzNTE4MTgxMywgNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgTTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzMyNzE3NDc5NDU0ODIgMjIuNTUyNDI3NDQzMDk4MjUsIDYwLjMxNTMyNDk5Mzg3NjI5IDIyLjY5MTk1ODE1ODgzNDg0LCA2MC4yOTg5OTQwMTcwMTM2MSAyMi44MjI5NzI5NTE0MDM2NyBNNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQzYwLjI3NjIzMTM1Mzg3MjA2IDIyLjkzMTUzMzA2OTg3NjI0NSwgNjAuMjUzNDY4NjkwNzMwNTEgMjMuMDQwMDkzMTg4MzQ4ODE2LCA2MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgTTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEM2MC4yNzcyOTI1Nzc2OTc2IDIyLjkyNjQ3MTg2MTM3Mzk3NiwgNjAuMjU1NTkxMTM4MzgxNTggMjMuMDI5OTcwNzcxMzQ0MjgsIDYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBNNjAuMjE0MTg4ODI5Njk2NjUgMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4xNzg5NzE4ODQzMjMyMiAyMy4zNDU3MTkwNzAwMTM0OTQsIDYwLjE0Mzc1NDkzODk0OTc5IDIzLjQ2NDAxMDcwNDMyMjk5LCA2MC4wOTYyNzM3MDg1MDMxNzYgMjMuNjIzNDk3MzQ2MDIzNDE3IE02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQzYwLjE3NzA4NTY1MzM3NzczIDIzLjM1MjA1NDgwODA3NTQyLCA2MC4xMzk5ODI0NzcwNTg4MSAyMy40NzY2ODIxODA0NDY4NDcsIDYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgTTYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQzYwLjA2MzM2NzU4NjA0NTQ2NiAyMy43MDc4Mjg1MTA0MTY4MSwgNjAuMDMwNDYxNDYzNTg3NzU2IDIzLjc5MjE1OTY3NDgxMDIsIDU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBNNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMDQyMTU5NDc4NjU3ODIgMjMuNzYyMTgwMjMxNTI1MiwgNTkuOTg4MDQ1MjQ4ODEyNDcgMjMuOTAwODYzMTE3MDI2OTgzLCA1OS45NDYwNTQxMzMyNzUyOSAyNC4wMDg0NzcxMjMyNjQ4NDYgTTU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuODg5MTA1NTc1NDIyNjEgMjQuMTI0OTY3MjYwMjY5MDM1LCA1OS44MzIxNTcwMTc1Njk5MjYgMjQuMjQxNDU3Mzk3MjczMjIzLCA1OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgTTU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuOTAwNDc0Njg2NTEzNzYgMjQuMTAxNzExMzcxODIyMywgNTkuODU0ODk1MjM5NzUyMjQgMjQuMTk0OTQ1NjIwMzc5NzU1LCA1OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgTTU5Ljc2NDU1NjI1NjAzMjQ1IDI0LjM3OTczNjk2NTE4NTM2NiBDNTkuNzEzNjQxNjIyNTI1NTggMjQuNDY1MTgyNzQ1MTA2MzgyLCA1OS42NjI3MjY5ODkwMTg3MSAyNC41NTA2Mjg1MjUwMjc0LCA1OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIE01OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgQzU5LjY4OTUxMzkzNzYxMDk1NCAyNC41MDU2NzQyMjQ5MjcwMywgNTkuNjE0NDcxNjE5MTg5NDUgMjQuNjMxNjExNDg0NjY4NjkyLCA1OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIE01OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIEM1OS40NzAwNzQ1ODc5ODEzOCAyNC44NTA5MTI5MTYxMDQ3NzUsIDU5LjM4NzEyOTI4NDY1MDExIDI0Ljk2NzA4NTA0MTU5NzQyLCA1OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBNNTkuNTUzMDE5ODkxMzEyNjQ0IDI0LjczNDc0MDc5MDYxMjEzMyBDNTkuNDg3MTkzNjE4OTYzNTY2IDI0LjgyNjkzNjIyMTM5NTA5OCwgNTkuNDIxMzY3MzQ2NjE0NDkgMjQuOTE5MTMxNjUyMTc4MDY2LCA1OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBNNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQzU5LjI1NzY2OTMyNDY0ODMyNiAyNS4xMzYyNjI1NDcxMDYwNTMsIDU5LjIwMjQ0ODYwMjMxNDY4IDI1LjIwMTQ2MTUzMDc2Mzc2NSwgNTkuMDQ1ODA3MDUzMzY1NjYgMjUuMzg2NDA3ODU4MTI4NzA2IE01OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBDNTkuMjExMjI3MzQzNTcyODIgMjUuMTkxMDk2NDkwNTQ2MzIsIDU5LjEwOTU2NDY0MDE2MzY3NiAyNS4zMTExMjk0MTc2NDQzLCA1OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgTTU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBDNTguOTM4NzA5MzQ0NzMyODMgMjUuNDkzNTA1NTY2NzYxNTM0LCA1OC44MzE2MTE2MzYxIDI1LjYwMDYwMzI3NTM5NDM2MywgNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBNNTkuMDQ1ODA3MDUzMzY1NjYgMjUuMzg2NDA3ODU4MTI4NzA2IEM1OC45MzYzMjM5ODExODM2OSAyNS40OTU4OTA5MzAzMTA2NzIsIDU4LjgyNjg0MDkwOTAwMTcyNCAyNS42MDUzNzQwMDI0OTI2NCwgNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBNNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBDNTguNjc1NDMwMTIwNzQzMzIgMjUuNzQ0ODIyMTMyNDE5MDYzLCA1OC41OTcyNjQ4ODMzNTc5NCAyNS44MTEwMjQ3MTE0NzI0NzQsIDU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IE01OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjU3IEM1OC42Njk3NTAwNDg2MjYxOCAyNS43NDk2MzI5MDgxMDMyOCwgNTguNTg1OTA0NzM5MTIzNjQgMjUuODIwNjQ2MjYyODQwOTAzLCA1OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBNNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjM1ODc4NjE5NjMyNjY3IDI2LjAwMjQzOTM3MTU4MjgzNSwgNTguMjc5MzIxMzI5MjA1IDI2LjA1OTE3NjE5NjE4MzcwMywgNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBNNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjM2MDgyODk3NTc0MzgzNiAyNi4wMDA5ODA4NTUxMTI3NSwgNTguMjgzNDA2ODg4MDM5MzMgMjYuMDU2MjU5MTYzMjQzNTM1LCA1OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IE01OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IEM1Ny45NzM1NTYxMjY4OTg1NSAyNi4yNjIzMjU1OTc1Nzc1MSwgNTcuODQ1MTgzOTYzMTg0OTY1IDI2LjMzODgxODgwMzg0MjM4MywgNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBNNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMDI3NTc5OTQ3MDEzMzEgMjYuMjMwMTM0Mzg4MDgzODgzLCA1Ny45NTMyMzE2MDM0MTQ0OCAyNi4yNzQ0MzYzODQ4NTUxMjUsIDU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgTTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgQzU3LjYyMDQxMDYxMDE0MzQ0IDI2LjQ1OTIxNzYxMTAzNjg5OCwgNTcuNDkzODk2NzU1MTAxNTIgMjYuNTIxMDY2NDY2MDQxMzUsIDU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgTTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgQzU3LjY0MjUxNzk3NDcwMTAyIDI2LjQ0ODQwOTk3OTA2MTczMywgNTcuNTM4MTExNDg0MjE2NjggMjYuNDk5NDUxMjAyMDkxMDIsIDU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgTTU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgQzU3LjI4NTE1MjMyMzM5MjcxIDI2LjYxNDE4NDY0MjI4MTE2LCA1Ny4xOTQ2NDAwMjM1MjA1NyAyNi42NDk1MDI2NTEyODcwMzMsIDU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBNNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuMjg2NDI3OTE5MjY2NzkgMjYuNjEzNjg2OTAzMjI2NTUsIDU3LjE5NzE5MTIxNTI2ODc0IDI2LjY0ODUwNzE3MzE3NzgxNiwgNTYuOTkwNjg0ODQ2MDIzNDIgMjYuNzI5MDg2MjA4NTAzMTczIE01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQzU2Ljg4NDIzMDIxMDE5OTY0IDI2Ljc2MDc3OTEyNjc0MjM5MiwgNTYuNzc3Nzc1NTc0Mzc1ODY2IDI2Ljc5MjQ3MjA0NDk4MTYxLCA1Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQzU2Ljg3NzI1NTQyMTI0MTQxNSAyNi43NjI4NTU2MTEzODI4MDgsIDU2Ljc2MzgyNTk5NjQ1OTQxIDI2Ljc5NjYyNTAxNDI2MjQ0LCA1Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIEM1Ni41MDYxNTUwMjAwMDczIDI2Ljg2NTU0OTQyMzUxNjYsIDU2LjQxNzY5NTEwNDMxMDYgMjYuODg0MDk3NTE3MzM2NTQ2LCA1Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgTTU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgQzU2LjQ3ODc1NDU5NDg0Nzk3IDI2Ljg3MTI5NDY4ODUwNTM3LCA1Ni4zNjI4OTQyNTM5OTE5NCAyNi44OTU1ODgwNDczMTQwODUsIDU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBNNTYuMTkwMTYwNDUxNDAzNjcgMjYuOTMxODA2NTE3MDEzNjEyIEM1Ni4wNDg4Njg5MjU1Mzg2MzUgMjYuOTQ5NDE4NDg3MzQ1NzYsIDU1LjkwNzU3NzM5OTY3MzU5IDI2Ljk2NzAzMDQ1NzY3NzkwNCwgNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IE01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQzU2LjA3Nzk5NTA0Mzk5MzAyIDI2Ljk0NTc4NzkyMDQ2NjYxNCwgNTUuOTY1ODI5NjM2NTgyMzggMjYuOTU5NzY5MzIzOTE5NjE2LCA1NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgTTU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBDNTUuNjgwNTI4MDE2NjgyNzg2IDI2Ljk4NzA0MDE0MDM2NTU4LCA1NS41ODA5NzE4MDYwMDM5MSAyNi45OTExNTc4MTU2OTc4MDcsIDU1LjM2NzE4NzUgMjcgTTU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBDNTUuNjgyMjQ3Nzc4OTM5NjMgMjYuOTg2OTY5MDEwNDcyNDg1LCA1NS41ODQ0MTEzMzA1MTc2IDI2Ljk5MTAxNTU1NTkxMTYyMywgNTUuMzY3MTg3NSAyNyBNNTUuMzY3MTg3NSAyNyBDNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNyBNNTUuMzY3MTg3NSAyNyBDNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNyBNNTUuMzY3MTg3NSAyNyBDMTEuMzI3ODIxNDU3NTYzMjEgMjcsIC0zMi43MTE1NDQ1ODQ4NzM1OCAyNywgLTU1LjM2NzE4NzUgMjcgTTU1LjM2NzE4NzUgMjcgQzI3LjkyOTA3NDcyNTYwMTY5NCAyNywgMC40OTA5NjE5NTEyMDMzODg1IDI3LCAtNTUuMzY3MTg3NSAyNyBNLTU1LjM2NzE4NzUgMjcgQy01NS4zNjcxODc1IDI3LCAtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcgTS01NS4zNjcxODc1IDI3IEMtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcsIC01NS4zNjcxODc1IDI3IE0tNTUuMzY3MTg3NSAyNyBDLTU1LjUwNjM2NTY3MjAzNTM3IDI2Ljk5NDI0MzU0ODIyMzk5LCAtNTUuNjQ1NTQzODQ0MDcwNzQ0IDI2Ljk4ODQ4NzA5NjQ0Nzk4MywgLTU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBNLTU1LjM2NzE4NzUgMjcgQy01NS40NTg4OTU3Mzg3MDc1OSAyNi45OTYyMDY5MTkxODk1MzYsIC01NS41NTA2MDM5Nzc0MTUxOCAyNi45OTI0MTM4MzgzNzkwNzIsIC01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgTS01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgQy01NS45MDE2Nzc1Nzc2MzU5ODYgMjYuOTY3NzY1ODY5NzI3OTU1LCAtNTYuMDIzMjcwOTI3OTEwMzEgMjYuOTUyNjA5Mjc0NDIyNTY0LCAtNTYuMTkwMTYwNDUxNDAzNjcgMjYuOTMxODA2NTE3MDEzNjEyIE0tNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuOTAwOTczNjAzOTE5NTUgMjYuOTY3ODUzNjE5OTYwMiwgLTU2LjAyMTg2Mjk4MDQ3NzQ1IDI2Ljk1Mjc4NDc3NDg4NzA0NiwgLTU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBNLTU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBDLTU2LjMxNTY0MTA5MTE2OTA0IDI2LjkwNTQ5NTk5MzY0NjkwMiwgLTU2LjQ0MTEyMTczMDkzNDQyIDI2Ljg3OTE4NTQ3MDI4MDE5LCAtNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBNLTU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBDLTU2LjMxNDc1NTQ3Mzg3OTQ1IDI2LjkwNTY4MTY4ODA2NTA2MiwgLTU2LjQzOTM1MDQ5NjM1NTIzIDI2Ljg3OTU1Njg1OTExNjUxNiwgLTU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgTS01Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNjg3MDE1MzM3MzM2Mzc2IDI2LjgxOTQ5MjUzODI2MTI3LCAtNTYuNzc5NDE1NzM4OTY4NzUgMjYuNzkxOTgzNzQ2ODI1ODg1LCAtNTYuOTkwNjg0ODQ2MDIzNDIgMjYuNzI5MDg2MjA4NTAzMTczIE0tNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBDLTU2LjcyNTE2MDU2ODg2MDQ3IDI2LjgwODEzNjIxMDgwMjU5LCAtNTYuODU1NzA2MjAyMDE2OTQgMjYuNzY5MjcxMDkxOTA4NTMsIC01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgTS01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQy01Ny4wOTcxODQ2Mjk0Njc2NiAyNi42ODc1Mjk4NjQwNDkyMDcsIC01Ny4yMDM2ODQ0MTI5MTE5IDI2LjY0NTk3MzUxOTU5NTI0LCAtNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBNLTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDLTU3LjEwNTEzODQ2NjY2ODc5IDI2LjY4NDQyNjI2NzE0MjUxMywgLTU3LjIxOTU5MjA4NzMxNDE3IDI2LjYzOTc2NjMyNTc4MTg1NiwgLTU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgTS01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNDcxNTI4MzU1NDc4OTggMjYuNTMyMDAxNzEwMjgzNzg3LCAtNTcuNTY3MzkyMDg3NjkzMTE1IDI2LjQ4NTEzNjc4NzI5MjI4NywgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgTS01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNTE2ODAxODczNjk3NTk1IDI2LjUwOTg2ODgzNTg4MTA4NiwgLTU3LjY1NzkzOTEyNDEzMDM0NCAyNi40NDA4NzEwMzg0ODY4ODMsIC01Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBDLTU3Ljg1MzU5NjQ4MzIwMjM0NSAyNi4zMzM4MDYwMjk5NjQ2MDYsIC01Ny45NjAyNjg1MDEyMTkzMiAyNi4yNzAyNDMzMDM4OTY3NywgLTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgTS01Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IEMtNTcuODgwNTI4MDEwMTg2MjEgMjYuMzE3NzU4MzIzMTMzMDc3LCAtNTguMDE0MTMxNTU1MTg3MDUgMjYuMjM4MTQ3ODkwMjMzNzEyLCAtNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBNLTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgQy01OC4yMTQ4OTAxOTE0OTQ5NiAyNi4xMDUxNzkxNDQyOTc3NiwgLTU4LjMyNzg1MjA5MjM3Nzc4IDI2LjAyNDUyNTg5NzI4Mjg3NCwgLTU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IE0tNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBDLTU4LjE4OTIyNDkxNjM4MjUyIDI2LjEyMzUwMzc5ODUxNDU0NSwgLTU4LjI3NjUyMTU0MjE1MjkgMjYuMDYxMTc1MjA1NzE2NDQ1LCAtNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgTS01OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBDLTU4LjUwNzEyNzYxOTg1MTQ0IDI1Ljg4NzM2NzA3OTQxNzAxLCAtNTguNTc2MDA0MTc2MjU0NTM2IDI1LjgyOTAzMTYxMTg1MjA1MiwgLTU4Ljc1MzU5NTM1ODEyODcwNiAyNS42Nzg2MTk1NTMzNjU2NiBNLTU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IEMtNTguNTAyMzA5MTcwNTg4MDIgMjUuODkxNDQ4MDk3NzY5MjQyLCAtNTguNTY2MzY3Mjc3NzI3NjkgMjUuODM3MTkzNjQ4NTU2NTE0LCAtNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IE0tNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IEMtNTguODQ5MTc1MzE1MTMwNzEgMjUuNTgzMDM5NTk2MzYzNjU1LCAtNTguOTQ0NzU1MjcyMTMyNzIgMjUuNDg3NDU5NjM5MzYxNjUsIC01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgTS01OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjYgQy01OC44Mjc0MzgwNDE3MjIwOSAyNS42MDQ3NzY4Njk3NzIyNzcsIC01OC45MDEyODA3MjUzMTU0NyAyNS41MzA5MzQxODYxNzg4OTMsIC01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgTS01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQy01OS4xMjI2OTYyMDk4NzQwMSAyNS4yOTU2MjUwMDI1ODQ4MywgLTU5LjE5OTU4NTM2NjM4MjM2NSAyNS4yMDQ4NDIxNDcwNDA5NiwgLTU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IE0tNTkuMDQ1ODA3MDUzMzY1NjYgMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTkuMTQxNjE4Nzc4ODUzOTc0IDI1LjI3MzI4MzE2NzYwNzg4LCAtNTkuMjM3NDMwNTA0MzQyMjkgMjUuMTYwMTU4NDc3MDg3MDQ4LCAtNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgTS01OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBDLTU5LjM3NTI5MjQ2ODA4Nzc0IDI0Ljk4MzY2MzUzNDY5OTM3LCAtNTkuNDM3Njk0ODg5MTkzNTIgMjQuODk2MjYzNTA1OTUwMzk3LCAtNTkuNTUzMDE5ODkxMzEyNjQ0IDI0LjczNDc0MDc5MDYxMjEzMyBNLTU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IEMtNTkuMzk3MTgzNjMwNjgwNjcgMjQuOTUzMDAzMDUzODA2Njk3LCAtNTkuNDgxNDc3MjE0Mzc5Mzc1IDI0LjgzNDk0MjU0NDE2NTA1MiwgLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgTS01OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIEMtNTkuNjI4NTA1MjE3NzY2NzQgMjQuNjA4MDYwMDY3NDY0MTg3LCAtNTkuNzAzOTkwNTQ0MjIwODMgMjQuNDgxMzc5MzQ0MzE2MjQsIC01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBNLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQy01OS42MzQxNTIxMDU2ODMzMiAyNC41OTg1ODMzNjY3ODA2MDIsIC01OS43MTUyODQzMjAwNTM5OTYgMjQuNDYyNDI1OTQyOTQ5MDc1LCAtNTkuNzY0NTU2MjU2MDMyNDQgMjQuMzc5NzM2OTY1MTg1MzcgTS01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBDLTU5LjgxNTk2MzgwNzkyMTE2NSAyNC4yNzQ1ODExMzcwNDIwNCwgLTU5Ljg2NzM3MTM1OTgwOTg4IDI0LjE2OTQyNTMwODg5ODcxMywgLTU5Ljk0NjA1NDEzMzI3NTI4IDI0LjAwODQ3NzEyMzI2NDg1IE0tNTkuNzY0NTU2MjU2MDMyNDQgMjQuMzc5NzM2OTY1MTg1MzcgQy01OS44MTE1MjQyNDQyMzQzMDYgMjQuMjgzNjYyNDA5NzEzNzUyLCAtNTkuODU4NDkyMjMyNDM2MTYgMjQuMTg3NTg3ODU0MjQyMTM1LCAtNTkuOTQ2MDU0MTMzMjc1MjggMjQuMDA4NDc3MTIzMjY0ODUgTS01OS45NDYwNTQxMzMyNzUyOCAyNC4wMDg0NzcxMjMyNjQ4NSBDLTU5Ljk4NzUxMTgzNjA5NDk2IDIzLjkwMjIzMDEzNjY2Mzc1LCAtNjAuMDI4OTY5NTM4OTE0NjQgMjMuNzk1OTgzMTUwMDYyNjU0LCAtNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBNLTU5Ljk0NjA1NDEzMzI3NTI4IDI0LjAwODQ3NzEyMzI2NDg1IEMtNTkuOTgwMzc3NjQ0OTM4NDY0IDIzLjkyMDUxMzUwMTY1MTY5OCwgLTYwLjAxNDcwMTE1NjYwMTY0IDIzLjgzMjU0OTg4MDAzODU1LCAtNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBNLTYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQy02MC4xMzY4Mzk3NTE1MDkyIDIzLjQ4NzIzODQwOTQ4NjgwMywgLTYwLjE3NzQwNTc5NDUxNTIyIDIzLjM1MDk3OTQ3Mjk1MDE5LCAtNjAuMjE0MTg4ODI5Njk2NjUgMjMuMjI3NDI3NDM1NzAzOTk0IE0tNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBDLTYwLjEyOTQwMDYxMjk3NTg1IDIzLjUxMjIyNjAzNTM0Nzk2NSwgLTYwLjE2MjUyNzUxNzQ0ODUzIDIzLjQwMDk1NDcyNDY3MjUxMywgLTYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBNLTYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBDLTYwLjIzNDk5NDY3NTE0NjMzIDIzLjEyODE5OTgwODczOTAxLCAtNjAuMjU1ODAwNTIwNTk2MDEgMjMuMDI4OTcyMTgxNzc0MDIzLCAtNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgTS02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4yMzI0Mjg1MDMwNzQ3MSAyMy4xNDA0Mzg0NDQ1NDI5NCwgLTYwLjI1MDY2ODE3NjQ1Mjc1NiAyMy4wNTM0NDk0NTMzODE4ODQsIC02MC4yOTg5OTQwMTcwMTM2MSAyMi44MjI5NzI5NTE0MDM2NyBNLTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMzE3MTQyNzY3NDEwNDUgMjIuNjc3Mzc1MTIyODk1MzM4LCAtNjAuMzM1MjkxNTE3ODA3MyAyMi41MzE3NzcyOTQzODcwMSwgLTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNLTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMzE4MjcyODkwMjUxNDYgMjIuNjY4MzA4NzQ0ODgwMzYsIC02MC4zMzc1NTE3NjM0ODkyOTQgMjIuNTEzNjQ0NTM4MzU3MDUsIC02MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgTS02MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgQy02MC4zNTQ0NjI2NDExMjkwMjUgMjIuMzA3NjU4NzIyMDcxMDU3LCAtNjAuMzU4ODE1MzE3MjI0NyAyMi4yMDI0MjA3MTY3ODA0NTUsIC02MC4zNjcxODc1IDIyIE0tNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIEMtNjAuMzU1OTI0NTY4NjAxNjcgMjIuMjcyMzEyNTc0NjE2MjIsIC02MC4zNjE3MzkxNzIxNyAyMi4xMzE3Mjg0MjE4NzA3OCwgLTYwLjM2NzE4NzUgMjIgTS02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSAyMiwgLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyIE0tNjAuMzY3MTg3NSAyMiBDLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyLCAtNjAuMzY3MTg3NSAyMiBNLTYwLjM2NzE4NzUgMjIgQy02MC4zNjcxODc1IDUuMjA5NjMxOTkzOTAxNzg3LCAtNjAuMzY3MTg3NSAtMTEuNTgwNzM2MDEyMTk2NDI1LCAtNjAuMzY3MTg3NSAtMjIgTS02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSA1LjM3NDAwMTYwMTcxOTE0NCwgLTYwLjM2NzE4NzUgLTExLjI1MTk5Njc5NjU2MTcxMywgLTYwLjM2NzE4NzUgLTIyIE0tNjAuMzY3MTg3NSAtMjIgQy02MC4zNjcxODc1IC0yMiwgLTYwLjM2NzE4NzUgLTIyLCAtNjAuMzY3MTg3NSAtMjIgTS02MC4zNjcxODc1IC0yMiBDLTYwLjM2NzE4NzUgLTIyLCAtNjAuMzY3MTg3NSAtMjIsIC02MC4zNjcxODc1IC0yMiBNLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYzNjY5MjAwNjY1OTUgLTIyLjA4NTA2NDYzNTEzMjkxNiwgLTYwLjM2MDE1MDkwMTMzMTkgLTIyLjE3MDEyOTI3MDI2NTgzNiwgLTYwLjM1MDEwOTk2NTAzMzM1IC0yMi40MTI4OTY3MjczNjE2NiBNLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYzNTM4MjU1NzU1MDk1IC0yMi4wODgyMzA1OTExODM0NDYsIC02MC4zNTk4ODkwMTE1MTAxOSAtMjIuMTc2NDYxMTgyMzY2ODkzLCAtNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IE0tNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IEMtNjAuMzM5NDk4MzUwMzU0NDMgLTIyLjQ5ODAyODEwMTE3MTYsIC02MC4zMjg4ODY3MzU2NzU1MiAtMjIuNTgzMTU5NDc0OTgxNTQsIC02MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgTS02MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYgQy02MC4zMzIyMTI0MjYzNTk4NzQgLTIyLjU1NjQ3OTIxNzAzODQ1OCwgLTYwLjMxNDMxNDg4NzY4NjQgLTIyLjcwMDA2MTcwNjcxNTI1NywgLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBNLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDLTYwLjI4MDc4NTQ4MjM1NjQ4NSAtMjIuOTA5ODEzNDM1MTk0OTMsIC02MC4yNjI1NzY5NDc2OTkzNiAtMjIuOTk2NjUzOTE4OTg2MTk0LCAtNjAuMjE0MTg4ODI5Njk2NjUgLTIzLjIyNzQyNzQzNTcwMzk5NCBNLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDLTYwLjI3NzUwMDA0MTE5MzMzNSAtMjIuOTI1NDgyNDIyNTkyNzc2LCAtNjAuMjU2MDA2MDY1MzczMDYgLTIzLjAyNzk5MTg5Mzc4MTg3NywgLTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgTS02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEMtNjAuMTc3OTEwMTYyODQ1NTMgLTIzLjM0OTI4NTMyOTU5MjM5NCwgLTYwLjE0MTYzMTQ5NTk5NDQxIC0yMy40NzExNDMyMjM0ODA3OTQsIC02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBNLTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4xNzE0MTMxNjc3OTUwMSAtMjMuMzcxMTA4MzUxMjc5MTksIC02MC4xMjg2Mzc1MDU4OTMzNiAtMjMuNTE0Nzg5MjY2ODU0Mzg2LCAtNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgTS02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDLTYwLjA0MzQ4Nzg1NjM4Nzk1IC0yMy43NTg3NzU4OTExNjYzNjIsIC01OS45OTA3MDIwMDQyNzI3MiAtMjMuODk0MDU0NDM2MzA5MzA3LCAtNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBNLTYwLjA5NjI3MzcwODUwMzE3NiAtMjMuNjIzNDk3MzQ2MDIzNDE3IEMtNjAuMDQ2Nzc2ODI4OTExNCAtMjMuNzUwMzQ2OTc2OTc1MTgsIC01OS45OTcyNzk5NDkzMTk2MiAtMjMuODc3MTk2NjA3OTI2OTQ4LCAtNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBNLTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQy01OS44ODA5NTcwOTAwOTc4NDQgLTI0LjE0MTYzNTI1MzQxMzE5NSwgLTU5LjgxNTg2MDA0NjkyMDQgLTI0LjI3NDc5MzM4MzU2MTU0OCwgLTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNjYgTS01OS45NDYwNTQxMzMyNzUyOSAtMjQuMDA4NDc3MTIzMjY0ODQ2IEMtNTkuODgzOTMyMDkxMTM3NzIgLTI0LjEzNTU0OTc5MTUxOTMyOCwgLTU5LjgyMTgxMDA0OTAwMDE2IC0yNC4yNjI2MjI0NTk3NzM4MDUsIC01OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzY2IE0tNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM2NiBDLTU5LjcwMTIzNDY5OTYwMzM4IC0yNC40ODYwMDQyNDgzMjYzNiwgLTU5LjYzNzkxMzE0MzE3NDMxNiAtMjQuNTkyMjcxNTMxNDY3MzUsIC01OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzMyBNLTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNjYgQy01OS43MDg2OTYxNDMzNjY2NiAtMjQuNDczNDgyMzMwMDEzNzM4LCAtNTkuNjUyODM2MDMwNzAwODcgLTI0LjU2NzIyNzY5NDg0MjExLCAtNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzMgTS01OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzMyBDLTU5LjUwNDYwNzU5MTM5NDYyNCAtMjQuODAyNTQ2NDM1NDgzOTMzLCAtNTkuNDU2MTk1MjkxNDc2NjA0IC0yNC44NzAzNTIwODAzNTU3MywgLTU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBNLTU5LjU1MzAxOTg5MTMxMjY0NCAtMjQuNzM0NzQwNzkwNjEyMTMzIEMtNTkuNDYxNzM5MzQwODUyMjEgLTI0Ljg2MjU4NzE1NjA3NTExLCAtNTkuMzcwNDU4NzkwMzkxNzggLTI0Ljk5MDQzMzUyMTUzODA5LCAtNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IE0tNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEMtNTkuMjE4MTkwNTgwNzAyODc0IC0yNS4xODI4NzUwMTIwMDIzNzMsIC01OS4xMjM0OTExMTQ0MjM3ODYgLTI1LjI5NDY4NjQ2MDU1NjQwNiwgLTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgTS01OS4zMTI4OTAwNDY5ODE5NyAtMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4yMjI2MDEwMDQ0NjUyMiAtMjUuMTc3NjY3NjM0NTA2MTY4LCAtNTkuMTMyMzExOTYxOTQ4NDcgLTI1LjI4NDI3MTcwNTU2NCwgLTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgTS01OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTguOTMxNzg1Njk1MjEzNzU1IC0yNS41MDA0MjkyMTYyODA2MDgsIC01OC44MTc3NjQzMzcwNjE4NSAtMjUuNjE0NDUwNTc0NDMyNTE0LCAtNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgTS01OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTguOTcwMTE5MjM1NDczNjYgLTI1LjQ2MjA5NTY3NjAyMDcwNiwgLTU4Ljg5NDQzMTQxNzU4MTY2IC0yNS41Mzc3ODM0OTM5MTI3MDIsIC01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBNLTU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IEMtNTguNjgyMjg2MDcwNDgyNzYgLTI1LjczOTAxNTQzOTE5MDgsIC01OC42MTA5NzY3ODI4MzY4MTUgLTI1Ljc5OTQxMTMyNTAxNTk0NiwgLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgTS01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBDLTU4LjYzMjY3OTcwNTA1MjQ0NiAtMjUuNzgxMDI5ODg3NTQxNTUsIC01OC41MTE3NjQwNTE5NzYxOSAtMjUuODgzNDQwMjIxNzE3NDQ2LCAtNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk2NiBNLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgQy01OC4zMTQxNDQyNzEwMDEzNTQgLTI2LjAzNDMxMzA5MzYyODQ0NSwgLTU4LjE5MDAzNzQ3ODU1NDM2IC0yNi4xMjI5MjM2NDAyNzQ5MjMsIC01OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBNLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgQy01OC4zMjgzMTEyOTA1MjI5NSAtMjYuMDI0MTk4MDM2MTA4OTYsIC01OC4yMTgzNzE1MTc1OTc1NTQgLTI2LjEwMjY5MzUyNTIzNTk1MywgLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IE0tNTguMTAxOTI4MjkwNjEyMTM2IC0yNi4xODU4MzIzOTEzMTI2NDQgQy01Ny45NjYxNjgzOTcwNzc4NjUgLTI2LjI2NjcyNzcyOTA0MTY0OCwgLTU3LjgzMDQwODUwMzU0MzU5NSAtMjYuMzQ3NjIzMDY2NzcwNjUsIC01Ny43NDY5MjQ0NjUxODUzNjYgLTI2LjM5NzM2ODc1NjAzMjQ0NiBNLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTguMDA3NzU4MjQzMjkyODYgLTI2LjI0MTk0NTU1OTgxNTcyNiwgLTU3LjkxMzU4ODE5NTk3MzU4IC0yNi4yOTgwNTg3MjgzMTg4MSwgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MjExMjgwMDk0NTA2NCAtMjYuNDU4ODY2ODk1ODg2MjQsIC01Ny40OTUzMzE1NTM3MTU5MTQgLTI2LjUyMDM2NTAzNTc0MDAzNSwgLTU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MDY4ODkwODc4NTgyOCAtMjYuNDY1ODI3ODgwNDg5MDIzLCAtNTcuNDY2ODUzNzEwNTMxMTkgLTI2LjUzNDI4NzAwNDk0NTYsIC01Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBNLTU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuMjIzODIwNDkwODE4MzYgLTI2LjYzODExNjM5NzYxNDYwMywgLTU3LjA3MTk3NjM1ODM3MTg4IC0yNi42OTczNjYxNjE5NTM5MjMsIC01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIE0tNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQy01Ny4yNDQ5MzM4NDM5MDY1OCAtMjYuNjI5ODc3OTQxNjc0NzI0LCAtNTcuMTE0MjAzMDY0NTQ4MzEgLTI2LjY4MDg4OTI1MDA3NDE2MywgLTU2Ljk5MDY4NDg0NjAyMzQyIC0yNi43MjkwODYyMDg1MDMxNzMgTS01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEMtNTYuODc5MjE4NjQ4MDM0OTMgLTI2Ljc2MjI3MTEzMzQ0Mjc4OCwgLTU2Ljc2Nzc1MjQ1MDA0NjQ0IC0yNi43OTU0NTYwNTgzODI0LCAtNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgTS01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEMtNTYuODkzMDQzMzQ2MTkwNTk1IC0yNi43NTgxNTUzNDI0Nzc1NjUsIC01Ni43OTU0MDE4NDYzNTc3NiAtMjYuNzg3MjI0NDc2NDUxOTU3LCAtNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgTS01Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBDLTU2LjQ0NjYxODI5MjY5MDU3IC0yNi44NzgwMzI5NjI0NzY2NiwgLTU2LjI5ODYyMTY0OTY3NzE0NSAtMjYuOTA5MDY0NTk1MjU2NjY2LCAtNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBNLTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNDQ2NjE3NzEzOTc0NjkgLTI2Ljg3ODAzMzA4MzgyMDYyLCAtNTYuMjk4NjIwNDkyMjQ1MzkgLTI2LjkwOTA2NDgzNzk0NDU4NCwgLTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgTS01Ni4xOTAxNjA0NTE0MDM2NyAtMjYuOTMxODA2NTE3MDEzNjEyIEMtNTYuMDgxMzQxNzUzNTI1MjIgLTI2Ljk0NTM3MDc1MzU1ODQ3NSwgLTU1Ljk3MjUyMzA1NTY0Njc4IC0yNi45NTg5MzQ5OTAxMDMzMzcsIC01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IE0tNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBDLTU2LjA4NzE2NTYxNzkwMDI3NiAtMjYuOTQ0NjQ0ODA5NjIxMTg4LCAtNTUuOTg0MTcwNzg0Mzk2ODggLTI2Ljk1NzQ4MzEwMjIyODc2NCwgLTU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgTS01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuNjYwMjczMzEzNzI0MjQgLTI2Ljk4Nzg3Nzg4MTA3NjczNCwgLTU1LjU0MDQ2MjQwMDA4NjgyNCAtMjYuOTkyODMzMjk3MTIwMTE4LCAtNTUuMzY3MTg3NSAtMjcgTS01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuNjg0ODA4ODg5NjgwMDg1IC0yNi45ODY4NjMwODIxNDg3MDUsIC01NS41ODk1MzM1NTE5OTg1MTQgLTI2Ljk5MDgwMzY5OTI2NDA2MywgLTU1LjM2NzE4NzUgLTI3IE0tNTUuMzY3MTg3NSAtMjcgQy01NS4zNjcxODc1IC0yNywgLTU1LjM2NzE4NzUgLTI3LCAtNTUuMzY3MTg3NSAtMjcgTS01NS4zNjcxODc1IC0yNyBDLTU1LjM2NzE4NzUgLTI3LCAtNTUuMzY3MTg3NSAtMjcsIC01NS4zNjcxODc1IC0yNyIgc3Ryb2tlPSIjOTM3MERCIiBzdHJva2Utd2lkdGg9IjEuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLWRhc2hhcnJheT0iMCAwIiBzdHlsZT0iIi8+PC9nPjxnIGNsYXNzPSJsYWJlbCIgc3R5bGU9IiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQ1LjM2NzE4NzUsIC0xMikiPjxyZWN0Lz48Zm9yZWlnbk9iamVjdCB3aWR0aD0iOTAuNzM0Mzc1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9Im5vZGVMYWJlbCAiPjxwPkdvIHNob3BwaW5nPC9wPjwvc3Bhbj48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9nPjwvZz48ZyBjbGFzcz0ibm9kZSBkZWZhdWx0ICAiIGlkPSJmbG93Y2hhcnQtQy0zIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMjEuODUxNTYyNSwgMzEwLjU3ODEyNSkiPjxwb2x5Z29uIHBvaW50cz0iNzAuNTc4MTI1LDAgMTQxLjE1NjI1LC03MC41NzgxMjUgNzAuNTc4MTI1LC0xNDEuMTU2MjUgMCwtNzAuNTc4MTI1IiBjbGFzcz0ibGFiZWwtY29udGFpbmVyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNzAuMDc4MTI1LCA3MC41NzgxMjUpIi8+PGcgY2xhc3M9ImxhYmVsIiBzdHlsZT0iIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDMuNTc4MTI1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9Ijg3LjE1NjI1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9Im5vZGVMYWJlbCAiPjxwPkxldCBtZSB0aGluazwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCAgIiBpZD0iZmxvd2NoYXJ0LUQtNSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjIuNDY4NzUsIDQ4Mi4xNTYyNSkiPjxyZWN0IGNsYXNzPSJiYXNpYyBsYWJlbC1jb250YWluZXIiIHN0eWxlPSIiIHg9Ii01NC40Njg3NSIgeT0iLTI3IiB3aWR0aD0iMTA4LjkzNzUiIGhlaWdodD0iNTQiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNC40Njg3NSwgLTEyKSI+PHJlY3QvPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI0OC45Mzc1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9Im5vZGVMYWJlbCAiPjxwPkxhcHRvcDwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCAgIiBpZD0iZmxvd2NoYXJ0LUUtNyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIxLjg1MTU2MjUsIDQ4Mi4xNTYyNSkiPjxyZWN0IGNsYXNzPSJiYXNpYyBsYWJlbC1jb250YWluZXIiIHN0eWxlPSIiIHg9Ii01NC45MTQwNjI1IiB5PSItMjciIHdpZHRoPSIxMDkuODI4MTI1IiBoZWlnaHQ9IjU0Ii8+PGcgY2xhc3M9ImxhYmVsIiBzdHlsZT0iIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjQuOTE0MDYyNSwgLTEyKSI+PHJlY3QvPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI0OS44MjgxMjUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0ibm9kZUxhYmVsICI+PHA+aVBob25lPC9wPjwvc3Bhbj48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9nPjwvZz48ZyBjbGFzcz0ibm9kZSBkZWZhdWx0ICAiIGlkPSJmbG93Y2hhcnQtRi05IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNzkuODgyODEyNSwgNDgyLjE1NjI1KSI+PHJlY3QgY2xhc3M9ImJhc2ljIGxhYmVsLWNvbnRhaW5lciIgc3R5bGU9IiIgeD0iLTUzLjExNzE4NzUiIHk9Ii0yNyIgd2lkdGg9IjEwNi4yMzQzNzUiIGhlaWdodD0iNTQiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMy4xMTcxODc1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjQ2LjIzNDM3NSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJub2RlTGFiZWwgIj48cD48aSBjbGFzcz0iZmEgZmEtY2FyIj48L2k+IENhcjwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PC9nPjwvZz48L2c+PC9zdmc+", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); + } + @Test public void importDiagram_WhenTheMermaidUrlIsNotDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java index ea21bf2aa..0de289d83 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java @@ -2,6 +2,7 @@ import com.structurizr.Workspace; import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -52,6 +53,20 @@ public void importDiagram_AsPNG() throws Exception { assertEquals("image/png", view.getContentType()); } + @Test + @Tag("IntegrationTest") + public void importDiagram_AsInlinePNG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_FORMAT_PROPERTY, "png"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_INLINE_PROPERTY, "true"); + ImageView view = workspace.getViews().createImageView("key"); + + new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("image/png", view.getContentType()); + } + @Test public void importDiagram_AsSVG() throws Exception { Workspace workspace = new Workspace("Name", "Description"); @@ -64,6 +79,20 @@ public void importDiagram_AsSVG() throws Exception { assertEquals("image/svg+xml", view.getContentType()); } + @Test + @Tag("IntegrationTest") + public void importDiagram_AsInlineSVG() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_FORMAT_PROPERTY, "svg"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_INLINE_PROPERTY, "true"); + ImageView view = workspace.getViews().createImageView("key"); + + new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + assertEquals("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IlNFUVVFTkNFIiBoZWlnaHQ9IjE1OHB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6MjQ2cHg7aGVpZ2h0OjE1OHB4O2JhY2tncm91bmQ6I0ZGRkZGRjsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0NiAxNTgiIHdpZHRoPSIyNDZweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNlcXVlbmNlIGRpYWdyYW0gZXhhbXBsZTwvdGl0bGU+PGRlZnMvPjxnPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyMTguNjc0OCIgeD0iMTAiIHk9IjI3Ljk5NTEiPlNlcXVlbmNlIGRpYWdyYW0gZXhhbXBsZTwvdGV4dD48Zz48dGl0bGU+Qm9iPC90aXRsZT48cmVjdCBmaWxsPSIjMDAwMDAwIiBmaWxsLW9wYWNpdHk9IjAuMDAwMDAiIGhlaWdodD0iNDkuMTMyOCIgd2lkdGg9IjgiIHg9Ijg2LjQ3NzUiIHk9IjczLjU5MzgiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTtzdHJva2UtZGFzaGFycmF5OjUsNTsiIHgxPSI4OS45NDkyIiB4Mj0iODkuOTQ5MiIgeTE9IjczLjU5MzgiIHkyPSIxMjIuNzI2NiIvPjwvZz48Zz48dGl0bGU+QWxpY2U8L3RpdGxlPjxyZWN0IGZpbGw9IiMwMDAwMDAiIGZpbGwtb3BhY2l0eT0iMC4wMDAwMCIgaGVpZ2h0PSI0OS4xMzI4IiB3aWR0aD0iOCIgeD0iMTQxLjg5MjEiIHk9IjczLjU5MzgiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTtzdHJva2UtZGFzaGFycmF5OjUsNTsiIHgxPSIxNDUuMDU4NiIgeDI9IjE0NS4wNTg2IiB5MT0iNzMuNTkzOCIgeTI9IjEyMi43MjY2Ii8+PC9nPjxnIGNsYXNzPSJwYXJ0aWNpcGFudCBwYXJ0aWNpcGFudC1oZWFkIiBkYXRhLXBhcnRpY2lwYW50PSJCb2IiPjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDEuMDU2NiIgeD0iNjkuOTQ5MiIgeT0iNDIuMjk2OSIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjI3LjA1NjYiIHg9Ijc2Ljk0OTIiIHk9IjYyLjI5MiI+Qm9iPC90ZXh0PjwvZz48ZyBjbGFzcz0icGFydGljaXBhbnQgcGFydGljaXBhbnQtdGFpbCIgZGF0YS1wYXJ0aWNpcGFudD0iQm9iIj48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQxLjA1NjYiIHg9IjY5Ljk0OTIiIHk9IjEyMS43MjY2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjcuMDU2NiIgeD0iNzYuOTQ5MiIgeT0iMTQxLjcyMTciPkJvYjwvdGV4dD48L2c+PGcgY2xhc3M9InBhcnRpY2lwYW50IHBhcnRpY2lwYW50LWhlYWQiIGRhdGEtcGFydGljaXBhbnQ9IkFsaWNlIj48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQ3LjY2NyIgeD0iMTIyLjA1ODYiIHk9IjQyLjI5NjkiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMy42NjciIHg9IjEyOS4wNTg2IiB5PSI2Mi4yOTIiPkFsaWNlPC90ZXh0PjwvZz48ZyBjbGFzcz0icGFydGljaXBhbnQgcGFydGljaXBhbnQtdGFpbCIgZGF0YS1wYXJ0aWNpcGFudD0iQWxpY2UiPjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDcuNjY3IiB4PSIxMjIuMDU4NiIgeT0iMTIxLjcyNjYiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMy42NjciIHg9IjEyOS4wNTg2IiB5PSIxNDEuNzIxNyI+QWxpY2U8L3RleHQ+PC9nPjxnIGNsYXNzPSJtZXNzYWdlIiBkYXRhLXBhcnRpY2lwYW50LTE9IkJvYiIgZGF0YS1wYXJ0aWNpcGFudC0yPSJBbGljZSI+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxMzMuODkyMSwxMDAuNzI2NiwxNDMuODkyMSwxMDQuNzI2NiwxMzMuODkyMSwxMDguNzI2NiwxMzcuODkyMSwxMDQuNzI2NiIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MTsiIHgxPSI5MC40Nzc1IiB4Mj0iMTM5Ljg5MjEiIHkxPSIxMDQuNzI2NiIgeTI9IjEwNC43MjY2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzEuNDE0NiIgeD0iOTcuNDc3NSIgeT0iOTkuNjYwNiI+aGVsbG88L3RleHQ+PC9nPjwhLS1TUkM9W0F5YWlvS2JMMjR1akI0dERJcXZMSUNiQ0oyekFwNUw4aEtaQ0JTWDl2TkJBSnJCR2pMRG1wQ2E0SWJlZlBBSmN2RUczMDAwMF0tLT48L2c+PC9zdmc+", view.getContent()); + assertEquals("image/svg+xml", view.getContentType()); + } + @Test public void importDiagram_WhenThePlantUMLURLIsNotSpecified() throws Exception { Workspace workspace = new Workspace("Name", "Description"); From 56b72cd1ae64610a7b8ce8dc6b830ff770da6a05 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 4 Sep 2025 12:44:55 +0100 Subject: [PATCH 364/418] Gradle test target now works offline by excluding tests tagged "IntegrationTest". --- build.gradle | 11 ++++++++++- .../api/WorkspaceApiClientIntegrationTests.java | 5 +++++ .../com/structurizr/view/ThemeUtilsTests.java | 3 +++ .../test/java/com/structurizr/dsl/DslTests.java | 5 +++++ .../export/dot/DOTDiagramExporterTests.java | 2 ++ .../export/ilograph/IlographExporterTests.java | 2 ++ .../mermaid/MermaidDiagramExporterTests.java | 2 ++ .../plantuml/C4PlantUMLDiagramExporterTests.java | 3 +++ .../StructurizrPlantUMLDiagramExporterTests.java | 2 ++ structurizr-import/build.gradle | 15 +-------------- 10 files changed, 35 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 876b332b5..86760cb7a 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,16 @@ subprojects { proj -> } test { - useJUnitPlatform() + useJUnitPlatform { + excludeTags "IntegrationTest" + } + } + + tasks.register("integrationTest", Test) { + useJUnitPlatform { + includeTags "IntegrationTest" + } + mustRunAfter check } compileJava.options.encoding = 'UTF-8' diff --git a/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java b/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java index e54981f04..3b4628c6c 100644 --- a/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java +++ b/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java @@ -10,6 +10,7 @@ import com.structurizr.view.SystemContextView; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -51,6 +52,7 @@ private File getArchivedWorkspace() { } @Test + @Tag("IntegrationTest") void putAndGetWorkspace_WithoutEncryption() throws Exception { Workspace workspace = new Workspace("Structurizr client library tests - without encryption", "A test workspace for the Structurizr client library"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); @@ -77,6 +79,7 @@ void putAndGetWorkspace_WithoutEncryption() throws Exception { } @Test + @Tag("IntegrationTest") void putAndGetWorkspace_WithEncryption() throws Exception { client.setEncryptionStrategy(new AesEncryptionStrategy("password")); Workspace workspace = new Workspace("Structurizr client library tests - with encryption", "A test workspace for the Structurizr client library"); @@ -104,6 +107,7 @@ void putAndGetWorkspace_WithEncryption() throws Exception { } @Test + @Tag("IntegrationTest") void lockWorkspace() throws Exception { client.unlockWorkspace(20081); assertTrue(client.lockWorkspace(20081)); @@ -111,6 +115,7 @@ void lockWorkspace() throws Exception { @Test + @Tag("IntegrationTest") void unlockWorkspace() throws Exception { client.lockWorkspace(20081); assertTrue(client.unlockWorkspace(20081)); diff --git a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java index 5f4882881..3ebc16983 100644 --- a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java @@ -4,6 +4,7 @@ import com.structurizr.model.Relationship; import com.structurizr.model.SoftwareSystem; import com.structurizr.model.Tags; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -23,6 +24,7 @@ void loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { } @Test + @Tag("IntegrationTest") void loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); @@ -137,6 +139,7 @@ void findRelationshipStyle_WithThemes() { } @Test + @Tag("IntegrationTest") void loadThemes_ReplacesRelativeIconReferences() throws Exception { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index b0c1b64f8..57ed5efbd 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -5,6 +5,7 @@ import com.structurizr.model.*; import com.structurizr.util.StringUtils; import com.structurizr.view.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -396,6 +397,7 @@ void test_includeLocalDirectory_WhenThereAreHiddenFiles() throws Exception { } @Test + @Tag("IntegrationTest") void test_includeUrl() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); parser.parse(new File("src/test/resources/dsl/include-url.dsl")); @@ -434,6 +436,7 @@ void test_includeLocalFile_ThrowsAnException_WhenRunningInRestrictedMode() { } @ParameterizedTest + @Tag("IntegrationTest") @ValueSource(strings = { "src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl", "src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl" }) void test_extendWorkspaceFromJson(String dslFile) throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); @@ -472,6 +475,7 @@ void test_extendWorkspaceFromJsonFile_WhenRunningInRestrictedMode() throws Excep } @ParameterizedTest + @Tag("IntegrationTest") @ValueSource(strings = { "src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl", "src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl" }) void test_extendWorkspaceFromDsl(String dslFile) throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); @@ -1091,6 +1095,7 @@ void test_imageViews_ViaFiles() throws Exception { } @Test + @Tag("IntegrationTest") void test_imageViews_ViaUrls() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); parser.parse(new File("src/test/resources/dsl/image-views/workspace-via-url.dsl")); diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java index 39f175254..b19a96f36 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -6,6 +6,7 @@ import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -53,6 +54,7 @@ public void test_BigBankPlcExample() throws Exception { } @Test + @Tag("IntegrationTest") public void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); ThemeUtils.loadThemes(workspace); diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java index a42da19bb..4761312d2 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -7,6 +7,7 @@ import com.structurizr.model.Model; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.ThemeUtils; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -26,6 +27,7 @@ public void test_BigBankPlcExample() throws Exception { } @Test + @Tag("IntegrationTest") void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); workspace.getViews().getConfiguration().getStyles().addElementStyle("Amazon Web Services - Route 53").addProperty(IlographExporter.ILOGRAPH_ICON, "AWS/Networking/Route-53.svg"); diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java index b5b1a8774..ed49a4431 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -6,6 +6,7 @@ import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -60,6 +61,7 @@ public void test_BigBankPlcExample() throws Exception { } @Test + @Tag("IntegrationTest") public void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); ThemeUtils.loadThemes(workspace); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index 49fae0fe2..b544dbdcf 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -6,6 +6,7 @@ import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -61,6 +62,7 @@ public void test_BigBankPlcExample() throws Exception { } @Test + @Tag("IntegrationTest") public void test_AmazonWebServicesExampleWithoutTags() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); ThemeUtils.loadThemes(workspace); @@ -77,6 +79,7 @@ public void test_AmazonWebServicesExampleWithoutTags() throws Exception { } @Test + @Tag("IntegrationTest") public void test_AmazonWebServicesExampleWithTags() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); ThemeUtils.loadThemes(workspace); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 19869f588..136e48cb9 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -6,6 +6,7 @@ import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.io.File; @@ -71,6 +72,7 @@ public void test_BigBankPlcExample() throws Exception { } @Test + @Tag("IntegrationTest") public void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); ThemeUtils.loadThemes(workspace); diff --git a/structurizr-import/build.gradle b/structurizr-import/build.gradle index d1f719fe1..7b5c3b32f 100644 --- a/structurizr-import/build.gradle +++ b/structurizr-import/build.gradle @@ -4,17 +4,4 @@ dependencies { } -description = 'Utilities to import diagrams and documentation into a Structurizr workspace' - -test { - useJUnitPlatform { - excludeTags "IntegrationTest" - } -} - -tasks.register("integrationTest", Test) { - useJUnitPlatform { - includeTags "IntegrationTest" - } - mustRunAfter check -} \ No newline at end of file +description = 'Utilities to import diagrams and documentation into a Structurizr workspace' \ No newline at end of file From c9d20b3605caf54613bf94b9cc35dac442bfc6ba Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 4 Sep 2025 12:47:26 +0100 Subject: [PATCH 365/418] . --- structurizr-dsl/src/test/resources/dsl/test.dsl | 1 + 1 file changed, 1 insertion(+) diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index d2de4ed88..cf0ef4407 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -355,6 +355,7 @@ workspace "Name" "Description" { colour #777777 dashed true routing curved + jump true fontSize 24 width 400 position 50 From dbe5d11c553ee7464d08dc0459ac7e329432cac1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 4 Sep 2025 14:14:30 +0100 Subject: [PATCH 366/418] structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. --- changelog.md | 1 + .../java/com/structurizr/dsl/DslLine.java | 5 + .../dsl/ImageViewContentParser.java | 115 +++++++++++------- .../structurizr/dsl/StructurizrDslParser.java | 27 +++- .../main/java/com/structurizr/dsl/Tokens.java | 2 +- .../java/com/structurizr/dsl/DslTests.java | 24 ++++ .../dsl/ImageViewContentParserTests.java | 19 ++- .../src/test/resources/dsl/image-view.dsl | 4 +- .../dsl/image-views/workspace-via-source.dsl | 34 ++++++ 9 files changed, 178 insertions(+), 53 deletions(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-source.dsl diff --git a/changelog.md b/changelog.md index 43d0c76ea..59077a0d7 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships). - structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. - structurizr-dsl: Adds support for a `jump` property on relationship styles. +- structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. - structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java index 6c8c99944..ca2ad4ec0 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java @@ -21,4 +21,9 @@ int getLineNumber() { return lineNumber; } + @Override + public String toString() { + return source; + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index 2866f9b16..e73a235ed 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -15,9 +15,9 @@ final class ImageViewContentParser extends AbstractParser { - private static final String PLANTUML_GRAMMAR = "plantuml "; - private static final String MERMAID_GRAMMAR = "mermaid "; - private static final String KROKI_GRAMMAR = "kroki "; + private static final String PLANTUML_GRAMMAR = "plantuml "; + private static final String MERMAID_GRAMMAR = "mermaid "; + private static final String KROKI_GRAMMAR = "kroki "; private static final String IMAGE_GRAMMAR = "image "; private static final int PLANTUML_SOURCE_INDEX = 1; @@ -33,7 +33,7 @@ final class ImageViewContentParser extends AbstractParser { } void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { - // plantuml + // plantuml if (tokens.hasMoreThan(PLANTUML_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + PLANTUML_GRAMMAR); @@ -45,22 +45,31 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { String source = tokens.get(PLANTUML_SOURCE_INDEX); try { - View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); - if (viewWithKey instanceof ModelView) { - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); - String plantuml = exporter.export((ModelView)viewWithKey).getDefinition(); - new PlantUMLImporter().importDiagram(context.getView(), plantuml); + if (source.contains("\n")) { + // inline source + new PlantUMLImporter().importDiagram(context.getView(), source); } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new PlantUMLImporter().importDiagram(context.getView(), content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); + View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); + if (viewWithKey instanceof ModelView) { + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + String plantuml = exporter.export((ModelView) viewWithKey).getDefinition(); + new PlantUMLImporter().importDiagram(context.getView(), plantuml); } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - new PlantUMLImporter().importDiagram(context.getView(), file); + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new PlantUMLImporter().importDiagram(context.getView(), content.getContent()); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + new PlantUMLImporter().importDiagram(context.getView(), file); + } else { + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); + } + } else { + throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); + } } } } @@ -74,7 +83,7 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { } void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { - // mermaid + // mermaid if (tokens.hasMoreThan(MERMAID_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + MERMAID_GRAMMAR); @@ -86,22 +95,31 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { String source = tokens.get(MERMAID_SOURCE_INDEX); try { - View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); - if (viewWithKey instanceof ModelView) { - MermaidDiagramExporter exporter = new MermaidDiagramExporter(); - String mermaid = exporter.export((ModelView)viewWithKey).getDefinition(); - new MermaidImporter().importDiagram(context.getView(), mermaid); + if (source.contains("\n")) { + // inline source + new MermaidImporter().importDiagram(context.getView(), source); } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new MermaidImporter().importDiagram(context.getView(), content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); + View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); + if (viewWithKey instanceof ModelView) { + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + String mermaid = exporter.export((ModelView) viewWithKey).getDefinition(); + new MermaidImporter().importDiagram(context.getView(), mermaid); } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - new MermaidImporter().importDiagram(context.getView(), file); + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new MermaidImporter().importDiagram(context.getView(), content.getContent()); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + new MermaidImporter().importDiagram(context.getView(), file); + } else { + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); + } + } else { + throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); + } } } } @@ -115,7 +133,7 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { } void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) { - // kroki + // kroki if (tokens.hasMoreThan(KROKI_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + KROKI_GRAMMAR); @@ -128,16 +146,25 @@ void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) { String source = tokens.get(KROKI_SOURCE_INDEX); try { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new KrokiImporter().importDiagram(context.getView(), format, content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + if (source.contains("\n")) { + // inline source + new KrokiImporter().importDiagram(context.getView(), format, source); } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - new KrokiImporter().importDiagram(context.getView(), format, file); + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new KrokiImporter().importDiagram(context.getView(), format, content.getContent()); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode"); + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + new KrokiImporter().importDiagram(context.getView(), format, file); + } else { + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); + } + } else { + throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode"); + } } } } catch (Exception e) { @@ -168,8 +195,12 @@ void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { } else { if (!restricted) { File file = new File(dslFile.getParentFile(), source); - context.getView().setContent(ImageUtils.getImageAsDataUri(file)); - context.getView().setTitle(file.getName()); + if (file.exists()) { + context.getView().setContent(ImageUtils.getImageAsDataUri(file)); + context.getView().setTitle(file.getName()); + } else { + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); + } } else { throw new RuntimeException("Images must be specified as a URL when running in restricted mode"); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 58fcc3e08..d7d33ef80 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -36,6 +36,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private static final String TEXT_BLOCK_MARKER = "\"\"\""; private static final Pattern STRING_SUBSTITUTION_PATTERN = Pattern.compile("(\\$\\{[a-zA-Z0-9-_.]+?})"); + private static final String STRING_SUBSTITUTION_TEMPLATE = "${%s}"; private static final String STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME = "structurizr.dsl.identifier"; @@ -1289,7 +1290,7 @@ private List preProcessLines(List lines) { for (String line : lines) { if (textBlock) { if (line.endsWith(TEXT_BLOCK_MARKER)) { - buf.append("\""); + buf.append(TEXT_BLOCK_MARKER); textBlock = false; textBlockLeadingSpace = -1; lineComplete = true; @@ -1304,14 +1305,18 @@ private List preProcessLines(List lines) { } } } - buf.append(line, textBlockLeadingSpace, line.length()); - buf.append("\n"); + if (StringUtils.isNullOrEmpty(line)) { + buf.append("\n"); + } else { + buf.append(line, textBlockLeadingSpace, line.length()); + buf.append("\n"); + } } } else if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(MULTI_LINE_SEPARATOR)) { buf.append(line, 0, line.length() - 1); lineComplete = false; } else if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(TEXT_BLOCK_MARKER)) { - buf.append(line, 0, line.length() - 2); + buf.append(line, 0, line.length()); lineComplete = false; textBlock = true; } else { @@ -1324,7 +1329,19 @@ private List preProcessLines(List lines) { } if (lineComplete) { - dslLines.add(new DslLine(buf.toString(), lineNumber)); + // replace the text block with a constant (that will become substituted later) + // (this makes it possible for text blocks to include double-quote characters) + String s = buf.toString(); + if (s.endsWith(TEXT_BLOCK_MARKER)) { + String[] parts = s.split(TEXT_BLOCK_MARKER); + String constantName = UUID.randomUUID().toString(); + String constantValue = parts[1].substring(0, parts[1].length() - 1); // remove final line break + addConstant(constantName, constantValue); + dslLines.add(new DslLine(parts[0] + "\"" + String.format(STRING_SUBSTITUTION_TEMPLATE, constantName) + "\"", lineNumber)); + } else { + dslLines.add(new DslLine(buf.toString(), lineNumber)); + } + buf = new StringBuilder(); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java index ba337b3bf..55fe452ba 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java @@ -11,7 +11,7 @@ final class Tokens { } String get(int index) { - return tokens.get(index).trim().replaceAll("\\\\\"", "\"").trim().replaceAll("\\\\n", "\n"); + return tokens.get(index).replaceAll("\\\\\"", "\"").replaceAll("\\\\n", "\n"); } void remove(int index) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 57ed5efbd..30a884caf 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1129,6 +1129,30 @@ void test_imageViews_ViaUrls() throws Exception { assertEquals("image/svg+xml", svgView.getContentType()); } + @Test + void test_imageViews_ViaSource() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/dsl/image-views/workspace-via-source.dsl")); + + Workspace workspace = parser.getWorkspace(); + assertEquals(3, workspace.getViews().getImageViews().size()); + + ImageView plantumlView = (ImageView)workspace.getViews().getViewWithKey("plantuml"); + assertNull(plantumlView.getTitle()); + assertEquals("http://localhost:7777/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", plantumlView.getContent()); + assertEquals("image/svg+xml", plantumlView.getContentType()); + + ImageView mermaidView = (ImageView)workspace.getViews().getViewWithKey("mermaid"); + assertNull(mermaidView.getTitle()); + assertEquals("http://localhost:8888/svg/Zmxvd2NoYXJ0IFRECiAgICBTdGFydCAtLT4gU3RvcA==", mermaidView.getContent()); + assertEquals("image/svg+xml", mermaidView.getContentType()); + + ImageView krokiView = (ImageView)workspace.getViews().getViewWithKey("kroki"); + assertNull(krokiView.getTitle()); + assertEquals("http://localhost:9999/graphviz/png/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", krokiView.getContent()); + assertEquals("image/png", krokiView.getContentType()); + } + @Test void test_EmptyDeploymentEnvironment() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index ca65bcadc..52e59ccb3 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -28,7 +28,7 @@ void test_parsePlantUML_ThrowsAnException_WithTooFewTokens() { parser.parsePlantUML(context, null, tokens("plantuml")); fail(); } catch (Exception e) { - assertEquals("Expected: plantuml ", e.getMessage()); + assertEquals("Expected: plantuml ", e.getMessage()); } } @@ -64,10 +64,10 @@ void test_parseMermaid_ThrowsAnException_WithTooFewTokens() { ImageViewDslContext context = new ImageViewDslContext(imageView); context.setWorkspace(workspace); parser = new ImageViewContentParser(true); - parser.parseMermaid(context, null, tokens("plantuml")); + parser.parseMermaid(context, null, tokens("mermaid")); fail(); } catch (Exception e) { - assertEquals("Expected: mermaid ", e.getMessage()); + assertEquals("Expected: mermaid ", e.getMessage()); } } @@ -97,6 +97,19 @@ void test_parseMermaid_WithViewKey() { assertEquals("https://mermaid.ink/svg/pako:eJxlkMtuwjAQRX9lNAhlE9SwqupCpLLuLt0RFiYeJxZ-RLYppYh_bxJHVR93NrM4c3U0N8DGCUKGred9B2-72gJoZU9VvGoCQZKfdQSptGYLOaW2IxPOx3QiFB8WA_saq2uIZOCVWxEa3lONhxEd4FQ2kz_L8hC9O9HvboD10LYR6j1dbjPpbFxdSLVdZHB0WmTly-ZhAMp_VFCfxOCxWD6D4b5VdhVdz6DoP7JyXzkZL9wTJNVD6vjjuZ4NxZRvwyc-Tt447TxbFFOSL1mBOaAhb7gSyG4YOzLjV-f_4f3-BQMfekI=", imageView.getContent()); } + @Test + void test_parseKroki_ThrowsAnException_WithTooFewTokens() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + parser = new ImageViewContentParser(true); + parser.parseKroki(context, null, tokens("kroki")); + fail(); + } catch (Exception e) { + assertEquals("Expected: kroki ", e.getMessage()); + } + } + @Test void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { diff --git a/structurizr-dsl/src/test/resources/dsl/image-view.dsl b/structurizr-dsl/src/test/resources/dsl/image-view.dsl index 27633c053..c443c4b61 100644 --- a/structurizr-dsl/src/test/resources/dsl/image-view.dsl +++ b/structurizr-dsl/src/test/resources/dsl/image-view.dsl @@ -2,8 +2,8 @@ workspace { views { image * "Image" { - image image.png - } + image image.png } + } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-source.dsl b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-source.dsl new file mode 100644 index 000000000..c342857ce --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-source.dsl @@ -0,0 +1,34 @@ +workspace { + + views { + properties { + "plantuml.url" "http://localhost:7777" + "mermaid.url" "http://localhost:8888" + "mermaid.compress" "false" + "kroki.url" "http://localhost:9999" + } + + image * "plantuml" { + plantuml """ + @startuml + Bob -> Alice : hello + @enduml + """ + } + + image * "mermaid" { + mermaid """ + flowchart TD + Start --> Stop + """ + } + + image * "kroki" { + kroki graphviz """ + digraph G {Hello->World} + + """ + } + } + +} \ No newline at end of file From 990b1c2f9e519a1e94e7d114b74a767919d0c459 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 5 Sep 2025 13:41:15 +0100 Subject: [PATCH 367/418] structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. --- changelog.md | 1 + .../com/structurizr/dsl/StructurizrDslParser.java | 3 ++- .../src/test/java/com/structurizr/dsl/DslTests.java | 12 ++++++++++++ ...-and-variables-from-workspace-extension-child.dsl | 7 +++++++ ...and-variables-from-workspace-extension-parent.dsl | 6 ++++++ 5 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-child.dsl create mode 100644 structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-parent.dsl diff --git a/changelog.md b/changelog.md index 59077a0d7..b2c46c6fd 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,7 @@ - structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. - structurizr-dsl: Adds support for a `jump` property on relationship styles. - structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. +- structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. - structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index d7d33ef80..4cc143efe 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -45,7 +45,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private final Stack contextStack; private final Set parsedTokens = new HashSet<>(); private final IdentifiersRegister identifiersRegister; - private final Map constantsAndVariables; + private Map constantsAndVariables; private final Features features = new Features(); private Map> archetypes = Map.of( @@ -78,6 +78,7 @@ public StructurizrDslParser() { void configureFrom(StructurizrDslParser parser) { setIdentifierScope(parser.getIdentifierScope()); archetypes = parser.archetypes; + constantsAndVariables = parser.constantsAndVariables; } /** diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 30a884caf..d90e5a46d 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1269,6 +1269,18 @@ void test_Constant() { } } + @Test + void test_ConstantsAndVariablesFromWorkspaceExtension() throws Exception { + File dslFile = new File("src/test/resources/dsl/constants-and-variables-from-workspace-extension-child.dsl"); + + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + + SoftwareSystem softwareSystem = parser.getWorkspace().getModel().getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("Description", softwareSystem.getDescription()); + } + @Test void test_UnbalancedCurlyBraces() { try { diff --git a/structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-child.dsl b/structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-child.dsl new file mode 100644 index 000000000..aa10fdab4 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-child.dsl @@ -0,0 +1,7 @@ +workspace extends constants-and-variables-from-workspace-extension-parent.dsl { + + model { + softwareSystem "${NAME}" "${DESCRIPTION}" + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-parent.dsl b/structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-parent.dsl new file mode 100644 index 000000000..442ce70ea --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/constants-and-variables-from-workspace-extension-parent.dsl @@ -0,0 +1,6 @@ +workspace { + + !const "NAME" "Name" + !var "DESCRIPTION" "Description" + +} \ No newline at end of file From d40de39c805437b9c890989e5bb7b9c4b370d236 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 11 Sep 2025 08:57:22 +0100 Subject: [PATCH 368/418] . --- structurizr-dsl/src/test/resources/dsl/include/model.dsl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/structurizr-dsl/src/test/resources/dsl/include/model.dsl b/structurizr-dsl/src/test/resources/dsl/include/model.dsl index 6bdd6ec8b..71e89755d 100644 --- a/structurizr-dsl/src/test/resources/dsl/include/model.dsl +++ b/structurizr-dsl/src/test/resources/dsl/include/model.dsl @@ -1,3 +1 @@ -softwareSystem = softwareSystem "Software System" { - !docs docs -} \ No newline at end of file +softwareSystem = softwareSystem "Software System" \ No newline at end of file From 91a64b30fda286b2bf5df7af967f6ff3c2ba7848 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 11 Sep 2025 09:18:59 +0100 Subject: [PATCH 369/418] structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). --- changelog.md | 3 +- gradle.properties | 2 +- .../com/structurizr/AbstractWorkspace.java | 18 +- .../com/structurizr/dsl/BrandingParser.java | 1 + .../dsl/ComponentFinderDslContext.java | 1 + .../com/structurizr/dsl/DecisionsParser.java | 2 + .../java/com/structurizr/dsl/DocsParser.java | 2 + .../java/com/structurizr/dsl/DslContext.java | 9 + .../java/com/structurizr/dsl/DslLine.java | 4 +- .../java/com/structurizr/dsl/DslUtils.java | 9 + .../structurizr/dsl/ElementStyleParser.java | 1 + .../dsl/ImageViewContentParser.java | 4 + .../dsl/ImpliedRelationshipsParser.java | 4 + .../com/structurizr/dsl/IncludeParser.java | 28 ++- .../structurizr/dsl/IncludedDslContext.java | 33 --- .../com/structurizr/dsl/PluginDslContext.java | 1 + .../com/structurizr/dsl/ScriptDslContext.java | 1 + .../structurizr/dsl/StructurizrDslParser.java | 44 ++-- .../com/structurizr/dsl/WorkspaceParser.java | 3 + .../java/com/structurizr/dsl/DslTests.java | 204 +++++++++++++----- .../structurizr/dsl/IncludeParserTests.java | 6 +- 21 files changed, 251 insertions(+), 129 deletions(-) delete mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java diff --git a/changelog.md b/changelog.md index b2c46c6fd..068d5d942 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## v4.2.0 (unreleased) +## v5.0.0 (unreleased) - structurizr-java: Fixes https://github.com/structurizr/java/issues/437 (Make ComponentFinder.run() not fail on empty Set). - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). @@ -15,6 +15,7 @@ - structurizr-dsl: Adds support for a `jump` property on relationship styles. - structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. - structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. +- structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). - structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. diff --git a/gradle.properties b/gradle.properties index c99ca35d7..638aa1b58 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=4.2.0 \ No newline at end of file +version=5.0.0 \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java index 755cfa1bb..9f067fb6f 100644 --- a/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java +++ b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java @@ -1,6 +1,7 @@ package com.structurizr; import com.structurizr.configuration.WorkspaceConfiguration; +import com.structurizr.util.StringUtils; import java.lang.reflect.Constructor; import java.util.Collections; @@ -229,17 +230,30 @@ public Map getProperties() { * @param value the value of the property */ public void addProperty(String name, String value) { - if (name == null || name.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(name)) { throw new IllegalArgumentException("A property name must be specified."); } - if (value == null || value.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(value)) { throw new IllegalArgumentException("A property value must be specified."); } properties.put(name, value); } + /** + * Removes a name-value pair property from this workspace. + * + * @param name the name of the property to remove + */ + public void removeProperty(String name) { + if (StringUtils.isNullOrEmpty(name)) { + throw new IllegalArgumentException("A property name must be specified."); + } + + properties.remove(name); + } + void setProperties(Map properties) { if (properties != null) { this.properties = new HashMap<>(properties); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java index b4f10353d..5b82b6594 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java @@ -30,6 +30,7 @@ void parseLogo(BrandingDslContext context, Tokens tokens, boolean restricted) { if (!restricted) { File file = new File(context.getFile().getParent(), path); if (file.exists() && !file.isDirectory()) { + context.setDslPortable(false); try { String dataUri = ImageUtils.getImageAsDataUri(file); context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(dataUri); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java index 96dc8c077..60eaf4a32 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java @@ -16,6 +16,7 @@ final class ComponentFinderDslContext extends DslContext { this.dslParser = dslParser; this.containerDslContext = containerDslContext; componentFinderBuilder.forContainer(containerDslContext.getContainer()); + setDslPortable(false); } @Override diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java index 0db52d27a..cf380e068 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java @@ -47,6 +47,8 @@ void parse(ComponentDslContext context, File dslFile, Tokens tokens) { private void parse(DslContext context, Documentable documentable, File dslFile, Tokens tokens) { // !adrs + context.setDslPortable(false); + if (tokens.hasMoreThan(TYPE_OR_FQN_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java index c758781f9..8cb4e6d51 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java @@ -35,6 +35,8 @@ void parse(ComponentDslContext context, File dslFile, Tokens tokens) { private void parse(DslContext context, Documentable documentable, File dslFile, Tokens tokens) { // !docs + context.setDslPortable(false); + if (tokens.hasMoreThan(FQN_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java index 41417ba8e..4998d6864 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java @@ -17,6 +17,7 @@ abstract class DslContext { private Workspace workspace; private boolean extendingWorkspace; + private boolean dslPortable = true; protected IdentifiersRegister identifiersRegister = new IdentifiersRegister(); @@ -36,6 +37,14 @@ void setExtendingWorkspace(boolean extendingWorkspace) { this.extendingWorkspace = extendingWorkspace; } + boolean isDslPortable() { + return dslPortable; + } + + void setDslPortable(boolean bool) { + this.dslPortable = bool; + } + void setIdentifierRegister(IdentifiersRegister identifersRegister) { this.identifiersRegister = identifersRegister; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java index ca2ad4ec0..b39952fa5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java @@ -8,8 +8,8 @@ class DslLine { private final String source; private final int lineNumber; - DslLine(String source, int lineNumber) { - this.source = source; + DslLine(String processedSource, int lineNumber) { + this.source = processedSource; this.lineNumber = lineNumber; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java index e20b936f2..33662ac87 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java @@ -42,4 +42,13 @@ public static void setDsl(Workspace workspace, String dsl) { } } + /** + * Clears the DSL associated with a workspace. + * + * @param workspace a Workspace object + */ + public static void clearDsl(Workspace workspace) { + workspace.removeProperty(STRUCTURIZR_DSL_PROPERTY_NAME); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java index dd76e5804..dd83c431e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java @@ -303,6 +303,7 @@ void parseIcon(ElementStyleDslContext context, Tokens tokens, boolean restricted if (file.exists() && !file.isDirectory()) { try { style.setIcon(ImageUtils.getImageAsDataUri(file)); + context.setDslPortable(false); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index e73a235ed..5c049f4f6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -63,6 +63,7 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { if (!restricted) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { + context.setDslPortable(false); new PlantUMLImporter().importDiagram(context.getView(), file); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); @@ -113,6 +114,7 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { if (!restricted) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { + context.setDslPortable(false); new MermaidImporter().importDiagram(context.getView(), file); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); @@ -158,6 +160,7 @@ void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) { if (!restricted) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { + context.setDslPortable(false); new KrokiImporter().importDiagram(context.getView(), format, file); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); @@ -196,6 +199,7 @@ void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { if (!restricted) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { + context.setDslPortable(false); context.getView().setContent(ImageUtils.getImageAsDataUri(file)); context.getView().setTitle(file.getName()); } else { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java index b647e5b3d..e880f25a7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java @@ -46,6 +46,10 @@ void parse(DslContext context, Tokens tokens, File dslFile, boolean restricted) } } + if (!BUILT_IN_IMPLIED_RELATIONSHIPS_STRATEGIES.contains(option)) { + context.setDslPortable(false); + } + try { Class impliedRelationshipsStrategyClass = context.loadClass(option, dslFile); Constructor constructor = impliedRelationshipsStrategyClass.getDeclaredConstructor(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java index 93252d57d..bfea57783 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -13,9 +14,11 @@ final class IncludeParser extends AbstractParser { private static final int SOURCE_INDEX = 1; - void parse(IncludedDslContext context, Tokens tokens) { + List parse(DslContext context, File dslFile, Tokens tokens) { // !include + List includedFiles = new ArrayList<>(); + if (tokens.hasMoreThan(SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); } @@ -28,28 +31,33 @@ void parse(IncludedDslContext context, Tokens tokens) { if (source.startsWith("https://") || source.startsWith("http://")) { RemoteContent content = readFromUrl(source); List lines = Arrays.asList(content.getContent().split("\n")); - context.addFile(context.getParentFile(), lines); + includedFiles.add(new IncludedFile(dslFile, lines)); } else { - if (context.getParentFile() != null) { - File path = new File(context.getParentFile().getParent(), source); + if (dslFile != null) { + File path = new File(dslFile.getParent(), source); try { if (!path.exists()) { throw new RuntimeException(path.getCanonicalPath() + " could not be found"); } - readFiles(context, path); + includedFiles.addAll(readFiles(path)); + context.setDslPortable(false); } catch (IOException e) { throw new RuntimeException("Error including " + path.getAbsolutePath() + ": " + e.getMessage()); } } } + + return includedFiles; } - private void readFiles(IncludedDslContext context, File path) throws IOException { + private List readFiles(File path) throws IOException { + List includedFiles = new ArrayList<>(); + if (path.isHidden() || path.getName().startsWith(".")) { // ignore - return; + return includedFiles; } if (path.isDirectory()) { @@ -58,16 +66,18 @@ private void readFiles(IncludedDslContext context, File path) throws IOException Arrays.sort(files); for (File file : files) { - readFiles(context, file); + includedFiles.addAll(readFiles(file)); } } } else { try { - context.addFile(path, Files.readAllLines(path.toPath(), StandardCharsets.UTF_8)); + includedFiles.add(new IncludedFile(path, Files.readAllLines(path.toPath(), StandardCharsets.UTF_8))); } catch (IOException e) { throw new RuntimeException("Error reading file at " + path.getAbsolutePath() + ": " + e.getMessage()); } } + + return includedFiles; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java deleted file mode 100644 index 42c86d00a..000000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludedDslContext.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.structurizr.dsl; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -final class IncludedDslContext extends DslContext { - - private final File parentFile; - private final List files = new ArrayList<>(); - - IncludedDslContext(File parentFile) { - this.parentFile = parentFile; - } - - File getParentFile() { - return parentFile; - } - - void addFile(File file, List lines) { - this.files.add(new IncludedFile(file, lines)); - } - - List getFiles() { - return new ArrayList<>(files); - } - - @Override - protected String[] getPermittedTokens() { - return new String[0]; - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java index a648c2e80..7885db081 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java @@ -15,6 +15,7 @@ class PluginDslContext extends DslContext { this.fullyQualifiedClassName = fullyQualifiedClassName; this.dslFile = dslFile; this.dslParser = dslParser; + setDslPortable(false); } void addParameter(String name, String value) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java index 6463e8324..ee25451ec 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java @@ -29,6 +29,7 @@ abstract class ScriptDslContext extends DslContext { this.parentContext = parentContext; this.dslFile = dslFile; this.dslParser = dslParser; + setDslPortable(false); } void addParameter(String name, String value) { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 4cc143efe..7eb8bd7a7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -60,6 +60,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { StructurizrDslTokens.RELATIONSHIP_TOKEN, new HashMap<>() ); + private boolean dslPortable = true; private final List dslSourceLines = new ArrayList<>(); private Workspace workspace; private boolean extendingWorkspace = false; @@ -123,9 +124,11 @@ public void setRestricted(boolean restricted) { */ public Workspace getWorkspace() { if (workspace != null) { - String value = workspace.getProperties().get(DslUtils.STRUCTURIZR_DSL_RETAIN_SOURCE_PROPERTY_NAME); - if (value == null || value.equalsIgnoreCase("true")) { - DslUtils.setDsl(workspace, getParsedDsl()); + if (dslPortable) { + String value = workspace.getProperties().get(DslUtils.STRUCTURIZR_DSL_RETAIN_SOURCE_PROPERTY_NAME); + if (value == null || value.equalsIgnoreCase("true")) { + DslUtils.setDsl(workspace, getParsedDsl()); + } } } @@ -210,11 +213,14 @@ void parse(List lines, DslContext dslContext) throws StructurizrDslParse * @throws StructurizrDslParserException when something goes wrong */ void parse(List lines, File dslFile, boolean fragment, boolean includeInDslSourceLines) throws StructurizrDslParserException { + if (includeInDslSourceLines) { + dslSourceLines.addAll(lines); + } + List dslLines = preProcessLines(lines); for (DslLine dslLine : dslLines) { String line = dslLine.getSource(); - String lineForDslSource = line; if (line.startsWith(BOM)) { // this caters for files encoded as "UTF-8 with BOM" @@ -266,9 +272,8 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn if (!restricted || tokens.get(1).startsWith("https://") || tokens.get(1).startsWith("http://")) { String leadingSpace = line.substring(0, line.indexOf(INCLUDE_FILE_TOKEN)); - IncludedDslContext context = new IncludedDslContext(dslFile); - new IncludeParser().parse(context, tokens); - for (IncludedFile includedFile : context.getFiles()) { + List files = new IncludeParser().parse(getContext(), dslFile, tokens); + for (IncludedFile includedFile : files) { List paddedLines = new ArrayList<>(); for (String unpaddedLine : includedFile.getLines()) { if (unpaddedLine.startsWith(BOM)) { @@ -278,15 +283,12 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn paddedLines.add(leadingSpace + unpaddedLine); } - parse(paddedLines, includedFile.getFile(), true, true); + parse(paddedLines, includedFile.getFile(), true, false); } } else { throwRestrictedModeException(firstToken + " "); } - // include the !include in the parser DSL as: # !include ... - lineForDslSource = null; - } else if (PLUGIN_TOKEN.equalsIgnoreCase(firstToken)) { if (!restricted) { String fullyQualifiedClassName = new PluginParser().parse(getContext(), tokens.withoutContextStartToken()); @@ -677,8 +679,11 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn workspace = new WorkspaceParser().parse(dslParserContext, tokens.withoutContextStartToken()); extendingWorkspace = !workspace.getModel().isEmpty(); - startContext(new WorkspaceDslContext()); + WorkspaceDslContext context = new WorkspaceDslContext(); + context.setDslPortable(dslParserContext.isDslPortable()); + startContext(context); parsedTokens.add(WORKSPACE_TOKEN); + } else if (IMPLIED_RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) || IMPLIED_RELATIONSHIPS_TOKEN.substring(1).equalsIgnoreCase(firstToken)) { new ImpliedRelationshipsParser().parse(getContext(), tokens, dslFile, restricted); @@ -1261,10 +1266,6 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } } } - - if (includeInDslSourceLines && lineForDslSource != null) { - dslSourceLines.add(lineForDslSource); - } } catch (Exception e) { if (e.getMessage() != null) { throw new StructurizrDslParserException(e.getMessage(), dslFile, dslLine.getLineNumber(), line); @@ -1332,15 +1333,16 @@ private List preProcessLines(List lines) { if (lineComplete) { // replace the text block with a constant (that will become substituted later) // (this makes it possible for text blocks to include double-quote characters) - String s = buf.toString(); - if (s.endsWith(TEXT_BLOCK_MARKER)) { - String[] parts = s.split(TEXT_BLOCK_MARKER); + String source = buf.toString(); + + if (source.endsWith(TEXT_BLOCK_MARKER)) { + String[] parts = source.split(TEXT_BLOCK_MARKER); String constantName = UUID.randomUUID().toString(); String constantValue = parts[1].substring(0, parts[1].length() - 1); // remove final line break addConstant(constantName, constantValue); dslLines.add(new DslLine(parts[0] + "\"" + String.format(STRING_SUBSTITUTION_TEMPLATE, constantName) + "\"", lineNumber)); } else { - dslLines.add(new DslLine(buf.toString(), lineNumber)); + dslLines.add(new DslLine(source, lineNumber)); } buf = new StringBuilder(); @@ -1412,6 +1414,8 @@ private void endContext() throws StructurizrDslParserException { if (!contextStack.empty()) { DslContext context = contextStack.pop(); context.end(); + + dslPortable &= context.isDslPortable(); } else { throw new StructurizrDslParserException("Unexpected end of context"); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java index 56bc1035e..e116de60a 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java @@ -75,6 +75,9 @@ Workspace parse(DslParserContext context, Tokens tokens) { workspace = structurizrDslParser.getWorkspace(); context.getParser().configureFrom(structurizrDslParser); } + + DslUtils.clearDsl(workspace); + context.setDslPortable(false); } } } catch (Exception e) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index d90e5a46d..43fcb6e0f 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -4,6 +4,7 @@ import com.structurizr.documentation.Section; import com.structurizr.model.*; import com.structurizr.util.StringUtils; +import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -325,15 +326,7 @@ void test_includeLocalFile() throws Exception { assertEquals(1, model.getSoftwareSystems().size()); assertNotNull(model.getSoftwareSystemWithName("Software System")); - assertEquals("workspace {\n" + - "\n" + - " model {\n" + - " softwareSystem = softwareSystem \"Software System\" {\n" + - " !docs docs\n" + - " }\n" + - " }\n" + - "\n" + - "}", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + assertEquals("", DslUtils.getDsl(workspace)); } @Test @@ -362,26 +355,7 @@ void test_includeLocalDirectory() throws Exception { assertNotNull(softwareSystem3); assertEquals(1, softwareSystem3.getDocumentation().getSections().size()); - assertEquals("workspace {\n" + - "\n" + - " model {\n" + - " !var SOFTWARE_SYSTEM_NAME \"Software System 1\"\n" + - " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + - " !docs ../../docs\n" + - " }\n" + - "\n" + - " !var SOFTWARE_SYSTEM_NAME \"Software System 2\"\n" + - " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + - " !docs ../../docs\n" + - " }\n" + - "\n" + - " !var SOFTWARE_SYSTEM_NAME \"Software System 3\"\n" + - " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + - " !docs ../../docs\n" + - " }\n" + - " }\n" + - "\n" + - "}", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + assertEquals("", DslUtils.getDsl(workspace)); } @Test @@ -404,20 +378,18 @@ void test_includeUrl() throws Exception { Workspace workspace = parser.getWorkspace(); Model model = workspace.getModel(); - ViewSet views = workspace.getViews(); - assertEquals(1, workspace.getModel().getSoftwareSystems().size()); - SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Software System"); + assertEquals(1, model.getSoftwareSystems().size()); + assertNotNull(model.getSoftwareSystemWithName("Software System")); - assertEquals("workspace {\n" + - "\n" + - " model {\n" + - " softwareSystem = softwareSystem \"Software System\" {\n" + - " !docs docs\n" + - " }\n" + - " }\n" + - "\n" + - "}", new String(Base64.getDecoder().decode(workspace.getProperties().get("structurizr.dsl")))); + assertEquals(""" + workspace { + + model { + !include https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/include/model.dsl + } + + }""", DslUtils.getDsl(workspace)); } @Test @@ -435,10 +407,35 @@ void test_includeLocalFile_ThrowsAnException_WhenRunningInRestrictedMode() { } } - @ParameterizedTest + @Test + @Tag("IntegrationTest") + void test_extendWorkspaceFromJsonFile() throws Exception { + String dslFile = "src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl"; + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File(dslFile)); + + Workspace workspace = parser.getWorkspace(); + Model model = workspace.getModel(); + assertEquals(CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.class, model.getImpliedRelationshipsStrategy().getClass()); + + assertEquals(1, model.getPeople().size()); + Person user = model.getPersonWithName("User"); + + assertEquals(3, workspace.getModel().getSoftwareSystems().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Software System 1"); + assertTrue(user.hasEfferentRelationshipWith(softwareSystem, "Uses")); + + assertEquals(2, softwareSystem.getContainers().size()); + assertNotNull(softwareSystem.getContainers().stream().filter(c -> c.getName().equals("Web Application 1")).findFirst()); + assertNotNull(softwareSystem.getContainers().stream().filter(c -> c.getName().equals("Web Application 2")).findFirst()); + + assertEquals("", DslUtils.getDsl(workspace)); + } + + @Test @Tag("IntegrationTest") - @ValueSource(strings = { "src/test/resources/dsl/extend/extend-workspace-from-json-file.dsl", "src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl" }) - void test_extendWorkspaceFromJson(String dslFile) throws Exception { + void test_extendWorkspaceFromJsonUrl() throws Exception { + String dslFile = "src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl"; StructurizrDslParser parser = new StructurizrDslParser(); parser.parse(new File(dslFile)); @@ -456,6 +453,26 @@ void test_extendWorkspaceFromJson(String dslFile) throws Exception { assertEquals(2, softwareSystem.getContainers().size()); assertNotNull(softwareSystem.getContainers().stream().filter(c -> c.getName().equals("Web Application 1")).findFirst()); assertNotNull(softwareSystem.getContainers().stream().filter(c -> c.getName().equals("Web Application 2")).findFirst()); + + assertEquals(""" + workspace extends https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/extend/workspace.json { + + model { + // !element with DSL identifier + !element softwareSystem1 { + webapp1 = container "Web Application 1" + } + + // !element with canonical name + !element "SoftwareSystem://Software System 1" { + webapp2 = container "Web Application 2" + } + + user -> softwareSystem1 "Uses" + softwareSystem3.webapp -> softwareSystem3.db + } + + }""", DslUtils.getDsl(workspace)); } @Test @@ -726,6 +743,9 @@ void test_pluginWithoutParameters() throws Exception { parser.parse(new File("src/test/resources/dsl/plugin-without-parameters.dsl")); assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Java")); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); } @Test @@ -734,6 +754,9 @@ void test_pluginWithParameters() throws Exception { parser.parse(new File("src/test/resources/dsl/plugin-with-parameters.dsl")); assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Java")); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); } @Test @@ -749,21 +772,27 @@ void test_script_ThrowsAnException_WhenTheParserIsInRestrictedMode() { } @Test - void test_script() throws Exception { + void test_externalScript() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); parser.parse(new File("src/test/resources/dsl/script-external.dsl")); assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Groovy")); assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Kotlin")); assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Ruby")); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); } @Test - void test_scriptWithParameters() throws Exception { + void test_externalScriptWithParameters() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); parser.parse(new File("src/test/resources/dsl/script-external-with-parameters.dsl")); assertNotNull(parser.getWorkspace().getModel().getPersonWithName("Groovy")); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); } @Test @@ -778,6 +807,9 @@ void test_inlineScript() throws Exception { assertTrue(parser.getWorkspace().getModel().getPersonWithName("User").hasTag("Groovy")); assertTrue(parser.getWorkspace().getModel().getPersonWithName("User").getRelationships().iterator().next().hasTag("Groovy")); assertEquals("Groovy", parser.getWorkspace().getViews().getSystemLandscapeViews().iterator().next().getDescription()); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); } @Test @@ -818,6 +850,9 @@ void test_docs() throws Exception { Content...""", sections.iterator().next().getContent()); assertEquals(1, component.getDocumentation().getSections().size()); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); } @Test @@ -850,6 +885,9 @@ void test_decisions() throws Exception { // log4brains decisions assertEquals(4, component.getDocumentation().getDecisions().size()); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); } @Test @@ -1092,6 +1130,9 @@ void test_imageViews_ViaFiles() throws Exception { assertEquals("image.svg", svgView.getTitle()); assertEquals("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXMtYXNjaWkiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgaGVpZ2h0PSIxMjBweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjExM3B4O2hlaWdodDoxMjBweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMTMgMTIwIiB3aWR0aD0iMTEzcHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTtzdHJva2UtZGFzaGFycmF5OjUuMCw1LjA7IiB4MT0iMjYiIHgyPSIyNiIgeTE9IjM2LjI5NjkiIHkyPSI4NS40Mjk3Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7c3Ryb2tlLWRhc2hhcnJheTo1LjAsNS4wOyIgeDE9IjgyIiB4Mj0iODIiIHkxPSIzNi4yOTY5IiB5Mj0iODUuNDI5NyIvPjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDMiIHg9IjUiIHk9IjUiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyOSIgeD0iMTIiIHk9IjI0Ljk5NTEiPkJvYjwvdGV4dD48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQzIiB4PSI1IiB5PSI4NC40Mjk3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjkiIHg9IjEyIiB5PSIxMDQuNDI0OCI+Qm9iPC90ZXh0PjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDkiIHg9IjU4IiB5PSI1Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzUiIHg9IjY1IiB5PSIyNC45OTUxIj5BbGljZTwvdGV4dD48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQ5IiB4PSI1OCIgeT0iODQuNDI5NyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjM1IiB4PSI2NSIgeT0iMTA0LjQyNDgiPkFsaWNlPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiMxODE4MTgiIHBvaW50cz0iNzAuNSw2My40Mjk3LDgwLjUsNjcuNDI5Nyw3MC41LDcxLjQyOTcsNzQuNSw2Ny40Mjk3IiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiIHgxPSIyNi41IiB4Mj0iNzYuNSIgeTE9IjY3LjQyOTciIHkyPSI2Ny40Mjk3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzAiIHg9IjMzLjUiIHk9IjYyLjM2MzgiPmhlbGxvPC90ZXh0PjwhLS1TUkM9W1N5ZkZLajJyS3QzQ29LbkVMUjFJbzRaRG9TYTcwMDAwXS0tPjwvZz48L3N2Zz4=", svgView.getContent()); assertEquals("image/svg+xml", svgView.getContentType()); + + // check that source isn't retained + assertEquals("", DslUtils.getDsl(workspace)); } @Test @@ -1127,6 +1168,44 @@ void test_imageViews_ViaUrls() throws Exception { assertEquals("image.svg", svgView.getTitle()); assertEquals("https://raw.githubusercontent.com/structurizr/java/master/structurizr-dsl/src/test/resources/dsl/image-views/image.svg", svgView.getContent()); assertEquals("image/svg+xml", svgView.getContentType()); + + // check that source is retained + assertEquals(""" + workspace { + + views { + properties { + "plantuml.url" "http://localhost:7777" + "plantuml.format" "svg" + "mermaid.url" "http://localhost:8888" + "mermaid.format" "svg" + "mermaid.compress" "false" + "kroki.url" "http://localhost:9999" + "kroki.format" "svg" + } + + image * "plantuml" { + plantuml https://raw.githubusercontent.com/structurizr/java/master/structurizr-dsl/src/test/resources/dsl/image-views/diagram.puml + } + + image * "mermaid" { + mermaid https://raw.githubusercontent.com/structurizr/java/master/structurizr-dsl/src/test/resources/dsl/image-views/diagram.mmd + } + + image * "kroki" { + kroki graphviz https://raw.githubusercontent.com/structurizr/java/master/structurizr-dsl/src/test/resources/dsl/image-views/diagram.dot + } + + image * "png" { + image https://raw.githubusercontent.com/structurizr/java/master/structurizr-dsl/src/test/resources/dsl/image-views/image.png + } + + image * "svg" { + image https://raw.githubusercontent.com/structurizr/java/master/structurizr-dsl/src/test/resources/dsl/image-views/image.svg + } + } + + }""", DslUtils.getDsl(workspace)); } @Test @@ -1429,9 +1508,8 @@ void springPetClinic() throws Exception { assertNotNull(ownerController.getEfferentRelationshipWith(ownerRepository)); assertNotNull(vetController.getEfferentRelationshipWith(vetRepository)); - // this checks that the component forEach { ... } lines don't get repeated in the outputted DSL source - String content = Files.readString(workspaceFile.toPath()); - assertEquals(content, new String(Base64.getDecoder().decode(parser.getWorkspace().getProperties().get("structurizr.dsl")))); + // checks that source isn't retained + assertEquals("", DslUtils.getDsl(workspace)); } else { System.out.println("Skipping Spring PetClinic example..."); @@ -1475,18 +1553,12 @@ void test_sourceIsRetained() throws Exception { }""", DslUtils.getDsl(workspace)); + // source not retained because workspace extends a file-based resource File childDslFile = new File("src/test/resources/dsl/source-child.dsl"); parser = new StructurizrDslParser(); parser.parse(childDslFile); workspace = parser.getWorkspace(); - assertEquals(""" -workspace extends source-parent.dsl { - - model { - b = softwareSystem "B" - } - -}""", DslUtils.getDsl(workspace)); + assertEquals("", DslUtils.getDsl(workspace)); } @Test @@ -1625,11 +1697,27 @@ void test_textBlock() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); parser.parse(new File("src/test/resources/dsl/text-block.dsl")); - SoftwareSystem softwareSystem = parser.getWorkspace().getModel().getSoftwareSystemWithName("Name"); + workspace = parser.getWorkspace(); + SoftwareSystem softwareSystem = workspace.getModel().getSoftwareSystemWithName("Name"); assertEquals(""" - Line 1 - Line 2 - Line 3""", softwareSystem.getDescription()); + + assertEquals(""" + workspace { + + model { + softwareSystem = softwareSystem "Name" { + description ""\" + - Line 1 + - Line 2 + - Line 3 + ""\" + } + } + + }""", DslUtils.getDsl(workspace)); } @Test diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java index cc5cad518..3a7e7e41c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java @@ -7,12 +7,12 @@ class IncludeParserTests extends AbstractTests { - private IncludeParser parser = new IncludeParser(); + private final IncludeParser parser = new IncludeParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(new IncludedDslContext(null), tokens("!include", "file", "extra")); + parser.parse(context(), null, tokens("!include", "file", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: !include ", e.getMessage()); @@ -22,7 +22,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenAFileIsNotSpecified() { try { - parser.parse(new IncludedDslContext(null), tokens("!include")); + parser.parse(context(), null, tokens("!include")); fail(); } catch (Exception e) { assertEquals("Expected: !include ", e.getMessage()); From f33f7edb4ba852b8afb0b263cf807d775a71fe2f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 11 Sep 2025 09:19:27 +0100 Subject: [PATCH 370/418] theme should not be available when running in restricted mode. --- .../structurizr/dsl/StructurizrDslParser.java | 4 +-- .../java/com/structurizr/dsl/ThemeParser.java | 36 +++++++++++-------- .../com/structurizr/dsl/ThemeParserTests.java | 33 +++++++++++------ 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 7eb8bd7a7..5561e2e17 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1114,10 +1114,10 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new DynamicViewRelationshipParser().parseUrl(getContext(DynamicViewRelationshipContext.class), tokens.withoutContextStartToken()); } else if (THEME_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { - new ThemeParser().parseTheme(getContext(), dslFile, tokens); + new ThemeParser().parseTheme(getContext(), dslFile, tokens, restricted); } else if (THEMES_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { - new ThemeParser().parseThemes(getContext(), dslFile, tokens); + new ThemeParser().parseThemes(getContext(), dslFile, tokens, restricted); } else if (TERMINOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { startContext(new TerminologyDslContext()); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java index c16c531c5..d228972ca 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java @@ -12,7 +12,7 @@ final class ThemeParser extends AbstractParser { private final static int FIRST_THEME_INDEX = 1; - void parseTheme(DslContext context, File dslFile, Tokens tokens) { + void parseTheme(DslContext context, File dslFile, Tokens tokens, boolean restricted) { // theme if (tokens.hasMoreThan(FIRST_THEME_INDEX)) { throw new RuntimeException("Too many tokens, expected: theme "); @@ -22,21 +22,21 @@ void parseTheme(DslContext context, File dslFile, Tokens tokens) { throw new RuntimeException("Expected: theme "); } - addTheme(context, dslFile, tokens.get(FIRST_THEME_INDEX)); + addTheme(context, dslFile, tokens.get(FIRST_THEME_INDEX), restricted); } - void parseThemes(DslContext context, File dslFile, Tokens tokens) { + void parseThemes(DslContext context, File dslFile, Tokens tokens, boolean restricted) { // themes [url|file] ... [url|file] if (!tokens.includes(FIRST_THEME_INDEX)) { throw new RuntimeException("Expected: themes [url|file] ... [url|file]"); } for (int i = FIRST_THEME_INDEX; i < tokens.size(); i++) { - addTheme(context, dslFile, tokens.get(i)); + addTheme(context, dslFile, tokens.get(i), restricted); } } - private void addTheme(DslContext context, File dslFile, String theme) { + private void addTheme(DslContext context, File dslFile, String theme, boolean restricted) { if (DEFAULT_THEME_NAME.equalsIgnoreCase(theme)) { theme = DEFAULT_THEME_URL; } @@ -45,20 +45,26 @@ private void addTheme(DslContext context, File dslFile, String theme) { // this adds the theme to the list of theme URLs in the workspace context.getWorkspace().getViews().getConfiguration().addTheme(theme); } else { - // this inlines the file-based theme into the workspace - File file = new File(dslFile.getParentFile(), theme); - if (file.exists()) { - if (file.isFile()) { - try { - ThemeUtils.inlineTheme(context.getWorkspace(), file); - } catch (Exception e) { - throw new RuntimeException("Error loading theme from " + file.getAbsolutePath() + ": " + e.getMessage()); + if (!restricted) { + context.setDslPortable(false); + + // this inlines the file-based theme into the workspace + File file = new File(dslFile.getParentFile(), theme); + if (file.exists()) { + if (file.isFile()) { + try { + ThemeUtils.inlineTheme(context.getWorkspace(), file); + } catch (Exception e) { + throw new RuntimeException("Error loading theme from " + file.getAbsolutePath() + ": " + e.getMessage()); + } + } else { + throw new RuntimeException(file.getAbsolutePath() + " is not a file"); } } else { - throw new RuntimeException(file.getAbsolutePath() + " is not a file"); + throw new RuntimeException(file.getAbsolutePath() + " does not exist"); } } else { - throw new RuntimeException(file.getAbsolutePath() + " does not exist"); + throw new RuntimeException("File-based themes are not supported when the DSL parser is running in restricted mode"); } } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java index 3a3c9312b..5a0075aa6 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java @@ -13,7 +13,7 @@ class ThemeParserTests extends AbstractTests { @Test void test_parseTheme_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parseTheme(context(), null, tokens("theme", "url", "extra")); + parser.parseTheme(context(), null, tokens("theme", "url", "extra"), false); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: theme ", e.getMessage()); @@ -23,7 +23,7 @@ void test_parseTheme_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseTheme_ThrowsAnException_WhenNoThemeIsSpecified() { try { - parser.parseTheme(context(), null, tokens("theme")); + parser.parseTheme(context(), null, tokens("theme"), false); fail(); } catch (Exception e) { assertEquals("Expected: theme ", e.getMessage()); @@ -32,7 +32,7 @@ void test_parseTheme_ThrowsAnException_WhenNoThemeIsSpecified() { @Test void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { - parser.parseTheme(context(), null, tokens("theme", "http://example.com/1")); + parser.parseTheme(context(), null, tokens("theme", "http://example.com/1"), false); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -40,7 +40,7 @@ void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { @Test void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { - parser.parseTheme(context(), null, tokens("theme", "default")); + parser.parseTheme(context(), null, tokens("theme", "default"), false); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); @@ -49,7 +49,7 @@ void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { @Test void test_parseThemes_ThrowsAnException_WhenNoThemesAreSpecified() { try { - parser.parseThemes(context(), null, tokens("themes")); + parser.parseThemes(context(), null, tokens("themes"), false); fail(); } catch (Exception e) { assertEquals("Expected: themes [url|file] ... [url|file]", e.getMessage()); @@ -58,7 +58,7 @@ void test_parseThemes_ThrowsAnException_WhenNoThemesAreSpecified() { @Test void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { - parser.parseThemes(context(), null, tokens("themes", "http://example.com/1")); + parser.parseThemes(context(), null, tokens("themes", "http://example.com/1"), false); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -66,7 +66,7 @@ void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { @Test void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { - parser.parseThemes(context(), null, tokens("themes", "http://example.com/1", "http://example.com/2", "http://example.com/3")); + parser.parseThemes(context(), null, tokens("themes", "http://example.com/1", "http://example.com/2", "http://example.com/3"), false); assertEquals(3, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -76,7 +76,7 @@ void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { @Test void test_parseThemes_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { - parser.parseThemes(context(), null, tokens("themes", "default")); + parser.parseThemes(context(), null, tokens("themes", "default"), false); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); @@ -86,7 +86,7 @@ void test_parseThemes_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { void test_parseTheme_ThrowsAnException_WhenTheThemeFileDoesNotExist() { File dslFile = new File("src/test/resources/themes/workspace.dsl"); try { - parser.parseTheme(context(), dslFile, tokens("theme", "my-theme.json")); + parser.parseTheme(context(), dslFile, tokens("theme", "my-theme.json"), false); fail(); } catch (Exception e) { assertTrue(e.getMessage().endsWith("/src/test/resources/themes/my-theme.json does not exist")); @@ -97,7 +97,7 @@ void test_parseTheme_ThrowsAnException_WhenTheThemeFileDoesNotExist() { void test_parseTheme_ThrowsAnException_WhenTheThemeFileIsADirectory() { File dslFile = new File("src/test/resources/workspace.dsl"); try { - parser.parseTheme(context(), dslFile, tokens("theme", "themes")); + parser.parseTheme(context(), dslFile, tokens("theme", "themes"), false); fail(); } catch (Exception e) { assertTrue(e.getMessage().endsWith("/src/test/resources/themes is not a file")); @@ -107,11 +107,22 @@ void test_parseTheme_ThrowsAnException_WhenTheThemeFileIsADirectory() { @Test void test_parseTheme_InlinesTheTheme_WhenAThemeFileIsSpecified() { File dslFile = new File("src/test/resources/themes/workspace.dsl"); - parser.parseTheme(context(), dslFile, tokens("theme", "theme.json")); + parser.parseTheme(context(), dslFile, tokens("theme", "theme.json"), false); assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); assertEquals("#ff0000", workspace.getViews().getConfiguration().getStyles().getElementStyle("Tag").getBackground()); assertEquals("#00ff00", workspace.getViews().getConfiguration().getStyles().getRelationshipStyle("Tag").getColor()); } + @Test + void test_parseTheme_ThrowsAnException_WhenAThemeFileIsSpecifiedAndTheParserIsRunningInRestrictedMode() { + try { + File dslFile = new File("src/test/resources/themes/workspace.dsl"); + parser.parseTheme(context(), dslFile, tokens("theme", "theme.json"), true); + fail(); + } catch (Exception e) { + assertEquals("File-based themes are not supported when the DSL parser is running in restricted mode", e.getMessage()); + } + } + } \ No newline at end of file From aaddf56b1aad63ef4ad1b8d5a5917c6a53009f83 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 14 Sep 2025 11:10:12 +0100 Subject: [PATCH 371/418] Removes support for deprecated enterprise and location concepts. --- changelog.md | 4 +++- .../api/BackwardsCompatibilityTests.java | 17 --------------- .../java/com/structurizr/model/Model.java | 12 ----------- .../java/com/structurizr/model/Person.java | 16 -------------- .../com/structurizr/model/SoftwareSystem.java | 21 ------------------- .../export/AbstractDiagramExporter.java | 3 --- .../structurizr/export/dot/DOTExporter.java | 21 ------------------- .../mermaid/MermaidDiagramExporter.java | 15 ------------- .../export/plantuml/C4PlantUMLExporter.java | 13 ------------ .../plantuml/StructurizrPlantUMLExporter.java | 20 ------------------ .../WebSequenceDiagramsExporter.java | 8 ------- 11 files changed, 3 insertions(+), 147 deletions(-) diff --git a/changelog.md b/changelog.md index 068d5d942..281ac63aa 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,8 @@ ## v5.0.0 (unreleased) -- structurizr-java: Fixes https://github.com/structurizr/java/issues/437 (Make ComponentFinder.run() not fail on empty Set). +- structurizr-core: Removes support for deprecated enterprise and location concepts. +- structurizr-component: Fixes https://github.com/structurizr/java/issues/437 (Make ComponentFinder.run() not fail on empty Set). - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). - structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. - structurizr-dsl: Adds a `Bucket` shape. @@ -16,6 +17,7 @@ - structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. - structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. - structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). +- structurizr-export: Removes support for deprecated enterprise and location concepts. - structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. diff --git a/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java index 6ae4cb020..ecdc97a12 100644 --- a/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java +++ b/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java @@ -22,23 +22,6 @@ void test() throws Exception { } } - @Test - void enterprise_and_location() throws Exception { - File file = new File(PATH_TO_WORKSPACE_FILES, "structurizr-36141-workspace.json"); - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(file); - - assertEquals("Big Bank plc", workspace.getModel().getEnterprise().getName()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); - assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); - - // make sure enterprise and location information is not lost when going to/from JSON - workspace = WorkspaceUtils.fromJson(WorkspaceUtils.toJson(workspace, false)); - - assertEquals("Big Bank plc", workspace.getModel().getEnterprise().getName()); - assertEquals(Location.Internal, workspace.getModel().getPersonWithName("Back Office Staff").getLocation()); - assertEquals(Location.External, workspace.getModel().getPersonWithName("Personal Banking Customer").getLocation()); - } - @Test void documentation() throws Exception { Workspace workspace = new Workspace("Name", "Description"); diff --git a/structurizr-core/src/main/java/com/structurizr/model/Model.java b/structurizr-core/src/main/java/com/structurizr/model/Model.java index 95964fa88..da7058f49 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Model.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -23,8 +23,6 @@ public final class Model implements PropertyHolder { private final Set relationships = new TreeSet<>(); private final Map relationshipsById = new HashMap<>(); - private Enterprise enterprise; - private Set people = new TreeSet<>(); private Set softwareSystems = new TreeSet<>(); private Set deploymentNodes = new TreeSet<>(); @@ -37,16 +35,6 @@ public final class Model implements PropertyHolder { Model() { } - @Deprecated - public Enterprise getEnterprise() { - return enterprise; - } - - @Deprecated - void setEnterprise(Enterprise enterprise) { - this.enterprise = enterprise; - } - /** * Creates a software system (with an unspecified location) and adds it to the model. * diff --git a/structurizr-core/src/main/java/com/structurizr/model/Person.java b/structurizr-core/src/main/java/com/structurizr/model/Person.java index dd9916897..84d445b1c 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/Person.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Person.java @@ -12,8 +12,6 @@ */ public final class Person extends StaticStructureElement { - private Location location = Location.Unspecified; - @Override @JsonIgnore public Element getParent() { @@ -23,20 +21,6 @@ public Element getParent() { Person() { } - @Deprecated - public Location getLocation() { - return location; - } - - @Deprecated - void setLocation(Location location) { - if (location != null) { - this.location = location; - } else { - this.location = Location.Unspecified; - } - } - @Override public String getCanonicalName() { return new CanonicalNameGenerator().generate(this); diff --git a/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java index 005cdda30..13d7a450c 100644 --- a/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java +++ b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java @@ -16,8 +16,6 @@ */ public final class SoftwareSystem extends StaticStructureElement implements Documentable { - private Location location = Location.Unspecified; - private Set containers = new TreeSet<>(); private Documentation documentation = new Documentation(); @@ -36,25 +34,6 @@ public Element getParent() { SoftwareSystem() { } - @Deprecated - public Location getLocation() { - return location; - } - - /** - * Sets the location of this software system. - * - * @param location a Location instance - */ - @Deprecated - void setLocation(Location location) { - if (location != null) { - this.location = location; - } else { - this.location = Location.Unspecified; - } - } - void add(Container container) { containers.add(container); } diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java index 56cef1709..f3d1839a1 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -602,9 +602,6 @@ protected void writeRelationships(ModelView view, IndentingWriter writer) { protected abstract void writeHeader(ModelView view, IndentingWriter writer); protected abstract void writeFooter(ModelView view, IndentingWriter writer); - protected abstract void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer); - protected abstract void endEnterpriseBoundary(ModelView view, IndentingWriter writer); - protected abstract void startGroupBoundary(ModelView view, String group, IndentingWriter writer); protected abstract void endGroupBoundary(ModelView view, IndentingWriter writer); diff --git a/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java index b4671c4ca..26c7aa549 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java @@ -78,27 +78,6 @@ protected void writeFooter(ModelView view, IndentingWriter writer) { writer.writeLine("}"); } - @Override - protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { - writer.writeLine("subgraph cluster_enterprise {"); - - writer.indent(); - writer.writeLine("margin=" + clusterInternalMargin); - writer.writeLine(String.format("label=<
%s

[Enterprise]>", enterpriseName)); - writer.writeLine("labelloc=b"); - writer.writeLine("color=\"#444444\""); - writer.writeLine("fontcolor=\"#444444\""); - writer.writeLine("fillcolor=\"#ffffff\""); - writer.writeLine(); - } - - @Override - protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { - writer.outdent(); - writer.writeLine("}"); - writer.writeLine(); - } - @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { String color = "#cccccc"; diff --git a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java index cb0569258..3082176a3 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java @@ -78,21 +78,6 @@ protected void writeFooter(ModelView view, IndentingWriter writer) { writer.outdent(); } - @Override - protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { - writer.writeLine("subgraph enterprise [\"" + enterpriseName + "\"]"); - writer.indent(); - writer.writeLine("style enterprise fill:#ffffff,stroke:#444444,color:#444444"); - writer.writeLine(); - } - - @Override - protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { - writer.outdent(); - writer.writeLine("end"); - writer.writeLine(); - } - @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { groupId++; diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index 7566c7b32..cdd26bafc 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -270,19 +270,6 @@ protected void writeFooter(ModelView view, IndentingWriter writer) { super.writeFooter(view, writer); } - @Override - protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { - writer.writeLine(String.format("Enterprise_Boundary(enterprise, \"%s\") {", enterpriseName)); - writer.indent(); - } - - @Override - protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { - writer.outdent(); - writer.writeLine("}"); - writer.writeLine(); - } - @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { groupId++; diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 2c8d3c775..230099a1c 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -146,26 +146,6 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { writer.writeLine(); } - @Override - protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { - if (!renderAsSequenceDiagram(view)) { - writer.writeLine(String.format("rectangle \"%s\" <> {", enterpriseName)); - writer.indent(); - writer.writeLine("skinparam RectangleBorderColor<> #444444"); - writer.writeLine("skinparam RectangleFontColor<> #444444"); - writer.writeLine(); - } - } - - @Override - protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { - if (!renderAsSequenceDiagram(view)) { - writer.outdent(); - writer.writeLine("}"); - writer.writeLine(); - } - } - @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { groupId++; diff --git a/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java index b731844a5..e05832792 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java @@ -83,14 +83,6 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { protected void writeFooter(ModelView view, IndentingWriter writer) { } - @Override - protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { - } - - @Override - protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { - } - @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { } From f797cff1d93ec58ec9cead20840dbe4a9dd79dfa Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 16 Sep 2025 16:59:06 +0100 Subject: [PATCH 372/418] Adds support for an "image.inline" property. --- changelog.md | 2 +- .../dsl/ImageViewContentParser.java | 8 ++-- .../diagrams/image/ImageImporter.java | 44 +++++++++++++++++++ .../diagrams/image/ImageImporterTests.java | 37 ++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java create mode 100644 structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java diff --git a/changelog.md b/changelog.md index 281ac63aa..8f40fbc6e 100644 --- a/changelog.md +++ b/changelog.md @@ -18,7 +18,7 @@ - structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. - structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). - structurizr-export: Removes support for deprecated enterprise and location concepts. -- structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace. +- structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, `kroki.inline`, and `image.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. - structurizr-inspection: Fixes `model.deploymentnode.technology` (it was checking the description property rather than technology). diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index 5c049f4f6..2264a16b8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -2,6 +2,7 @@ import com.structurizr.export.mermaid.MermaidDiagramExporter; import com.structurizr.export.plantuml.StructurizrPlantUMLExporter; +import com.structurizr.importer.diagrams.image.ImageImporter; import com.structurizr.importer.diagrams.kroki.KrokiImporter; import com.structurizr.importer.diagrams.mermaid.MermaidImporter; import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; @@ -12,6 +13,7 @@ import com.structurizr.view.View; import java.io.File; +import java.net.URL; final class ImageViewContentParser extends AbstractParser { @@ -193,15 +195,13 @@ void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { try { if (Url.isUrl(source)) { - context.getView().setContent(source); - context.getView().setTitle(source.substring(source.lastIndexOf("/")+1)); + new ImageImporter().importDiagram(context.getView(), source); } else { if (!restricted) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { context.setDslPortable(false); - context.getView().setContent(ImageUtils.getImageAsDataUri(file)); - context.getView().setTitle(file.getName()); + new ImageImporter().importDiagram(context.getView(), file); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java new file mode 100644 index 000000000..dbdf10b60 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java @@ -0,0 +1,44 @@ +package com.structurizr.importer.diagrams.image; + +import com.structurizr.importer.diagrams.AbstractDiagramImporter; +import com.structurizr.util.ImageUtils; +import com.structurizr.view.ImageView; + +import java.io.File; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public class ImageImporter extends AbstractDiagramImporter { + + public static final String IMAGE_INLINE_PROPERTY = "image.inline"; + + public void importDiagram(ImageView view, File file) throws Exception { + view.setContent(ImageUtils.getImageAsDataUri(file)); + view.setContentType(ImageUtils.getContentType(file)); + view.setTitle(file.getName()); + } + + public void importDiagram(ImageView view, String url) throws Exception { + String inline = getViewOrViewSetProperty(view, IMAGE_INLINE_PROPERTY); + if ("true".equals(inline)) { + String imageFormat = ImageUtils.getContentType(url); + if (!imageFormat.equals(CONTENT_TYPE_IMAGE_PNG) && !imageFormat.equals(CONTENT_TYPE_IMAGE_SVG)) { + throw new IllegalArgumentException(String.format("Found %s - expected a format of %s or %s", imageFormat, PNG_FORMAT, SVG_FORMAT)); + } + + if (imageFormat.equals(CONTENT_TYPE_IMAGE_SVG)) { + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + } else { + view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + } + + view.setContentType(imageFormat); + } else { + view.setContent(url); + } + + view.setTitle(url.substring(url.lastIndexOf("/")+1)); + } + +} \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java new file mode 100644 index 000000000..4a916217f --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java @@ -0,0 +1,37 @@ +package com.structurizr.importer.diagrams.image; + +import com.structurizr.Workspace; +import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ImageImporterTests { + + @Test + public void importDiagram_Url() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + ImageView view = workspace.getViews().createImageView("key"); + + new ImageImporter().importDiagram(view, "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png"); + assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png", view.getContent()); + assertNull(view.getContentType()); + assertEquals("alexa-for-business.png", view.getTitle()); + } + + @Test + @Tag("IntegrationTest") + public void importDiagram_Url_Inline() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().getConfiguration().addProperty(ImageImporter.IMAGE_INLINE_PROPERTY, "true"); + ImageView view = workspace.getViews().createImageView("key"); + + new ImageImporter().importDiagram(view, "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png"); + assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("image/png", view.getContentType()); + assertEquals("alexa-for-business.png", view.getTitle()); + } + +} \ No newline at end of file From b3fb2c3ac78250828fcb61c6f6a22dac62d25ddb Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 16 Sep 2025 17:08:40 +0100 Subject: [PATCH 373/418] Removes support for deprecated enterprise and location concepts. --- .../autolayout/graphviz/DOTExporter.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java index 013cb3446..a28303f2b 100644 --- a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java @@ -53,20 +53,6 @@ protected void writeFooter(ModelView view, IndentingWriter writer) { writer.writeLine("}"); } - @Override - protected void startEnterpriseBoundary(ModelView view, String enterpriseName, IndentingWriter writer) { - writer.writeLine("subgraph cluster_enterprise {"); - writer.indent(); - writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); - } - - @Override - protected void endEnterpriseBoundary(ModelView view, IndentingWriter writer) { - writer.outdent(); - writer.writeLine("}"); - writer.writeLine(); - } - @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { writer.writeLine("subgraph \"cluster_group_" + (groupId++) + "\" {"); From 53656dc1b01ed94ab1ffce178e4427da0eb74305 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 17 Sep 2025 08:12:42 +0100 Subject: [PATCH 374/418] Adds some constructor/method versions without description parameters. --- .../main/java/com/structurizr/Workspace.java | 9 ++ .../java/com/structurizr/view/ViewSet.java | 145 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/structurizr-core/src/main/java/com/structurizr/Workspace.java b/structurizr-core/src/main/java/com/structurizr/Workspace.java index 46e2b7fdd..0b785e708 100644 --- a/structurizr-core/src/main/java/com/structurizr/Workspace.java +++ b/structurizr-core/src/main/java/com/structurizr/Workspace.java @@ -30,6 +30,15 @@ public final class Workspace extends AbstractWorkspace implements Documentable { Workspace() { } + /** + * Creates a new workspace. + * + * @param name the name of the workspace + */ + public Workspace(String name) { + this(name, ""); + } + /** * Creates a new workspace. * diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java index ff395e202..f597f4027 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -53,6 +53,18 @@ public final class ViewSet { this.model = model; } + /** + * Creates a custom view. + * + * @param key the key for the view (must be unique) + * @param title a title of the view + * @return a CustomView object + * @throws IllegalArgumentException if the key is not unique + */ + public CustomView createCustomView(String key, String title) { + return createCustomView(key, title, ""); + } + /** * Creates a custom view. * @@ -80,6 +92,17 @@ public CustomView createCustomView(String key, String title, String description) return view; } + /** + * Creates a system landscape view. + * + * @param key the key for the view (must be unique) + * @return a SystemLandscapeView object + * @throws IllegalArgumentException if the key is not unique + */ + public SystemLandscapeView createSystemLandscapeView(String key) { + return createSystemLandscapeView(key, ""); + } + /** * Creates a system landscape view. * @@ -107,6 +130,18 @@ public SystemLandscapeView createSystemLandscapeView(String key, String descript return view; } + /** + * Creates a system context view, where the scope of the view is the specified software system. + * + * @param softwareSystem the SoftwareSystem object representing the scope of the view + * @param key the key for the view (must be unique) + * @return a SystemContextView object + * @throws IllegalArgumentException if the software system is null or the key is not unique + */ + public SystemContextView createSystemContextView(SoftwareSystem softwareSystem, String key) { + return createSystemContextView(softwareSystem, key, ""); + } + /** * Creates a system context view, where the scope of the view is the specified software system. * @@ -135,6 +170,18 @@ public SystemContextView createSystemContextView(SoftwareSystem softwareSystem, return view; } + /** + * Creates a container view, where the scope of the view is the specified software system. + * + * @param softwareSystem the SoftwareSystem object representing the scope of the view + * @param key the key for the view (must be unique) + * @return a ContainerView object + * @throws IllegalArgumentException if the software system is null or the key is not unique + */ + public ContainerView createContainerView(SoftwareSystem softwareSystem, String key) { + return createContainerView(softwareSystem, key, ""); + } + /** * Creates a container view, where the scope of the view is the specified software system. * @@ -163,6 +210,18 @@ public ContainerView createContainerView(SoftwareSystem softwareSystem, String k return view; } + /** + * Creates a component view, where the scope of the view is the specified container. + * + * @param container the Container object representing the scope of the view + * @param key the key for the view (must be unique) + * @return a ContainerView object + * @throws IllegalArgumentException if the container is null or the key is not unique + */ + public ComponentView createComponentView(Container container, String key) { + return createComponentView(container, key, ""); + } + /** * Creates a component view, where the scope of the view is the specified container. * @@ -191,6 +250,17 @@ public ComponentView createComponentView(Container container, String key, String return view; } + /** + * Creates a dynamic view. + * + * @param key the key for the view (must be unique) + * @return a DynamicView object + * @throws IllegalArgumentException if the key is not unique + */ + public DynamicView createDynamicView(String key) { + return createDynamicView(key, ""); + } + /** * Creates a dynamic view. * @@ -217,6 +287,25 @@ public DynamicView createDynamicView(String key, String description) { return view; } + /** + * Creates a dynamic view, where the scope is the specified software system. The following + * elements can be added to the resulting view: + * + *
    + *
  • People
  • + *
  • Software systems
  • + *
  • Containers that reside inside the specified software system
  • + *
+ * + * @param softwareSystem the SoftwareSystem object representing the scope of the view + * @param key the key for the view (must be unique) + * @return a DynamicView object + * @throws IllegalArgumentException if the software system is null or the key is not unique + */ + public DynamicView createDynamicView(SoftwareSystem softwareSystem, String key) { + return createDynamicView(softwareSystem, key, ""); + } + /** * Creates a dynamic view, where the scope is the specified software system. The following * elements can be added to the resulting view: @@ -252,6 +341,26 @@ public DynamicView createDynamicView(SoftwareSystem softwareSystem, String key, return view; } + /** + * Creates a dynamic view, where the scope is the specified container. The following + * elements can be added to the resulting view: + * + *
    + *
  • People
  • + *
  • Software systems
  • + *
  • Containers with the same parent software system as the specified container
  • + *
  • Components within the specified container
  • + *
+ * + * @param container the Container object representing the scope of the view + * @param key the key for the view (must be unique) + * @return a DynamicView object + * @throws IllegalArgumentException if the container is null or the key is not unique + */ + public DynamicView createDynamicView(Container container, String key) { + return createDynamicView(container, key, ""); + } + /** * Creates a dynamic view, where the scope is the specified container. The following * elements can be added to the resulting view: @@ -288,6 +397,17 @@ public DynamicView createDynamicView(Container container, String key, String des return view; } + /** + * Creates a deployment view. + * + * @param key the key for the deployment view (must be unique) + * @return a DeploymentView object + * @throws IllegalArgumentException if the key is not unique + */ + public DeploymentView createDeploymentView(String key) { + return createDeploymentView(key, ""); + } + /** * Creates a deployment view. * @@ -314,6 +434,18 @@ public DeploymentView createDeploymentView(String key, String description) { return view; } + /** + * Creates a deployment view, where the scope of the view is the specified software system. + * + * @param softwareSystem the SoftwareSystem object representing the scope of the view + * @param key the key for the deployment view (must be unique) + * @return a DeploymentView object + * @throws IllegalArgumentException if the software system is null or the key is not unique + */ + public DeploymentView createDeploymentView(SoftwareSystem softwareSystem, String key) { + return createDeploymentView(softwareSystem, key, ""); + } + /** * Creates a deployment view, where the scope of the view is the specified software system. * @@ -342,6 +474,19 @@ public DeploymentView createDeploymentView(SoftwareSystem softwareSystem, String return view; } + /** + * Creates a FilteredView on top of an existing static view. + * + * @param view the static view to base the FilteredView upon + * @param key the key for the filtered view (must be unique) + * @param mode whether to Include or Exclude elements/relationships based upon their tag + * @param tags the tags to include or exclude + * @return a FilteredView object + */ + public FilteredView createFilteredView(StaticView view, String key, FilterMode mode, String... tags) { + return createFilteredView(view, key, "", mode, tags); + } + /** * Creates a FilteredView on top of an existing static view. * From b40cdfe68e8734db65935855968dad37b989f1a0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 17 Sep 2025 08:43:25 +0100 Subject: [PATCH 375/418] . --- .../src/test/java/com/structurizr/view/ViewSetTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java index 675345cf7..94591bb7e 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java @@ -273,7 +273,7 @@ void createComponentView() { @Test void createDynamicView_GeneratesAKey_WhenANullKeyIsSpecified() { Workspace workspace = new Workspace("Name", "Description"); - DynamicView view = workspace.getViews().createDynamicView(null, "Description"); + DynamicView view = workspace.getViews().createDynamicView(null); assertEquals("Dynamic-001", view.getKey()); assertTrue(view.isGeneratedKey()); } @@ -397,7 +397,7 @@ void createDynamicViewForContainer() { @Test void createDeploymentView_GeneratesAKey_WhenANullKeyIsSpecified() { Workspace workspace = new Workspace("Name", "Description"); - DeploymentView view = workspace.getViews().createDeploymentView(null, "Description"); + DeploymentView view = workspace.getViews().createDeploymentView(null); assertEquals("Deployment-001", view.getKey()); assertTrue(view.isGeneratedKey()); } From bb2428a74c21c6e944aec226398a1bc47917dc63 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 17 Sep 2025 09:06:10 +0100 Subject: [PATCH 376/418] *PlantUML exporters: replaces skinparams with styles + adds support for dark mode exports. --- changelog.md | 2 + .../autolayout/graphviz/DOTExporterTests.java | 357 +- .../GraphvizAutomaticLayoutTests.java | 35 +- .../com/structurizr/view/AbstractStyle.java | 56 +- .../com/structurizr/view/ElementStyle.java | 29 +- .../structurizr/view/RelationshipStyle.java | 22 +- .../java/com/structurizr/view/Styles.java | 141 +- .../com/structurizr/view/StylesTests.java | 98 +- .../java/com/structurizr/dsl/DslTests.java | 2 +- .../dsl/ImageViewContentParserTests.java | 5 +- .../src/test/resources/dsl/include-url.dsl | 2 +- .../export/AbstractDiagramExporter.java | 193 +- .../structurizr/export/IndentingWriter.java | 9 +- .../plantuml/AbstractPlantUMLExporter.java | 92 +- .../export/plantuml/C4PlantUMLExporter.java | 83 +- .../plantuml/PlantUMLBoundaryStyle.java | 81 + .../export/plantuml/PlantUMLElementStyle.java | 107 + .../export/plantuml/PlantUMLGroupStyle.java | 81 + .../export/plantuml/PlantUMLLegendStyle.java | 44 + .../plantuml/PlantUMLRelationshipStyle.java | 71 + .../export/plantuml/PlantUMLRootStyle.java | 52 + .../export/plantuml/PlantUMLStyle.java | 26 + .../plantuml/StructurizrPlantUMLExporter.java | 582 +-- .../export/dot/36141-Components.dot | 43 - .../export/dot/36141-Containers.dot | 37 - .../dot/36141-DevelopmentDeployment.dot | 97 - .../export/dot/36141-LiveDeployment.dot | 152 - .../structurizr/export/dot/36141-SignIn.dot | 29 - .../export/dot/36141-SystemContext.dot | 28 - .../export/dot/36141-SystemLandscape.dot | 36 - .../dot/54915-AmazonWebServicesDeployment.dot | 75 - .../export/dot/DOTDiagramExporterTests.java | 1076 ++++- .../export/dot/groups-Components.dot | 35 - .../export/dot/groups-Containers.dot | 35 - .../export/dot/groups-SystemLandscape.dot | 49 - .../structurizr/export/dot/nested-groups.dot | 69 - .../export/ilograph/36141.ilograph | 616 --- .../export/ilograph/54915.ilograph | 127 - .../ilograph/IlographExporterTests.java | 792 +++- .../54915-AmazonWebServicesDeployment.mmd | 48 - .../mermaid/MermaidDiagramExporterTests.java | 477 ++- .../export/mermaid/groups-Components.mmd | 26 - .../export/mermaid/groups-Containers.mmd | 26 - .../export/mermaid/groups-SystemLandscape.mmd | 34 - .../export/mermaid/nested-groups.mmd | 43 - .../C4PlantUMLDiagramExporterTests.java | 1600 ++++++-- .../plantuml/PlantUMLBoundaryStyleTests.java | 29 + .../plantuml/PlantUMLElementStyleTests.java | 32 + .../plantuml/PlantUMLGroupStyleTests.java | 31 + .../plantuml/PlantUMLLegendStyleTests.java | 23 + .../PlantUMLRelationshipStyleTests.java | 58 + .../plantuml/PlantUMLRootStyleTests.java | 53 + ...ructurizrPlantUMLDiagramExporterTests.java | 3556 +++++++++++++---- .../plantuml/c4plantuml/36141-Components.puml | 52 - .../plantuml/c4plantuml/36141-Containers.puml | 46 - .../36141-DevelopmentDeployment.puml | 55 - .../c4plantuml/36141-LiveDeployment.puml | 77 - .../c4plantuml/36141-SignIn-sequence.puml | 26 - .../plantuml/c4plantuml/36141-SignIn.puml | 36 - .../c4plantuml/36141-SystemContext.puml | 31 - .../c4plantuml/36141-SystemLandscape.puml | 40 - ...-AmazonWebServicesDeployment-WithTags.puml | 52 - ...azonWebServicesDeployment-WithoutTags.puml | 39 - .../plantuml/c4plantuml/group-styles-1.puml | 28 - .../plantuml/c4plantuml/group-styles-2.puml | 28 - .../c4plantuml/groups-Components.puml | 26 - .../c4plantuml/groups-Containers.puml | 26 - .../c4plantuml/groups-SystemLandscape.puml | 32 - .../nested-groups-with-dot-separator.puml | 26 - .../plantuml/c4plantuml/nested-groups.puml | 38 - .../printProperties-containerView.puml | 28 - .../printProperties-deploymentView.puml | 21 - .../structurizr/36141-Components.puml | 118 - .../structurizr/36141-Containers.puml | 94 - .../36141-DevelopmentDeployment.puml | 130 - .../structurizr/36141-LiveDeployment.puml | 192 - .../structurizr/36141-SignIn-sequence.puml | 49 - .../plantuml/structurizr/36141-SignIn.puml | 62 - .../structurizr/36141-SystemContext.puml | 59 - .../structurizr/36141-SystemLandscape.puml | 85 - ...15-AmazonWebServicesDeployment-Legend.puml | 102 - .../54915-AmazonWebServicesDeployment.puml | 113 - ...onent-view-with-external-components-1.puml | 56 - ...onent-view-with-external-components-2.puml | 62 - ...mic-view-container-scoped-with-groups.puml | 62 - ...ew-software-system-scoped-with-groups.puml | 62 - .../dynamic-view-unscoped-with-groups.puml | 46 - ...namic-view-with-external-components-1.puml | 56 - ...namic-view-with-external-components-2.puml | 62 - .../plantuml/structurizr/group-styles-1.puml | 60 - .../plantuml/structurizr/group-styles-2.puml | 60 - .../structurizr/groups-Components.puml | 58 - .../structurizr/groups-Containers.puml | 58 - .../structurizr/groups-SystemLandscape.puml | 72 - .../plantuml/structurizr/nested-groups.puml | 88 - .../websequencediagrams/36141-SignIn.wsd | 13 - .../WebSequenceDiagramsExporterTests.java | 33 +- .../test/resources/amazon-web-services.dsl | 83 + .../test/resources/amazon-web-services.json | 257 ++ ...36141-workspace.json => big-bank-plc.json} | 0 .../structurizr-54915-workspace.json | 353 -- .../diagrams/plantuml/PlantUMLImporter.java | 21 +- .../plantuml/PlantUMLImporterTests.java | 3 +- 103 files changed, 8143 insertions(+), 6635 deletions(-) create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyle.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLElementStyle.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLGroupStyle.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLLegendStyle.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyle.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRootStyle.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLStyle.java delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyleTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLElementStyleTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLGroupStyleTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLLegendStyleTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyleTests.java create mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRootStyleTests.java delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd create mode 100644 structurizr-export/src/test/resources/amazon-web-services.dsl create mode 100644 structurizr-export/src/test/resources/amazon-web-services.json rename structurizr-export/src/test/resources/{structurizr-36141-workspace.json => big-bank-plc.json} (100%) delete mode 100644 structurizr-export/src/test/resources/structurizr-54915-workspace.json diff --git a/changelog.md b/changelog.md index 8f40fbc6e..56f85766d 100644 --- a/changelog.md +++ b/changelog.md @@ -18,6 +18,8 @@ - structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. - structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). - structurizr-export: Removes support for deprecated enterprise and location concepts. +- structurizr-export: PlantUML exporters - replaces skinparams with styles. +- structurizr-export: PlantUML exporters - adds support for dark mode exports. - structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, `kroki.inline`, and `image.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. diff --git a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java index fc58220ce..96bdabf8f 100644 --- a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java @@ -31,17 +31,19 @@ public void test_writeCustomView() { Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box 1\"]\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: Box 2\"]\n" + - "\n" + - " 1 -> 2 [id=3]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box 1"] + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: Box 2"] + + 1 -> 2 [id=3] + + }""", content); } @Test @@ -60,18 +62,20 @@ public void test_writeSystemLandscapeView() { Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + - "\n" + - " 2 -> 3 [id=4]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: User"] + 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label="3: Software System"] + + 2 -> 3 [id=4] + + }""", content); } @Test @@ -92,26 +96,28 @@ public void test_writeSystemLandscapeViewWithGroupedElements() throws Exception Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " subgraph \"cluster_group_1\" {\n" + - " margin=25\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_2\" {\n" + - " margin=25\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + - " }\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - "\n" + - " 2 -> 3 [id=4]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph "cluster_group_1" { + margin=25 + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: User"] + } + + subgraph "cluster_group_2" { + margin=25 + 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label="3: Software System"] + } + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + + 2 -> 3 [id=4] + + }""", content); } @Test @@ -194,18 +200,20 @@ public void test_writeSystemLandscapeViewInGermanLocale() throws Exception { Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + - "\n" + - " 2 -> 3 [id=4]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: User"] + 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label="3: Software System"] + + 2 -> 3 [id=4] + + }""", content); } @Test @@ -224,18 +232,20 @@ public void test_writeSystemContextView() throws Exception { Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + - "\n" + - " 2 -> 3 [id=4]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: User"] + 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label="3: Software System"] + + 2 -> 3 [id=4] + + }""", content); } @@ -257,26 +267,28 @@ public void test_writeSystemContextViewWithGroupedElements() throws Exception { Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " subgraph \"cluster_group_1\" {\n" + - " margin=25\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: User\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_2\" {\n" + - " margin=25\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Software System\"]\n" + - " }\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - "\n" + - " 2 -> 3 [id=4]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph "cluster_group_1" { + margin=25 + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: User"] + } + + subgraph "cluster_group_2" { + margin=25 + 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label="3: Software System"] + } + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + + 2 -> 3 [id=4] + + }""", content); } @Test @@ -301,32 +313,34 @@ public void test_writeContainerViewWithGroupedElementsInASingleSoftwareSystem() Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - "\n" + - " subgraph cluster_2 {\n" + - " margin=25\n" + - " subgraph \"cluster_group_1\" {\n" + - " margin=25\n" + - " 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label=\"3: Container 1\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_2\" {\n" + - " margin=25\n" + - " 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label=\"4: Container 2\"]\n" + - " }\n" + - "\n" + - " 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label=\"5: Container 3\"]\n" + - " }\n" + - "\n" + - " 3 -> 4 [id=6]\n" + - " 4 -> 5 [id=7]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + + subgraph cluster_2 { + margin=25 + subgraph "cluster_group_1" { + margin=25 + 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label="3: Container 1"] + } + + subgraph "cluster_group_2" { + margin=25 + 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label="4: Container 2"] + } + + 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label="5: Container 3"] + } + + 3 -> 4 [id=6] + 4 -> 5 [id=7] + + }""", content); } @Test @@ -351,32 +365,34 @@ public void test_writeContainerViewWithGroupedElementsInMultipleSoftwareSystems( Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " subgraph cluster_1 {\n" + - " margin=25\n" + - " subgraph \"cluster_group_1\" {\n" + - " margin=25\n" + - " 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label=\"2: Container 1\"]\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - " subgraph cluster_3 {\n" + - " margin=25\n" + - " subgraph \"cluster_group_2\" {\n" + - " margin=25\n" + - " 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label=\"4: Container 2\"]\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - " 2 -> 4 [id=5]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph cluster_1 { + margin=25 + subgraph "cluster_group_1" { + margin=25 + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: Container 1"] + } + + } + + subgraph cluster_3 { + margin=25 + subgraph "cluster_group_2" { + margin=25 + 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label="4: Container 2"] + } + + } + + 2 -> 4 [id=5] + + }""", content); } @Test @@ -402,32 +418,34 @@ public void test_writeComponentViewWithGroupedElements() throws Exception { Diagram diagram = exporter.export(view); String content = diagram.getDefinition(); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5]\n" + - " node [shape=box,fontsize=5]\n" + - " edge []\n" + - "\n" + - " 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label=\"1: Box\"]\n" + - "\n" + - " subgraph cluster_3 {\n" + - " margin=25\n" + - " subgraph \"cluster_group_1\" {\n" + - " margin=25\n" + - " 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label=\"5: Component 2\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_2\" {\n" + - " margin=25\n" + - " 6 [width=1.500000,height=1.000000,fixedsize=true,id=6,label=\"6: Component 3\"]\n" + - " }\n" + - "\n" + - " 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label=\"4: Component 1\"]\n" + - " }\n" + - "\n" + - " 4 -> 5 [id=7]\n" + - " 5 -> 6 [id=8]\n" + - "}", content); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + + subgraph cluster_3 { + margin=25 + subgraph "cluster_group_1" { + margin=25 + 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label="5: Component 2"] + } + + subgraph "cluster_group_2" { + margin=25 + 6 [width=1.500000,height=1.000000,fixedsize=true,id=6,label="6: Component 3"] + } + + 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label="4: Component 1"] + } + + 4 -> 5 [id=7] + 5 -> 6 [id=8] + + }""", content); } @Test @@ -486,9 +504,7 @@ public void test_AmazonWebServicesExample() throws Exception { DOTExporter exporter = new DOTExporter(RankDirection.LeftRight, 300, 300); Diagram diagram = exporter.export(workspace.getViews().getDeploymentViews().iterator().next()); - String content = diagram.getDefinition(); - - String expectedResult = """ + assertEquals(""" digraph { compound=true graph [splines=polyline,rankdir=LR,ranksep=1.0,nodesep=1.0,fontsize=5] @@ -526,9 +542,8 @@ public void test_AmazonWebServicesExample() throws Exception { 11 -> 14 [id=15] 7 -> 8 [id=16] 8 -> 11 [id=17] - }"""; - - assertEquals(expectedResult, content); + + }""", diagram.getDefinition()); } } \ No newline at end of file diff --git a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java index c499674a4..710553436 100644 --- a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java @@ -8,6 +8,7 @@ import com.structurizr.view.AutomaticLayout; import com.structurizr.view.Shape; import com.structurizr.view.SystemContextView; +import com.structurizr.view.SystemLandscapeView; import org.junit.jupiter.api.Test; import java.io.File; @@ -22,36 +23,36 @@ public void apply_Workspace() throws Exception { File tempDir = Files.createTempDirectory("graphviz").toFile(); GraphvizAutomaticLayout graphviz = new GraphvizAutomaticLayout(tempDir); - Workspace workspace = new Workspace("Name", ""); - Person user = workspace.getModel().addPerson("User"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); - user.uses(softwareSystem, "Uses"); + Workspace workspace = new Workspace("Name"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + a.uses(b, "Uses"); - SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); view.addAllElements(); workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.PERSON).shape(Shape.Person); - assertEquals(0, view.getElementView(user).getX()); - assertEquals(0, view.getElementView(user).getY()); - assertEquals(0, view.getElementView(softwareSystem).getX()); - assertEquals(0, view.getElementView(softwareSystem).getY()); + assertEquals(0, view.getElementView(a).getX()); + assertEquals(0, view.getElementView(a).getY()); + assertEquals(0, view.getElementView(b).getX()); + assertEquals(0, view.getElementView(b).getY()); graphviz.apply(workspace); // no change - the view doesn't have automatic layout configured - assertEquals(0, view.getElementView(user).getX()); - assertEquals(0, view.getElementView(user).getY()); - assertEquals(0, view.getElementView(softwareSystem).getX()); - assertEquals(0, view.getElementView(softwareSystem).getY()); + assertEquals(0, view.getElementView(a).getX()); + assertEquals(0, view.getElementView(a).getY()); + assertEquals(0, view.getElementView(b).getX()); + assertEquals(0, view.getElementView(b).getY()); view.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom); graphviz.apply(workspace); - assertEquals(233, view.getElementView(user).getX()); - assertEquals(208, view.getElementView(user).getY()); - assertEquals(208, view.getElementView(softwareSystem).getX()); - assertEquals(908, view.getElementView(softwareSystem).getY()); + assertEquals(208, view.getElementView(a).getX()); + assertEquals(208, view.getElementView(a).getY()); + assertEquals(208, view.getElementView(b).getX()); + assertEquals(808, view.getElementView(b).getY()); } } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java index 39ad047be..168519a8e 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java @@ -1,17 +1,43 @@ package com.structurizr.view; import com.structurizr.PropertyHolder; +import com.structurizr.util.StringUtils; import java.util.Collections; import java.util.HashMap; import java.util.Map; -public abstract class AbstractStyle implements PropertyHolder { +public abstract class AbstractStyle implements PropertyHolder, Comparable { + private String tag; private ColorScheme colorScheme = null; - private Map properties = new HashMap<>(); + AbstractStyle() { + } + + AbstractStyle(String tag) { + this.tag = tag; + } + + AbstractStyle(String tag, ColorScheme colorScheme) { + this.tag = tag; + this.colorScheme = colorScheme; + } + + /** + * The tag to which this style applies. + * + * @return the tag, as a String + */ + public String getTag() { + return tag; + } + + void setTag(String tag) { + this.tag = tag; + } + /** * Gets the color scheme of this style. * @@ -46,11 +72,11 @@ public Map getProperties() { * @param value the value of the property */ public void addProperty(String name, String value) { - if (name == null || name.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(name)) { throw new IllegalArgumentException("A property name must be specified."); } - if (value == null || value.trim().length() == 0) { + if (StringUtils.isNullOrEmpty(value)) { throw new IllegalArgumentException("A property value must be specified."); } @@ -63,4 +89,26 @@ void setProperties(Map properties) { } } + @Override + public String toString() { + return this.tag + " (" + this.colorScheme + ")"; + } + + @Override + public int compareTo(AbstractStyle other) { + if (this.colorScheme == null && other.colorScheme == null) { + return this.tag.compareTo(other.tag); + } + + if (this.colorScheme == null) { + return -1; + } + + if (other.colorScheme == null) { + return 1; + } + + return (this.colorScheme + "/" + this.tag).compareTo(other.colorScheme + "/" + other.tag); + } + } \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java index 90bd169c3..fe4799b60 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java @@ -12,8 +12,6 @@ public final class ElementStyle extends AbstractStyle { public static final int DEFAULT_WIDTH = 450; public static final int DEFAULT_HEIGHT = 300; - private String tag; - @JsonInclude(value = JsonInclude.Include.NON_NULL) private Integer width; @@ -59,13 +57,12 @@ public final class ElementStyle extends AbstractStyle { ElementStyle() { } - ElementStyle(String tag) { - this.tag = tag; + public ElementStyle(String tag) { + super(tag); } ElementStyle(String tag, ColorScheme colorScheme) { - this.tag = tag; - setColorScheme(colorScheme); + super(tag, colorScheme); } public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize) { @@ -73,7 +70,8 @@ public ElementStyle(String tag, Integer width, Integer height, String background } public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize, Shape shape) { - this.tag = tag; + super(tag); + this.width = width; this.height = height; setBackground(background); @@ -82,19 +80,6 @@ public ElementStyle(String tag, Integer width, Integer height, String background this.shape = shape; } - /** - * The tag to which this element style applies. - * - * @return the tag, as a String - */ - public String getTag() { - return tag; - } - - public void setTag(String tag) { - this.tag = tag; - } - /** * Gets the width of the element, in pixels. * @@ -443,6 +428,10 @@ void copyFrom(ElementStyle elementStyle) { this.setIcon(elementStyle.getIcon()); } + if (elementStyle.getIconPosition() != null) { + this.setIconPosition(elementStyle.getIconPosition()); + } + if (elementStyle.getBorder() != null) { this.setBorder(elementStyle.getBorder()); } diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java index be966314e..5f77360b4 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java @@ -8,9 +8,6 @@ public final class RelationshipStyle extends AbstractStyle { private static final int START_OF_LINE = 0; private static final int END_OF_LINE = 100; - /** the name of the tag to which this style applies */ - private String tag; - /** the thickness of the line, in pixels */ @JsonInclude(value = JsonInclude.Include.NON_NULL) private Integer thickness; @@ -54,21 +51,12 @@ public final class RelationshipStyle extends AbstractStyle { RelationshipStyle() { } - RelationshipStyle(String tag) { - this.tag = tag; + public RelationshipStyle(String tag) { + super(tag); } RelationshipStyle(String tag, ColorScheme colorScheme) { - this.tag = tag; - setColorScheme(colorScheme); - } - - public String getTag() { - return tag; - } - - public void setTag(String tag) { - this.tag = tag; + super(tag, colorScheme); } public Integer getThickness() { @@ -253,6 +241,10 @@ void copyFrom(RelationshipStyle relationshipStyle) { this.setRouting(relationshipStyle.getRouting()); } + if (relationshipStyle.getJump() != null) { + this.setJump(relationshipStyle.getJump()); + } + if (relationshipStyle.getFontSize() != null) { this.setFontSize(relationshipStyle.getFontSize()); } diff --git a/structurizr-core/src/main/java/com/structurizr/view/Styles.java b/structurizr-core/src/main/java/com/structurizr/view/Styles.java index 805ed188b..ba0c7e7ee 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Styles.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Styles.java @@ -8,16 +8,16 @@ public final class Styles { - private static final Integer DEFAULT_WIDTH_OF_ELEMENT = 450; - private static final Integer DEFAULT_HEIGHT_OF_ELEMENT = 300; + public static final String DEFAULT_BACKGROUND_LIGHT = "#ffffff"; + public static final String DEFAULT_COLOR_LIGHT = "#444444"; - private static final Integer DEFAULT_WIDTH_OF_PERSON = 400; - private static final Integer DEFAULT_HEIGHT_OF_PERSON = 400; + public static final String DEFAULT_BACKGROUND_DARK = "#111111"; + public static final String DEFAULT_COLOR_DARK = "#cccccc"; - private Collection elements = new LinkedList<>(); - private Collection relationships = new LinkedList<>(); + private Collection elements = new TreeSet<>(); + private Collection relationships = new TreeSet<>(); - private List themes = new ArrayList<>(); + private final List themes = new ArrayList<>(); public Collection getElements() { return elements; @@ -100,7 +100,7 @@ public RelationshipStyle addRelationshipStyle(String tag, ColorScheme colorSchem } /** - * Gets the element style that has been defined (in this workspace) for the given tag. + * Gets the element style that has been defined (in this workspace) for the given tag (without a color scheme). * * @param tag the tag (a String) * @return an ElementStyle instance, or null if no element style has been defined in this workspace @@ -125,14 +125,24 @@ public ElementStyle getElementStyle(String tag, ColorScheme colorScheme) { } /** - * Finds the element style for the given tag. This method creates an empty style, - * and copies properties from any element styles (from the workspace and any themes) for the given tag. - * + * Finds the element style for the given tag and light color scheme. * * @param tag the tag (a String) * @return an ElementStyle instance, or null if there is no style for the given tag */ public ElementStyle findElementStyle(String tag) { + return findElementStyle(tag, ColorScheme.Light); + } + + /** + * Finds the element style for the given tag and color scheme. This method creates an empty style, + * and copies properties from any element styles (from the workspace and any themes) for the given tag. + * + * @param tag the tag (a String) + * @param colorScheme the target color scheme + * @return an ElementStyle instance, or null if there is no style for the given tag + */ + public ElementStyle findElementStyle(String tag, ColorScheme colorScheme) { if (tag == null) { return null; } @@ -149,8 +159,10 @@ public ElementStyle findElementStyle(String tag) { for (ElementStyle elementStyle : elementStyles) { if (elementStyle != null && elementStyle.getTag().equals(tag)) { - elementStyleExists = true; - style.copyFrom(elementStyle); + if (elementStyle.getColorScheme() == null || elementStyle.getColorScheme() == colorScheme) { + elementStyleExists = true; + style.copyFrom(elementStyle); + } } } @@ -162,7 +174,7 @@ public ElementStyle findElementStyle(String tag) { } /** - * Gets the relationship style that has been defined (in this workspace) for the given tag. + * Gets the relationship style that has been defined (in this workspace) for the given tag (without a color scheme). * * @param tag the tag (a String) * @return an RelationshipStyle instance, or null if no relationship style has been defined in this workspace @@ -187,14 +199,25 @@ public RelationshipStyle getRelationshipStyle(String tag, ColorScheme colorSchem } /** - * Finds the relationship style for the given tag. This method creates an empty style, - * and copies properties from any relationship styles (from the workspace and any themes) for the given tag. + * Finds the relationship style for the given tag with a light color scheme. * * * @param tag the tag (a String) * @return a RelationshipStyle instance, or null if there is no style for the given tag */ public RelationshipStyle findRelationshipStyle(String tag) { + return findRelationshipStyle(tag, ColorScheme.Light); + } + + /** + * Finds the relationship style for the given tag and color scheme. This method creates an empty style, + * and copies properties from any relationship styles (from the workspace and any themes) for the given tag. + * + * @param tag the tag (a String) + * @param colorScheme the target color scheme + * @return a RelationshipStyle instance, or null if there is no style for the given tag + */ + public RelationshipStyle findRelationshipStyle(String tag, ColorScheme colorScheme) { if (tag == null) { return null; } @@ -211,8 +234,10 @@ public RelationshipStyle findRelationshipStyle(String tag) { for (RelationshipStyle relationshipStyle : relationshipStyles) { if (relationshipStyle != null && relationshipStyle.getTag().equals(tag)) { - style.copyFrom(relationshipStyle); - relationshipStyleExists = true; + if (relationshipStyle.getColorScheme() == null || relationshipStyle.getColorScheme() == colorScheme) { + style.copyFrom(relationshipStyle); + relationshipStyleExists = true; + } } } @@ -224,23 +249,28 @@ public RelationshipStyle findRelationshipStyle(String tag) { } /** - * Finds the element style used to render the specified element, according to the following rules: + * Finds the element style used to render the specified element with a light color scheme. + * + * @param element an Element object + * @return an ElementStyle object + */ + public ElementStyle findElementStyle(Element element) { + return findElementStyle(element, ColorScheme.Light); + } + + /** + * Finds the element style used to render the specified element and color scheme, according to the following rules: * * 1. Start with a default style. * 2. Calculate set of tags associated with the element. * 3. Find the style properties for each tag (themes first, followed by workspace styles) * * @param element an Element object + * @param colorScheme a ColorScheme (Light or Dark) * @return an ElementStyle object */ - public ElementStyle findElementStyle(Element element) { - ElementStyle style = new ElementStyle(Tags.ELEMENT).background("#dddddd").color("#000000").shape(Shape.Box).fontSize(24).border(Border.Solid).opacity(100).metadata(true).description(true); - - if (element instanceof DeploymentNode) { - style.setBackground("#ffffff"); - style.setColor("#000000"); - style.setStroke("#888888"); - } + public ElementStyle findElementStyle(Element element, ColorScheme colorScheme) { + ElementStyle style = new ElementStyle(Tags.ELEMENT).shape(Shape.Box).width(ElementStyle.DEFAULT_WIDTH).height(ElementStyle.DEFAULT_HEIGHT).fontSize(24).border(Border.Solid).opacity(100).metadata(true).description(true); if (element != null) { Set tagsUsedToComposeStyle = new LinkedHashSet<>(); @@ -257,7 +287,7 @@ public ElementStyle findElementStyle(Element element) { for (String tag : tags.split(",")) { if (!StringUtils.isNullOrEmpty(tag)) { - ElementStyle elementStyle = findElementStyle(tag); + ElementStyle elementStyle = findElementStyle(tag, colorScheme); if (elementStyle != null) { style.copyFrom(elementStyle); tagsUsedToComposeStyle.add(elementStyle.getTag()); @@ -268,42 +298,57 @@ public ElementStyle findElementStyle(Element element) { style.setTag(TagUtils.toString(tagsUsedToComposeStyle)); } - if (style.getWidth() == null) { - if (style.getShape() == Shape.Person) { - style.setWidth(DEFAULT_WIDTH_OF_PERSON); + if (style.getBackground() == null) { + if (colorScheme == ColorScheme.Dark) { + style.background(DEFAULT_BACKGROUND_DARK); + if (style.getStroke() == null) { + style.stroke(DEFAULT_COLOR_DARK); + } } else { - style.setWidth(DEFAULT_WIDTH_OF_ELEMENT); + style.background(DEFAULT_BACKGROUND_LIGHT); + if (style.getStroke() == null) { + style.stroke(DEFAULT_COLOR_LIGHT); + } + } + } else { + if (style.getStroke() == null) { + java.awt.Color color = java.awt.Color.decode(style.getBackground()); + style.setStroke(String.format("#%06X", (0xFFFFFF & color.darker().getRGB()))); } } - if (style.getHeight() == null) { - if (style.getShape() == Shape.Person || style.getShape() == Shape.Robot) { - style.setHeight(DEFAULT_HEIGHT_OF_PERSON); + if (style.getColor() == null) { + if (colorScheme == ColorScheme.Dark) { + style.color(DEFAULT_COLOR_DARK); } else { - style.setHeight(DEFAULT_HEIGHT_OF_ELEMENT); + style.color(DEFAULT_COLOR_LIGHT); } } - if (style.getStroke() == null) { - java.awt.Color color = java.awt.Color.decode(style.getBackground()); - style.setStroke(String.format("#%06X", (0xFFFFFF & color.darker().getRGB()))); - } return style; } /** - * Finds the relationship style used to render the specified relationship, according to the following rules: + * Finds the relationship style used to render the specified relationship with a light color scheme. + */ + public RelationshipStyle findRelationshipStyle(Relationship relationship) { + return findRelationshipStyle(relationship, ColorScheme.Light); + } + + /** + * Finds the relationship style used to render the specified relationship and color scheme, according to the following rules: * * 1. Start with a default style. * 2. Calculate set of tags associated with the relationship, and any linked relationship(s). * 3. Find the style properties for each tag (themes first, followed by workspace styles) * * @param relationship a Relationship object + * @param colorScheme a ColorScheme (Light or Dark) * @return a RelationshipStyle object */ - public RelationshipStyle findRelationshipStyle(Relationship relationship) { - RelationshipStyle style = new RelationshipStyle(Tags.RELATIONSHIP).thickness(2).color("#707070").dashed(true).routing(Routing.Direct).fontSize(24).width(200).position(50).opacity(100); + public RelationshipStyle findRelationshipStyle(Relationship relationship, ColorScheme colorScheme) { + RelationshipStyle style = new RelationshipStyle(Tags.RELATIONSHIP).thickness(2).style(LineStyle.Dashed).dashed(true).routing(Routing.Direct).fontSize(24).width(200).position(50).opacity(100); if (relationship != null) { Set tagsUsedToComposeStyle = new LinkedHashSet<>(); @@ -322,7 +367,7 @@ public RelationshipStyle findRelationshipStyle(Relationship relationship) { for (String tag : tags.split(",")) { if (!StringUtils.isNullOrEmpty(tag)) { - RelationshipStyle relationshipStyle = findRelationshipStyle(tag); + RelationshipStyle relationshipStyle = findRelationshipStyle(tag, colorScheme); if (relationshipStyle != null) { style.copyFrom(relationshipStyle); tagsUsedToComposeStyle.add(relationshipStyle.getTag()); @@ -333,6 +378,14 @@ public RelationshipStyle findRelationshipStyle(Relationship relationship) { style.setTag(TagUtils.toString(tagsUsedToComposeStyle)); } + if (style.getColor() == null) { + if (colorScheme == ColorScheme.Dark) { + style.color(DEFAULT_COLOR_DARK); + } else { + style.color(DEFAULT_COLOR_LIGHT); + } + } + return style; } diff --git a/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java index caf878673..f0c6baab6 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java @@ -4,6 +4,8 @@ import com.structurizr.model.*; import org.junit.jupiter.api.Test; +import java.util.LinkedList; +import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; @@ -12,18 +14,54 @@ public class StylesTests extends AbstractWorkspaceTestBase { private final Styles styles = new Styles(); + @Test + void test_sortingOfElementStyles() { + ElementStyle softwareLight = styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Light); + ElementStyle softwareDark = styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Dark); + ElementStyle software = styles.addElementStyle(Tags.SOFTWARE_SYSTEM); + ElementStyle elementDark = styles.addElementStyle(Tags.ELEMENT, ColorScheme.Dark); + ElementStyle elementLight = styles.addElementStyle(Tags.ELEMENT, ColorScheme.Light); + ElementStyle element = styles.addElementStyle(Tags.ELEMENT); + + List elementStyles = new LinkedList<>(styles.getElements()); + assertSame(element, elementStyles.get(0)); + assertSame(software, elementStyles.get(1)); + assertSame(elementDark, elementStyles.get(2)); + assertSame(softwareDark, elementStyles.get(3)); + assertSame(elementLight, elementStyles.get(4)); + assertSame(softwareLight, elementStyles.get(5)); + } + + @Test + void test_sortingOfRelationshipStyles() { + RelationshipStyle tag2Light = styles.addRelationshipStyle("Tag 2", ColorScheme.Light); + RelationshipStyle tag2Dark = styles.addRelationshipStyle("Tag 2", ColorScheme.Dark); + RelationshipStyle tag2 = styles.addRelationshipStyle("Tag 2"); + RelationshipStyle tag1Light = styles.addRelationshipStyle("Tag 1", ColorScheme.Light); + RelationshipStyle tag1Dark = styles.addRelationshipStyle("Tag 1", ColorScheme.Dark); + RelationshipStyle tag1 = styles.addRelationshipStyle("Tag 1"); + + List relationshipStyles = new LinkedList<>(styles.getRelationships()); + assertSame(tag1, relationshipStyles.get(0)); + assertSame(tag2, relationshipStyles.get(1)); + assertSame(tag1Dark, relationshipStyles.get(2)); + assertSame(tag2Dark, relationshipStyles.get(3)); + assertSame(tag1Light, relationshipStyles.get(4)); + assertSame(tag2Light, relationshipStyles.get(5)); + } + @Test void findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { ElementStyle style = styles.findElementStyle((Element) null); assertEquals(Integer.valueOf(450), style.getWidth()); assertEquals(Integer.valueOf(300), style.getHeight()); - assertEquals("#dddddd", style.getBackground()); - assertEquals("#000000", style.getColor()); + assertEquals("#ffffff", style.getBackground()); + assertEquals("#444444", style.getColor()); + assertEquals("#444444", style.getStroke()); assertEquals(Integer.valueOf(24), style.getFontSize()); assertEquals(Shape.Box, style.getShape()); assertNull(style.getIcon()); assertEquals(Border.Solid, style.getBorder()); - assertEquals("#9a9a9a", style.getStroke()); assertNull(style.getStrokeWidth()); assertEquals(Integer.valueOf(100), style.getOpacity()); assertEquals(true, style.getMetadata()); @@ -36,13 +74,32 @@ void findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { ElementStyle style = styles.findElementStyle(element); assertEquals(Integer.valueOf(450), style.getWidth()); assertEquals(Integer.valueOf(300), style.getHeight()); - assertEquals("#dddddd", style.getBackground()); - assertEquals("#000000", style.getColor()); + assertEquals("#ffffff", style.getBackground()); + assertEquals("#444444", style.getColor()); + assertEquals("#444444", style.getStroke()); + assertEquals(Integer.valueOf(24), style.getFontSize()); + assertEquals(Shape.Box, style.getShape()); + assertNull(style.getIcon()); + assertEquals(Border.Solid, style.getBorder()); + assertNull(style.getStrokeWidth()); + assertEquals(Integer.valueOf(100), style.getOpacity()); + assertEquals(true, style.getMetadata()); + assertEquals(true, style.getDescription()); + } + + @Test + void findElementStyleForDarkMode_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { + SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); + ElementStyle style = styles.findElementStyle(element, ColorScheme.Dark); + assertEquals(Integer.valueOf(450), style.getWidth()); + assertEquals(Integer.valueOf(300), style.getHeight()); + assertEquals("#111111", style.getBackground()); + assertEquals("#cccccc", style.getColor()); + assertEquals("#cccccc", style.getStroke()); assertEquals(Integer.valueOf(24), style.getFontSize()); assertEquals(Shape.Box, style.getShape()); assertNull(style.getIcon()); assertEquals(Border.Solid, style.getBorder()); - assertEquals("#9a9a9a", style.getStroke()); assertNull(style.getStrokeWidth()); assertEquals(Integer.valueOf(100), style.getOpacity()); assertEquals(true, style.getMetadata()); @@ -116,24 +173,23 @@ void findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABox() { } @Test - void findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsAPerson() { - SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); - element.addTags("Some Tag"); - - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#ff0000").color("#ffffff"); - styles.addElementStyle("Some Tag").shape(Shape.Person); - - ElementStyle style = styles.findElementStyle(element); - assertEquals(Shape.Person, style.getShape()); - assertEquals(Integer.valueOf(400), style.getWidth()); - assertEquals(Integer.valueOf(400), style.getHeight()); + void findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull_ForLightColorScheme() { + RelationshipStyle style = styles.findRelationshipStyle((Relationship) null); + assertEquals(Integer.valueOf(2), style.getThickness()); + assertEquals("#444444", style.getColor()); + assertTrue(style.getDashed()); + assertEquals(Routing.Direct, style.getRouting()); + assertEquals(Integer.valueOf(24), style.getFontSize()); + assertEquals(Integer.valueOf(200), style.getWidth()); + assertEquals(Integer.valueOf(50), style.getPosition()); + assertEquals(Integer.valueOf(100), style.getOpacity()); } @Test - void findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { - RelationshipStyle style = styles.findRelationshipStyle((Relationship) null); + void findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull_ForDarkColorScheme() { + RelationshipStyle style = styles.findRelationshipStyle((Relationship) null, ColorScheme.Dark); assertEquals(Integer.valueOf(2), style.getThickness()); - assertEquals("#707070", style.getColor()); + assertEquals("#cccccc", style.getColor()); assertTrue(style.getDashed()); assertEquals(Routing.Direct, style.getRouting()); assertEquals(Integer.valueOf(24), style.getFontSize()); @@ -148,7 +204,7 @@ void findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { Relationship relationship = element.uses(element, "Uses"); RelationshipStyle style = styles.findRelationshipStyle(relationship); assertEquals(Integer.valueOf(2), style.getThickness()); - assertEquals("#707070", style.getColor()); + assertEquals("#444444", style.getColor()); assertTrue(style.getDashed()); assertEquals(Routing.Direct, style.getRouting()); assertEquals(Integer.valueOf(24), style.getFontSize()); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 43fcb6e0f..022b748d2 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -386,7 +386,7 @@ void test_includeUrl() throws Exception { workspace { model { - !include https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/include/model.dsl + !include https://raw.githubusercontent.com/structurizr/java/refs/heads/master/structurizr-dsl/src/test/resources/dsl/include/model.dsl } }""", DslUtils.getDsl(workspace)); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index 52e59ccb3..4a2266c73 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -55,7 +55,8 @@ void test_parsePlantUML_WithViewKey() { parser = new ImageViewContentParser(true); parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape")); - assertEquals("https://plantuml.com/plantuml/svg/HP2nJiD038RtUmghF00f6oYD6f2OO2eI0p2Od9EUUh4Zdwkq8DwTkrB0bZpylsL_zZePgkt7w18P99fGqKI1XSbPi4YmEIQZ4HwGVUfm8kTC9Z21Tp6J4NnGwYm8EvTsWSk44JuT0AhAV2zic_11iAoovAd7VRGdEbWRmy0ZiK6N2sbsPyNfENZRmbLLkaSyF59AED1vGkM-dDi6Jv2HbCIE1UT_Qm517YBLTTiq9uXRx7Q3ofxzdSHys8K_HNOAsLchJb6wHJtfMRt6abbDM_Go1nwWnvYeGFnjWiLgrRvodJBXpR9gNZRIsupw-xUt-h9OpG9-c311wzoQsEUdVmC0", imageView.getContent()); + assertEquals("System Landscape", imageView.getTitle()); + assertEquals("https://plantuml.com/plantuml/svg/ZLBBJiCm4BpxArRb32rLuH2IgE4b3kL243q01pVU9bOJRsHlr0VYtt6Qj1n0Y9LihMTjpth6KyVISbELWZMN2A7JEmp6apZTEiOAPj8ebyaQms5RYT_CSSSjkipgcZMPlYY4GmQ7jRIIoO8XWuAf1YPO43DLeBJ5h3qY2gqGF8T5ucsDGeIEjwM_1C0ICNpu1E1QPglSKcFK3PLa0pXPxkDgNxqdmmTyieyM__HZE8Ix4Yiqx1TdVNhwDD-KY_bBev8e-XV1J1lyIT3XQTjk0ADlvBdGsSgWSm6C_sgmmzDMHnZto0DPlVEeB9DIvwPjDu3CpsYx3MaX5QsroO-KZtAZgwQQQyL509EBKVTuRqOdf6YbbYRtjWwYA3bOTtuPlwQqvBMq29tDxxs10mZ3txIAOv0E4Y6cQ9J_B5y0", imageView.getContent()); } @Test @@ -94,7 +95,7 @@ void test_parseMermaid_WithViewKey() { parser = new ImageViewContentParser(true); parser.parseMermaid(context, null, tokens("mermaid", "SystemLandscape")); - assertEquals("https://mermaid.ink/svg/pako:eJxlkMtuwjAQRX9lNAhlE9SwqupCpLLuLt0RFiYeJxZ-RLYppYh_bxJHVR93NrM4c3U0N8DGCUKGred9B2-72gJoZU9VvGoCQZKfdQSptGYLOaW2IxPOx3QiFB8WA_saq2uIZOCVWxEa3lONhxEd4FQ2kz_L8hC9O9HvboD10LYR6j1dbjPpbFxdSLVdZHB0WmTly-ZhAMp_VFCfxOCxWD6D4b5VdhVdz6DoP7JyXzkZL9wTJNVD6vjjuZ4NxZRvwyc-Tt447TxbFFOSL1mBOaAhb7gSyG4YOzLjV-f_4f3-BQMfekI=", imageView.getContent()); + assertEquals("https://mermaid.ink/svg/pako:eJxtkMtqwzAQRX9lmFK8cWgChYKaGNp1d-4uzkKxRraIHkaaNE1D_r12lEJfs5qBM4fLPQG2QREK7KIcenh9bjyANX5X89ESKNJybxm0sVbc6Ms0fmLSfptflJHj4mDdYH1MTA5epFeplQM1uJnQEc6yK_ldViaOYUc_3QCL0bZU5i1_rgodPM8OZLqeBWyDVUX1tLwbgeoPlcwHCXiY3z6Ck7EzfsZhEDAf3otqXQfNBxkJctRNdvzKufg_4f1lyjbYEL-unJe8whLQUXTSKBQn5J7c1Oq1PzyfPwHoMXnQ", imageView.getContent()); } @Test diff --git a/structurizr-dsl/src/test/resources/dsl/include-url.dsl b/structurizr-dsl/src/test/resources/dsl/include-url.dsl index 9739a90f7..e8e813e37 100644 --- a/structurizr-dsl/src/test/resources/dsl/include-url.dsl +++ b/structurizr-dsl/src/test/resources/dsl/include-url.dsl @@ -1,7 +1,7 @@ workspace { model { - !include https://raw.githubusercontent.com/structurizr/dsl/master/src/test/dsl/include/model.dsl + !include https://raw.githubusercontent.com/structurizr/java/refs/heads/master/structurizr-dsl/src/test/resources/dsl/include/model.dsl } } \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java index f3d1839a1..81894c514 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -13,8 +13,18 @@ public abstract class AbstractDiagramExporter extends AbstractExporter implement protected static final String GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator"; + protected final ColorScheme colorScheme; + private Object frame = null; + public AbstractDiagramExporter() { + this(ColorScheme.Light); + } + + public AbstractDiagramExporter(ColorScheme colorScheme) { + this.colorScheme = colorScheme; + } + /** * Exports all views in the workspace. * @@ -120,14 +130,13 @@ private Diagram export(CustomView view, Integer animationStep) { IndentingWriter writer = new IndentingWriter(); writeHeader(view, writer); - List elements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - elements.add((CustomElement)elementView.getElement()); - } - + List elements = getGroupableElements(view, null); writeElements(view, elements, writer); - writer.writeLine(); + if (!elements.isEmpty()) { + writer.writeLine(); + } + writeRelationships(view, writer); writeFooter(view, writer); @@ -154,13 +163,13 @@ private Diagram export(SystemLandscapeView view, Integer animationStep) { IndentingWriter writer = new IndentingWriter(); writeHeader(view, writer); - List elements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - elements.add((GroupableElement)elementView.getElement()); - } + List elements = getGroupableElements(view, null); writeElements(view, elements, writer); - writer.writeLine(); + if (!elements.isEmpty()) { + writer.writeLine(); + } + writeRelationships(view, writer); writeFooter(view, writer); @@ -187,13 +196,13 @@ private Diagram export(SystemContextView view, Integer animationStep) { IndentingWriter writer = new IndentingWriter(); writeHeader(view, writer); - List elements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - elements.add((GroupableElement)elementView.getElement()); - } + List elements = getGroupableElements(view, null); writeElements(view, elements, writer); - writer.writeLine(); + if (!elements.isEmpty()) { + writer.writeLine(); + } + writeRelationships(view, writer); writeFooter(view, writer); @@ -235,13 +244,7 @@ public Diagram export(ContainerView view, Integer animationStep) { for (SoftwareSystem softwareSystem : softwareSystems) { startSoftwareSystemBoundary(view, softwareSystem, writer); - List scopedElements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - if (elementView.getElement().getParent() == softwareSystem) { - scopedElements.add((StaticStructureElement) elementView.getElement()); - } - } - + List scopedElements = getGroupableElements(view, softwareSystem); writeElements(view, scopedElements, writer); endSoftwareSystemBoundary(view, writer); @@ -307,12 +310,7 @@ public Diagram export(ComponentView view, Integer animationStep) { if (container.getSoftwareSystem() == softwareSystem) { startContainerBoundary(view, container, writer); - List scopedElements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - if (elementView.getElement().getParent() == container) { - scopedElements.add((StaticStructureElement) elementView.getElement()); - } - } + List scopedElements = getGroupableElements(view, container); writeElements(view, scopedElements, writer); endContainerBoundary(view, writer); @@ -369,11 +367,12 @@ public Diagram export(DynamicView view, String order) { if (element == null) { // dynamic view with no scope - List elements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - elements.add((StaticStructureElement) elementView.getElement()); - } + List elements = getGroupableElements(view, null); writeElements(view, elements, writer); + + if (!elements.isEmpty()) { + elementsWritten = true; + } } else { if (element instanceof SoftwareSystem) { // dynamic view with software system scope @@ -381,13 +380,7 @@ public Diagram export(DynamicView view, String order) { for (SoftwareSystem softwareSystem : softwareSystems) { startSoftwareSystemBoundary(view, softwareSystem, writer); - List scopedElements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - if (elementView.getElement().getParent() == softwareSystem) { - scopedElements.add((StaticStructureElement) elementView.getElement()); - } - } - + List scopedElements = getGroupableElements(view, softwareSystem); writeElements(view, scopedElements, writer); endSoftwareSystemBoundary(view, writer); @@ -416,12 +409,7 @@ public Diagram export(DynamicView view, String order) { if (container.getSoftwareSystem() == softwareSystem) { startContainerBoundary(view, container, writer); - List scopedElements = new ArrayList<>(); - for (ElementView elementView : view.getElements()) { - if (elementView.getElement().getParent() == container) { - scopedElements.add((StaticStructureElement) elementView.getElement()); - } - } + List scopedElements = getGroupableElements(view, container); writeElements(view, scopedElements, writer); endContainerBoundary(view, writer); @@ -472,14 +460,7 @@ public Diagram export(DeploymentView view, Integer animationStep) { IndentingWriter writer = new IndentingWriter(); writeHeader(view, writer); - List elements = new ArrayList<>(); - - for (ElementView elementView : view.getElements()) { - if (elementView.getElement() instanceof DeploymentNode && elementView.getElement().getParent() == null) { - elements.add((DeploymentNode)elementView.getElement()); - } - } - + List elements = getGroupableElements(view, null); writeElements(view, elements, writer); writeRelationships(view, writer); @@ -488,7 +469,7 @@ public Diagram export(DeploymentView view, Integer animationStep) { return createDiagram(view, writer.toString()); } - protected void writeElements(ModelView view, List elements, IndentingWriter writer) { + protected List findGroups(ModelView view, List elements) { String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); boolean nested = !StringUtils.isNullOrEmpty(groupSeparator); @@ -513,8 +494,16 @@ protected void writeElements(ModelView view, List elements, In List groupsAsList = new ArrayList<>(groupsAsSet); Collections.sort(groupsAsList); + return groupsAsList; + } + + protected void writeElements(ModelView view, List elements, IndentingWriter writer) { + String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); + boolean nested = !StringUtils.isNullOrEmpty(groupSeparator); + List groupsAsList = findGroups(view, elements); + // first render grouped elements - if (groupsAsList.size() > 0) { + if (!groupsAsList.isEmpty()) { if (nested) { String context = ""; @@ -585,18 +574,24 @@ protected void writeElements(ModelView view, List elements, In } } - protected void writeRelationships(ModelView view, IndentingWriter writer) { - Collection relationshipList; - + protected Collection getRelationshipsInView(ModelView view) { if (view instanceof DynamicView) { - relationshipList = view.getRelationships(); + return view.getRelationships(); } else { - relationshipList = view.getRelationships().stream().sorted(Comparator.comparing(rv -> rv.getRelationship().getId())).collect(Collectors.toList()); + return view.getRelationships().stream().sorted(Comparator.comparing(rv -> rv.getRelationship().getId())).collect(Collectors.toList()); } + } + + protected void writeRelationships(ModelView view, IndentingWriter writer) { + Collection relationshipList = getRelationshipsInView(view); for (RelationshipView relationshipView : relationshipList) { writeRelationship(view, relationshipView, writer); } + + if (!relationshipList.isEmpty()) { + writer.writeLine(); + } } protected abstract void writeHeader(ModelView view, IndentingWriter writer); @@ -732,4 +727,80 @@ protected String getViewOrViewSetProperty(ModelView view, String name, String de ); } + @Override + protected ElementStyle findElementStyle(ModelView view, Element element) { + return view.getViewSet().getConfiguration().getStyles().findElementStyle(element, colorScheme); + } + + protected ElementStyle findElementStyle(ModelView view, String tag) { + return view.getViewSet().getConfiguration().getStyles().findElementStyle(tag, colorScheme); + } + + @Override + protected RelationshipStyle findRelationshipStyle(ModelView view, Relationship relationship) { + return view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship, colorScheme); + } + + protected List getGroupableElements(ModelView view, Element parent) { + List elements = new ArrayList<>(); + + if (view instanceof CustomView) { + for (ElementView elementView : view.getElements()) { + elements.add((CustomElement)elementView.getElement()); + } + } else if (view instanceof SystemLandscapeView) { + for (ElementView elementView : view.getElements()) { + elements.add((GroupableElement)elementView.getElement()); + } + } else if (view instanceof SystemContextView) { + for (ElementView elementView : view.getElements()) { + elements.add((GroupableElement)elementView.getElement()); + } + } else if (view instanceof ContainerView) { + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof Container) { + elements.add((StaticStructureElement) elementView.getElement()); + } + } + } else if (view instanceof ComponentView) { + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof Component) { + elements.add((StaticStructureElement) elementView.getElement()); + } + } + } else if (view instanceof DynamicView) { + DynamicView dynamicView = (DynamicView)view; + Element element = dynamicView.getElement(); + if (element == null) { + for (ElementView elementView : view.getElements()) { + elements.add((StaticStructureElement) elementView.getElement()); + } + } else if (element instanceof SoftwareSystem) { + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof Container) { + elements.add((StaticStructureElement) elementView.getElement()); + } + } + } else if (element instanceof Container) { + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof Component) { + elements.add((StaticStructureElement) elementView.getElement()); + } + } + } + } else if (view instanceof DeploymentView) { + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof DeploymentNode && elementView.getElement().getParent() == null) { + elements.add((DeploymentNode)elementView.getElement()); + } + } + } + + if (parent != null) { + return elements.stream().filter(e -> e.getParent() == parent).collect(Collectors.toList()); + } else { + return elements; + } + } + } \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java b/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java index f7bf0a0bc..214fb00e2 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java @@ -6,7 +6,7 @@ public final class IndentingWriter { private IndentType indentType = IndentType.Spaces; private int indentQuantity = 2; - private StringBuilder buf = new StringBuilder(); + private final StringBuilder buf = new StringBuilder(); public IndentingWriter() { } @@ -49,6 +49,13 @@ public void writeLine(String content) { buf.append(String.format("%s%s\n", padding(), content.replace("\n", "\\n"))); } + public void replace(String before, String after) { + int start = buf.indexOf(before); + int end = start + before.length(); + + buf.replace(start, end, after); + } + @Override public String toString() { String s = buf.toString(); diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java index 7424cbd2e..52a974b27 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java @@ -5,6 +5,7 @@ import com.structurizr.export.IndentingWriter; import com.structurizr.model.*; import com.structurizr.util.StringUtils; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ElementStyle; import com.structurizr.view.ModelView; import com.structurizr.view.Shape; @@ -13,32 +14,25 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.net.URL; -import java.util.LinkedHashMap; -import java.util.Map; - -import static java.lang.String.format; public abstract class AbstractPlantUMLExporter extends AbstractDiagramExporter { + protected static final int DEFAULT_FONT_SIZE = 24; + public static final String PLANTUML_TITLE_PROPERTY = "plantuml.title"; public static final String PLANTUML_INCLUDES_PROPERTY = "plantuml.includes"; public static final String PLANTUML_ANIMATION_PROPERTY = "plantuml.animation"; public static final String PLANTUML_SEQUENCE_DIAGRAM_PROPERTY = "plantuml.sequenceDiagram"; - private static final double MAX_ICON_SIZE = 30.0; - - private final Map skinParams = new LinkedHashMap<>(); - - protected Map getSkinParams() { - return skinParams; - } + public static final String DIAGRAM_TITLE_TAG = "Diagram:Title"; + public static final String DIAGRAM_DESCRIPTION_TAG = "Diagram:Description"; - public void addSkinParam(String name, String value) { - skinParams.put(name, value); + public AbstractPlantUMLExporter() { + this(ColorScheme.Light); } - public void clearSkinParams() { - skinParams.clear(); + public AbstractPlantUMLExporter(ColorScheme colorScheme) { + super(colorScheme); } String plantUMLShapeOf(ModelView view, Element element) { @@ -178,38 +172,54 @@ protected boolean isAnimationSupported(ModelView view) { @Override protected void writeHeader(ModelView view, IndentingWriter writer) { writer.writeLine("@startuml"); - writer.writeLine("set separator none"); if (includeTitle(view)) { - String viewTitle = view.getTitle(); - if (StringUtils.isNullOrEmpty(viewTitle)) { - viewTitle = view.getName(); - } - writer.writeLine("title " + viewTitle); - } + ElementStyle titleStyle = findElementStyle(view, DIAGRAM_TITLE_TAG); + ElementStyle descriptionStyle = findElementStyle(view, DIAGRAM_DESCRIPTION_TAG); - writer.writeLine(); - } + String title = view.getTitle(); + if (StringUtils.isNullOrEmpty(title)) { + title = view.getName(); + } - protected void writeSkinParams(IndentingWriter writer) { - if (!skinParams.isEmpty()) { - writer.writeLine("skinparam {"); - writer.indent(); - for (final String name : skinParams.keySet()) { - writer.writeLine(format("%s %s", name, skinParams.get(name))); + String description = view.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + writer.writeLine( + String.format( + "title %s", + titleStyle != null ? titleStyle.getFontSize() : DEFAULT_FONT_SIZE, + title + ) + ); + } else { + writer.writeLine( + String.format( + "title %s\\n%s", + titleStyle != null ? titleStyle.getFontSize() : DEFAULT_FONT_SIZE, + title, + descriptionStyle != null ? descriptionStyle.getFontSize() : DEFAULT_FONT_SIZE, + description + ) + ); } - writer.outdent(); - writer.writeLine("}"); } + + writer.writeLine(); + writer.writeLine("set separator none"); } protected void writeIncludes(ModelView view, IndentingWriter writer) { - String[] includes = getViewOrViewSetProperty(view, PLANTUML_INCLUDES_PROPERTY, "").split(","); - for (String include : includes) { - if (!StringUtils.isNullOrEmpty(include)) { - include = include.trim(); - writer.writeLine("!include " + include); + String commaSeparatedIncludes = getViewOrViewSetProperty(view, PLANTUML_INCLUDES_PROPERTY, ""); + if (!StringUtils.isNullOrEmpty(commaSeparatedIncludes)) { + String[] includes = commaSeparatedIncludes.split(","); + + for (String include : includes) { + if (!StringUtils.isNullOrEmpty(include)) { + include = include.trim(); + writer.writeLine("!include " + include); + } } + writer.writeLine(); } } @@ -223,11 +233,11 @@ protected Diagram createDiagram(ModelView view, String definition) { return new PlantUMLDiagram(view, definition); } - protected boolean elementStyleHasSupportedIcon(ElementStyle elementStyle) { - return !StringUtils.isNullOrEmpty(elementStyle.getIcon()) && elementStyle.getIcon().startsWith("http"); + protected boolean isSupportedIcon(String icon) { + return !StringUtils.isNullOrEmpty(icon) && icon.startsWith("http"); } - protected double calculateIconScale(String iconUrl) { + protected double calculateIconScale(String iconUrl, int maxIconSize) { double scale = 0.5; try { @@ -237,7 +247,7 @@ protected double calculateIconScale(String iconUrl) { int width = bi.getWidth(); int height = bi.getHeight(); - scale = MAX_ICON_SIZE / Math.max(width, height); + scale = ((double)maxIconSize) / Math.max(width, height); } catch (UnsupportedOperationException | UnsatisfiedLinkError | IIOException e) { // This is a known issue on native builds since AWT packages aren't available. // So we just swallow the error and use the default scale diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index cdd26bafc..ef3c2a718 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -7,7 +7,6 @@ import com.structurizr.view.*; import java.util.*; -import java.util.stream.Collectors; import static java.lang.String.format; @@ -22,6 +21,8 @@ public class C4PlantUMLExporter extends AbstractPlantUMLExporter { public static final String C4PLANTUML_SPRITE = "c4plantuml.sprite"; public static final String C4PLANTUML_SHADOW = "c4plantuml.shadow"; + private static final int MAX_ICON_SIZE = 30; + /** *

Set this property to true by calling {@link Configuration#addProperty(String, String)} in your * {@link ViewSet} in order to have all {@link ModelItem#getProperties()} for {@link Component}s @@ -51,28 +52,16 @@ public class C4PlantUMLExporter extends AbstractPlantUMLExporter { public C4PlantUMLExporter() { } + public C4PlantUMLExporter(ColorScheme colorScheme) { + super(colorScheme); + } + @Override protected void writeHeader(ModelView view, IndentingWriter writer) { super.writeHeader(view, writer); groupId = 0; - Font font = view.getViewSet().getConfiguration().getBranding().getFont(); - if (font != null) { - String fontName = font.getName(); - if (!StringUtils.isNullOrEmpty(fontName)) { - addSkinParam("defaultFontName", "\"" + fontName + "\""); - } - } - - writeSkinParams(writer); - - if (renderAsSequenceDiagram(view)) { - if (usePlantUMLStandardLibrary(view)) { - writer.writeLine("!include "); - } else { - writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml"); - } - } else { + if (!renderAsSequenceDiagram(view)) { if (view.getAutomaticLayout() != null) { switch (view.getAutomaticLayout().getRankDirection()) { case LeftRight: @@ -85,9 +74,41 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { } else { writer.writeLine("top to bottom direction"); } + } - writer.writeLine(); + writer.writeLine(); + writer.writeLine(""); + writer.writeLine(); + + if (renderAsSequenceDiagram(view)) { + if (usePlantUMLStandardLibrary(view)) { + writer.writeLine("!include "); + } else { + writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml"); + } + } else { if (usePlantUMLStandardLibrary(view)) { writer.writeLine("!include "); writer.writeLine("!include "); @@ -120,6 +141,7 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { } } } + writer.writeLine(); writeIncludes(view, writer); @@ -169,15 +191,13 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { } if (!elementStyles.isEmpty()) { - writer.writeLine(); - for (String tagList : elementStyles.keySet()) { ElementStyle elementStyle = elementStyles.get(tagList); tagList = tagList.replaceFirst("Element,", ""); String sprite = ""; - if (elementStyleHasSupportedIcon(elementStyle)) { - double scale = calculateIconScale(elementStyle.getIcon()); + if (isSupportedIcon(elementStyle.getIcon())) { + double scale = calculateIconScale(elementStyle.getIcon(), MAX_ICON_SIZE); sprite = "img:" + elementStyle.getIcon() + "{scale=" + scale + "}"; } sprite = elementStyle.getProperties().getOrDefault(C4PLANTUML_SPRITE, sprite); @@ -201,11 +221,11 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { line = line.replace(", $borderThickness=\"1\")", ")"); writer.writeLine(line); } - } - if (!relationshipStyles.isEmpty()) { writer.writeLine(); + } + if (!relationshipStyles.isEmpty()) { for (String tagList : relationshipStyles.keySet()) { RelationshipStyle relationshipStyle = relationshipStyles.get(tagList); tagList = tagList.replaceFirst("Relationship,", ""); @@ -224,6 +244,8 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { lineStyle )); } + + writer.writeLine(); } if (!boundaryStyles.isEmpty()) { @@ -253,19 +275,12 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { } } } - - writer.writeLine(); } @Override protected void writeFooter(ModelView view, IndentingWriter writer) { - if (includeLegend(view)) { - writer.writeLine(); - writer.writeLine("SHOW_LEGEND(" + !(includeStereotypes(view)) + ")"); - } else { - writer.writeLine(); - writer.writeLine((includeStereotypes(view) ? "show" : "hide") + " stereotypes"); - } + writer.writeLine("SHOW_LEGEND(" + includeLegend(view) + ")"); + writer.writeLine((includeStereotypes(view) ? "show" : "hide") + " stereotypes"); super.writeFooter(view, writer); } diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyle.java new file mode 100644 index 000000000..ad6cd4f6a --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyle.java @@ -0,0 +1,81 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.IndentingWriter; +import com.structurizr.view.Border; + +import java.util.Base64; + +import static java.lang.String.format; + +class PlantUMLBoundaryStyle extends PlantUMLStyle { + + private final String background; + private final String color; + private final String stroke; + private final int strokeWidth; + private final String lineStyle; + private final int fontSize; + private final boolean shadow; + + PlantUMLBoundaryStyle(String name, String background, String color, String stroke, int strokeWidth, Border border, int fontSize, boolean shadow) { + super(name); + + this.background = background; + this.color = color; + this.stroke = stroke; + this.strokeWidth = strokeWidth; + + switch (border) { + case Dotted: + this.lineStyle = (strokeWidth) + "-" + (strokeWidth); + break; + case Dashed: + this.lineStyle = (strokeWidth * 5) + "-" + (strokeWidth * 5); + break; + default: + this.lineStyle = "0"; + break; + } + + this.fontSize = fontSize; + this.shadow = shadow; + } + + @Override + String getClassSelector() { + return "Boundary-" + Base64.getEncoder().encodeToString(name.getBytes()); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PlantUMLBoundaryStyle that = (PlantUMLBoundaryStyle) o; + return getClassSelector().equals(that.getClassSelector()); + } + + @Override + public String toString() { + IndentingWriter writer = new IndentingWriter(); + writer.indent(); + writer.writeLine(format("// %s", name)); + writer.writeLine(format(".%s {", getClassSelector())); + writer.indent(); + + writer.writeLine(String.format("BackgroundColor: %s;", background)); + writer.writeLine(String.format("LineColor: %s;", stroke)); + writer.writeLine(String.format("LineStyle: %s;", lineStyle)); + writer.writeLine(String.format("LineThickness: %s;", strokeWidth)); + writer.writeLine(String.format("FontColor: %s;", color)); + writer.writeLine(String.format("FontSize: %s;", fontSize)); + writer.writeLine("HorizontalAlignment: center;"); + writer.writeLine(String.format("Shadowing: %s;", shadow ? SHADOW_DISTANCE : 0)); + + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine(); + + return writer.toString(); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLElementStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLElementStyle.java new file mode 100644 index 000000000..06da055f0 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLElementStyle.java @@ -0,0 +1,107 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.IndentingWriter; +import com.structurizr.view.Border; +import com.structurizr.view.Shape; + +import java.util.Base64; + +import static java.lang.String.format; + +class PlantUMLElementStyle extends PlantUMLStyle { + + private final Shape shape; + private int width; + private final String background; + private final String color; + private final String stroke; + private final int strokeWidth; + private final String lineStyle; + private final int fontSize; + private final String icon; + private final boolean shadow; + + PlantUMLElementStyle(String name, Shape shape, int width, String background, String color, String stroke, int strokeWidth, Border border, int fontSize, String icon, boolean shadow) { + super(name); + + this.shape = shape; + this.width = width; + this.background = background; + this.color = color; + this.stroke = stroke; + this.strokeWidth = strokeWidth; + + switch (border) { + case Dotted: + this.lineStyle = (strokeWidth) + "-" + (strokeWidth); + break; + case Dashed: + this.lineStyle = (strokeWidth * 5) + "-" + (strokeWidth * 5); + break; + default: + this.lineStyle = "0"; + break; + } + + this.fontSize = fontSize; + this.icon = icon; + this.shadow = shadow; + } + + Shape getShape() { + return shape; + } + + int getFontSize() { + return fontSize; + } + + String getIcon() { + return icon; + } + + void setWidth(int width) { + this.width = width; + } + + @Override + String getClassSelector() { + return "Element-" + Base64.getEncoder().encodeToString(name.getBytes()); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PlantUMLElementStyle that = (PlantUMLElementStyle) o; + return getClassSelector().equals(that.getClassSelector()); + } + + @Override + public String toString() { + IndentingWriter writer = new IndentingWriter(); + writer.indent(); + writer.writeLine(format("// %s", name)); + writer.writeLine(format(".%s {", getClassSelector())); + writer.indent(); + + writer.writeLine(String.format("BackgroundColor: %s;", background)); + writer.writeLine(String.format("LineColor: %s;", stroke)); + writer.writeLine(String.format("LineStyle: %s;", lineStyle)); + writer.writeLine(String.format("LineThickness: %s;", strokeWidth)); + if (shape == Shape.RoundedBox) { + writer.writeLine("RoundCorner: 20;"); + } + writer.writeLine(String.format("FontColor: %s;", color)); + writer.writeLine(String.format("FontSize: %s;", fontSize)); + writer.writeLine("HorizontalAlignment: center;"); + writer.writeLine(String.format("Shadowing: %s;", shadow ? SHADOW_DISTANCE : 0)); + writer.writeLine(String.format("MaximumWidth: %s;", width)); + + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine(); + + return writer.toString(); + } +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLGroupStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLGroupStyle.java new file mode 100644 index 000000000..19507abff --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLGroupStyle.java @@ -0,0 +1,81 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.IndentingWriter; +import com.structurizr.view.Border; + +import java.util.Base64; + +import static java.lang.String.format; + +class PlantUMLGroupStyle extends PlantUMLStyle { + + private final String background; + private final String color; + private final String stroke; + private final int strokeWidth; + private final String lineStyle; + private final int fontSize; + private final boolean shadow; + + PlantUMLGroupStyle(String name, String background, String color, String stroke, int strokeWidth, Border border, int fontSize, boolean shadow) { + super(name); + + this.background = background; + this.color = color; + this.stroke = stroke; + this.strokeWidth = strokeWidth; + + switch (border) { + case Dotted: + this.lineStyle = (strokeWidth) + "-" + (strokeWidth); + break; + case Dashed: + this.lineStyle = (strokeWidth * 5) + "-" + (strokeWidth * 5); + break; + default: + this.lineStyle = "0"; + break; + } + + this.fontSize = fontSize; + this.shadow = shadow; + } + + @Override + String getClassSelector() { + return "Group-" + Base64.getEncoder().encodeToString(name.getBytes()); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PlantUMLGroupStyle that = (PlantUMLGroupStyle) o; + return getClassSelector().equals(that.getClassSelector()); + } + + @Override + public String toString() { + IndentingWriter writer = new IndentingWriter(); + writer.indent(); + writer.writeLine(format("// %s", name)); + writer.writeLine(format(".%s {", getClassSelector())); + writer.indent(); + + writer.writeLine(String.format("BackgroundColor: %s;", background)); + writer.writeLine(String.format("LineColor: %s;", stroke)); + writer.writeLine(String.format("LineStyle: %s;", lineStyle)); + writer.writeLine(String.format("LineThickness: %s;", strokeWidth)); + writer.writeLine(String.format("FontColor: %s;", color)); + writer.writeLine(String.format("FontSize: %s;", fontSize)); + writer.writeLine("HorizontalAlignment: center;"); + writer.writeLine(String.format("Shadowing: %s;", shadow ? SHADOW_DISTANCE : 0)); + + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine(); + + return writer.toString(); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLLegendStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLLegendStyle.java new file mode 100644 index 000000000..8274cb74e --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLLegendStyle.java @@ -0,0 +1,44 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.IndentingWriter; + +import static java.lang.String.format; + +class PlantUMLLegendStyle extends PlantUMLStyle { + + PlantUMLLegendStyle() { + super("Element-Transparent"); + } + + @Override + String getClassSelector() { + return "Element-Transparent"; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PlantUMLLegendStyle that = (PlantUMLLegendStyle) o; + return getClassSelector().equals(that.getClassSelector()); + } + + @Override + public String toString() { + IndentingWriter writer = new IndentingWriter(); + writer.indent(); + writer.writeLine("// transparent element for relationships in legend"); + writer.writeLine(format(".%s {", getClassSelector())); + writer.indent(); + + writer.writeLine("BackgroundColor: transparent;"); + writer.writeLine("LineColor: transparent;"); + writer.writeLine("FontColor: transparent;"); + + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine(); + + return writer.toString(); + } +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyle.java new file mode 100644 index 000000000..06379cd53 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyle.java @@ -0,0 +1,71 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.IndentingWriter; +import com.structurizr.view.LineStyle; + +import java.util.Base64; + +import static java.lang.String.format; + +class PlantUMLRelationshipStyle extends PlantUMLStyle { + + private final String color; + private final String lineStyle; + private final int thickness; + private final int fontSize; + + PlantUMLRelationshipStyle(String name, String color, LineStyle lineStyle, int thickness, int fontSize) { + super(name); + + this.color = color; + this.thickness = thickness; + this.fontSize = fontSize; + + switch (lineStyle) { + case Dotted: + this.lineStyle = (thickness) + "-" + (thickness); + break; + case Dashed: + this.lineStyle = (thickness * 5) + "-" + (thickness * 5); + break; + default: + this.lineStyle = "0"; + break; + } + } + + @Override + String getClassSelector() { + return "Relationship-" + Base64.getEncoder().encodeToString(name.getBytes()); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PlantUMLRelationshipStyle that = (PlantUMLRelationshipStyle) o; + return getClassSelector().equals(that.getClassSelector()); + } + + @Override + public String toString() { + IndentingWriter writer = new IndentingWriter(); + writer.indent(); + writer.writeLine(format("// %s", name)); + writer.writeLine(format(".%s {", getClassSelector())); + writer.indent(); + + writer.writeLine(String.format("LineThickness: %s;", thickness)); + writer.writeLine(String.format("LineStyle: %s;", lineStyle)); + writer.writeLine(String.format("LineColor: %s;", color)); + writer.writeLine(String.format("FontColor: %s;", color)); + writer.writeLine(String.format("FontSize: %s;", fontSize)); + + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine(); + + return writer.toString(); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRootStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRootStyle.java new file mode 100644 index 000000000..769b21584 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLRootStyle.java @@ -0,0 +1,52 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.IndentingWriter; +import com.structurizr.util.StringUtils; + +import static java.lang.String.format; + +class PlantUMLRootStyle extends PlantUMLStyle { + + private final String background; + private final String color; + private final String fontName; + + PlantUMLRootStyle(String background, String color, String fontName) { + super(".root"); + + this.background = background; + this.color = color; + this.fontName = fontName; + } + + @Override + String getClassSelector() { + return "root"; + } + + @Override + public boolean equals(Object o) { + return o != null && getClass() == o.getClass(); + } + + @Override + public String toString() { + IndentingWriter writer = new IndentingWriter(); + writer.indent(); + writer.writeLine(format("%s {", getClassSelector())); + writer.indent(); + + writer.writeLine(String.format("BackgroundColor: %s;", background)); + writer.writeLine(String.format("FontColor: %s;", color)); + if (!StringUtils.isNullOrEmpty(fontName)) { + writer.writeLine(String.format("FontName: %s;", fontName)); + } + + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine(); + + return writer.toString(); + } +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLStyle.java new file mode 100644 index 000000000..6eaccf324 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLStyle.java @@ -0,0 +1,26 @@ +package com.structurizr.export.plantuml; + +import java.util.Objects; + +abstract class PlantUMLStyle { + + protected static final int SHADOW_DISTANCE = 10; + + protected final String name; + + PlantUMLStyle(String name) { + this.name = name; + } + + String getName() { + return name; + } + + abstract String getClassSelector(); + + @Override + public final int hashCode() { + return Objects.hashCode(getClassSelector()); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 230099a1c..6dccc103c 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -8,7 +8,6 @@ import com.structurizr.view.*; import java.util.*; -import java.util.stream.Collectors; import static java.lang.String.format; @@ -16,19 +15,24 @@ public class StructurizrPlantUMLExporter extends AbstractPlantUMLExporter { public static final String PLANTUML_SHADOW = "plantuml.shadow"; - private int groupId = 0; + private static final int DEFAULT_STROKE_WIDTH = 2; + private static final double METADATA_FONT_SIZE_RATIO = 0.7; + private static final int MAX_ICON_SIZE_RATIO = 3; + + private Set plantUMLStyles; public StructurizrPlantUMLExporter() { - addSkinParam("arrowFontSize", "10"); - addSkinParam("defaultTextAlignment", "center"); - addSkinParam("wrapWidth", "200"); - addSkinParam("maxMessageSize", "100"); + this(ColorScheme.Light); + } + + public StructurizrPlantUMLExporter(ColorScheme colorScheme) { + super(colorScheme); } @Override protected void writeHeader(ModelView view, IndentingWriter writer) { + plantUMLStyles = new HashSet<>(); super.writeHeader(view, writer); - groupId = 0; if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { // do nothing @@ -51,104 +55,59 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { } else { writer.writeLine("top to bottom direction"); } - - writer.writeLine(); } + writer.writeLine("hide stereotype"); + writer.writeLine(); + + writer.writeLine(""); + writer.writeLine(); + + String fontName = null; Font font = view.getViewSet().getConfiguration().getBranding().getFont(); if (font != null) { - String fontName = font.getName(); + fontName = font.getName(); if (!StringUtils.isNullOrEmpty(fontName)) { - addSkinParam("defaultFontName", "\"" + fontName + "\""); + writer.writeLine("FontName: " + fontName); } } + if (colorScheme == ColorScheme.Dark) { + plantUMLStyles.add(new PlantUMLRootStyle( + Styles.DEFAULT_BACKGROUND_DARK, + Styles.DEFAULT_COLOR_DARK, + fontName)); + } else { + plantUMLStyles.add(new PlantUMLRootStyle( + Styles.DEFAULT_BACKGROUND_LIGHT, + Styles.DEFAULT_COLOR_LIGHT, + fontName)); + } - writeSkinParams(writer); writeIncludes(view, writer); + } - writer.writeLine(); - writer.writeLine("hide stereotype"); - writer.writeLine(); - - List elements = view.getElements().stream().map(ElementView::getElement).sorted(Comparator.comparing(Element::getName)).collect(Collectors.toList()); - for (Element element : elements) { - String id = idOf(element); - - String type = plantUMLShapeOf(view, element); - if ("actor".equals(type)) { - type = "rectangle"; // the actor shape is not supported in this implementation - } - - ElementStyle elementStyle = findElementStyle(view, element); - - String background = elementStyle.getBackground(); - String stroke = elementStyle.getStroke(); - String color = elementStyle.getColor(); - Shape shape = elementStyle.getShape(); - - if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { - type = "sequenceParticipant"; - } - - writer.writeLine(format("skinparam %s<<%s>> {", type, id)); - writer.indent(); - if (element instanceof DeploymentNode) { - writer.writeLine("BackgroundColor #ffffff"); - } else { - writer.writeLine(String.format("BackgroundColor %s", background)); - } - writer.writeLine(String.format("FontColor %s", color)); - writer.writeLine(String.format("BorderColor %s", stroke)); - - if (shape == Shape.RoundedBox) { - writer.writeLine("roundCorner 20"); - } - - boolean shadow = "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")); - writer.writeLine(String.format("shadowing %s", shadow)); - - writer.outdent(); - writer.writeLine("}"); - } + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + super.writeFooter(view, writer); + writeStyles(writer); + } - if (!renderAsSequenceDiagram(view)) { - // boundaries - List 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) { - boundaryElements.addAll(getBoundarySoftwareSystems(view)); - } else if (dynamicView.getElement() instanceof Container) { - boundaryElements.addAll(getBoundaryContainers(view)); - } - } + private void writeStyles(IndentingWriter writer) { + StringBuilder styles = new StringBuilder(); + List sortedStyles = plantUMLStyles.stream().sorted(Comparator.comparing(PlantUMLStyle::getName)).toList(); - for (Element boundaryElement : boundaryElements) { - ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(boundaryElement); - String id = idOf(boundaryElement); - String color = elementStyle.getStroke(); - boolean shadow = "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")); - - writer.writeLine(format("skinparam rectangle<<%s>> {", id)); - writer.indent(); - writer.writeLine(String.format("BorderColor %s", color)); - writer.writeLine(String.format("FontColor %s", color)); - writer.writeLine(String.format("shadowing %s", shadow)); - writer.outdent(); - writer.writeLine("}"); - } - } + sortedStyles.stream().filter(style -> style instanceof PlantUMLRootStyle).forEach(style -> styles.append(style.toString())); + sortedStyles.stream().filter(style -> style instanceof PlantUMLElementStyle).forEach(style -> styles.append(style.toString())); + sortedStyles.stream().filter(style -> style instanceof PlantUMLRelationshipStyle).forEach(style -> styles.append(style.toString())); + sortedStyles.stream().filter(style -> style instanceof PlantUMLBoundaryStyle).forEach(style -> styles.append(style.toString())); + sortedStyles.stream().filter(style -> style instanceof PlantUMLGroupStyle).forEach(style -> styles.append(style.toString())); + sortedStyles.stream().filter(style -> style instanceof PlantUMLLegendStyle).forEach(style -> styles.append(style.toString())); - writer.writeLine(); + writer.replace("", ""); } @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { - groupId++; String groupName = group; String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); @@ -156,37 +115,37 @@ protected void startGroupBoundary(ModelView view, String group, IndentingWriter groupName = group.substring(group.lastIndexOf(groupSeparator) + groupSeparator.length()); } - if (!renderAsSequenceDiagram(view)) { - String color = "#cccccc"; - String icon = ""; - - ElementStyle elementStyleForGroup = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group:" + group); - ElementStyle elementStyleForAllGroups = view.getViewSet().getConfiguration().getStyles().findElementStyle("Group"); - - if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getColor())) { - color = elementStyleForGroup.getColor(); - } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getColor())) { - color = elementStyleForAllGroups.getColor(); - } - - if (elementStyleForGroup != null && elementStyleHasSupportedIcon(elementStyleForGroup)) { - icon = elementStyleForGroup.getIcon(); - } else if (elementStyleForAllGroups != null && elementStyleHasSupportedIcon(elementStyleForAllGroups)) { - icon = elementStyleForAllGroups.getColor(); - } + ElementStyle elementStyle = findGroupStyle(view, group); + PlantUMLGroupStyle plantUMLBoundaryStyle = new PlantUMLGroupStyle( + group, + elementStyle.getBackground(), + elementStyle.getColor(), + elementStyle.getStroke(), + elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH, + elementStyle.getBorder(), + elementStyle.getFontSize(), + "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")) + ); + plantUMLStyles.add(plantUMLBoundaryStyle); + if (!renderAsSequenceDiagram(view)) { + String icon = elementStyle.getIcon(); if (!StringUtils.isNullOrEmpty(icon)) { - double scale = calculateIconScale(icon); + double scale = calculateIconScale(icon, elementStyle.getFontSize() * MAX_ICON_SIZE_RATIO); icon = "\\n\\n"; + } else { + icon = ""; } - writer.writeLine(String.format("rectangle \"%s%s\" <> as group%s {", groupName, icon, groupId, groupId)); + writer.writeLine( + String.format( + "rectangle \"%s%s\" <<%s>> as group%s {", + groupName, + icon, + classSelectorForGroup(group), + Base64.getEncoder().encodeToString(group.getBytes())) + ); writer.indent(); - writer.writeLine(String.format("skinparam RectangleBorderColor<> %s", groupId, color)); - writer.writeLine(String.format("skinparam RectangleFontColor<> %s", groupId, color)); - writer.writeLine(String.format("skinparam RectangleBorderStyle<> dashed", groupId)); - - writer.writeLine(); } } @@ -202,7 +161,28 @@ protected void endGroupBoundary(ModelView view, IndentingWriter writer) { @Override protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { if (!renderAsSequenceDiagram(view)) { - writer.writeLine(String.format("rectangle \"%s\\n%s\" <<%s>> {", softwareSystem.getName(), typeOf(view, softwareSystem, true), idOf(softwareSystem))); + ElementStyle elementStyle = findBoundaryStyle(view, softwareSystem); + PlantUMLBoundaryStyle plantUMLBoundaryStyle = new PlantUMLBoundaryStyle( + softwareSystem.getName(), + elementStyle.getBackground(), + elementStyle.getColor(), + elementStyle.getStroke(), + elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH, + elementStyle.getBorder(), + elementStyle.getFontSize(), + "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")) + ); + plantUMLStyles.add(plantUMLBoundaryStyle); + + writer.writeLine( + String.format( + "rectangle \"%s\\n%s\" <<%s>> {", + softwareSystem.getName(), + calculateMetadataFontSize(elementStyle.getFontSize()), + typeOf(view, softwareSystem, true), + plantUMLBoundaryStyle.getClassSelector() + ) + ); writer.indent(); } } @@ -219,7 +199,25 @@ protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) @Override protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { if (!renderAsSequenceDiagram(view)) { - writer.writeLine(String.format("rectangle \"%s\\n%s\" <<%s>> {", container.getName(), typeOf(view, container, true), idOf(container))); + ElementStyle elementStyle = findBoundaryStyle(view, container); + PlantUMLBoundaryStyle plantUMLBoundaryStyle = new PlantUMLBoundaryStyle( + container.getName(), + elementStyle.getBackground(), + elementStyle.getColor(), + elementStyle.getStroke(), + elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH, + elementStyle.getBorder(), + elementStyle.getFontSize(), + "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")) + ); + plantUMLStyles.add(plantUMLBoundaryStyle); + + writer.writeLine( + String.format( + "rectangle \"%s\\n%s\" <<%s>> {", + container.getName(), + calculateMetadataFontSize(findBoundaryStyle(view, container).getFontSize()), typeOf(view, container, true), + plantUMLBoundaryStyle.getClassSelector())); writer.indent(); } } @@ -237,9 +235,24 @@ protected void endContainerBoundary(ModelView view, IndentingWriter writer) { protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { ElementStyle elementStyle = findElementStyle(view, deploymentNode); + PlantUMLElementStyle plantUMLElementStyle = new PlantUMLElementStyle( + elementStyle.getTag(), + elementStyle.getShape(), + elementStyle.getWidth(), + elementStyle.getBackground(), + elementStyle.getColor(), + elementStyle.getStroke(), + elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH, + elementStyle.getBorder(), + elementStyle.getFontSize(), + elementStyle.getIcon(), + "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")) + ); + plantUMLStyles.add(plantUMLElementStyle); + String icon = ""; - if (elementStyleHasSupportedIcon(elementStyle)) { - double scale = calculateIconScale(elementStyle.getIcon()); + if (isSupportedIcon(elementStyle.getIcon())) { + double scale = calculateIconScale(elementStyle.getIcon(), elementStyle.getFontSize() * MAX_ICON_SIZE_RATIO); icon = "\\n\\n"; } @@ -251,11 +264,13 @@ protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode d } writer.writeLine( - format("rectangle \"%s\\n%s%s\" <<%s>> as %s%s {", + format( + "rectangle \"%s\\n%s%s\" <<%s>> as %s%s {", deploymentNode.getName() + (!"1".equals(deploymentNode.getInstances()) ? " (x" + deploymentNode.getInstances() + ")" : ""), + calculateMetadataFontSize(findBoundaryStyle(view, deploymentNode).getFontSize()), typeOf(view, deploymentNode, true), icon, - idOf(deploymentNode), + classSelectorFor(elementStyle), idOf(deploymentNode), url ) @@ -290,10 +305,17 @@ public Diagram export(DynamicView view) { writeElement(view, element, writer); } + if (!elements.isEmpty()) { + writer.writeLine(); + } + writeRelationships(view, writer); writeFooter(view, writer); - return createDiagram(view, writer.toString()); + Diagram diagram = createDiagram(view, writer.toString()); + diagram.setLegend(createLegend(view)); + + return diagram; } else { return super.export(view); } @@ -303,19 +325,34 @@ public Diagram export(DynamicView view) { protected void writeElement(ModelView view, Element element, IndentingWriter writer) { ElementStyle elementStyle = findElementStyle(view, element); + PlantUMLElementStyle plantUMLElementStyle = new PlantUMLElementStyle( + elementStyle.getTag(), + elementStyle.getShape(), + elementStyle.getWidth(), + elementStyle.getBackground(), + elementStyle.getColor(), + elementStyle.getStroke(), + elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH, + elementStyle.getBorder(), + elementStyle.getFontSize(), + elementStyle.getIcon(), + "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")) + ); + plantUMLStyles.add(plantUMLElementStyle); + + int metadataFontSize = calculateMetadataFontSize(elementStyle.getFontSize()); + if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { - writer.writeLine(String.format("%s \"%s\\n%s\" as %s <<%s>> %s", + writer.writeLine(String.format("%s \"%s\\n%s\" as %s <<%s>> %s", plantumlSequenceType(view, element), element.getName(), + metadataFontSize, typeOf(view, element, true), idOf(element), - idOf(element), + plantUMLElementStyle.getClassSelector(), elementStyle.getBackground())); } else { String shape = plantUMLShapeOf(view, element); - if ("actor".equals(shape)) { - shape = "rectangle"; - } String name = element.getName(); String description = element.getDescription(); String type = typeOf(view, element, true); @@ -350,23 +387,24 @@ protected void writeElement(ModelView view, Element element, IndentingWriter wri if (StringUtils.isNullOrEmpty(type) || false == elementStyle.getMetadata()) { type = ""; } else { - type = String.format("\\n%s", type); + type = String.format("\\n%s", metadataFontSize, type); } - if (elementStyleHasSupportedIcon(elementStyle)) { - double scale = calculateIconScale(elementStyle.getIcon()); + if (isSupportedIcon(elementStyle.getIcon())) { + double scale = calculateIconScale(elementStyle.getIcon(), elementStyle.getFontSize() * MAX_ICON_SIZE_RATIO); icon = "\\n\\n"; } + String classSelector = plantUMLElementStyle.getClassSelector(); String id = idOf(element); writer.writeLine(format("%s \"==%s%s%s%s\" <<%s>> as %s%s", shape, name, type, - description, icon, - id, + description, + classSelector, id, url) ); @@ -382,6 +420,15 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi Relationship relationship = relationshipView.getRelationship(); RelationshipStyle style = findRelationshipStyle(view, relationship); + PlantUMLRelationshipStyle plantUMLRelationshipStyle = new PlantUMLRelationshipStyle( + style.getTag(), + style.getColor(), + style.getStyle(), + style.getThickness(), + style.getFontSize() + ); + plantUMLStyles.add(plantUMLRelationshipStyle); + String description = ""; String technology = relationship.getTechnology(); @@ -405,46 +452,36 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi } writer.writeLine( - String.format("%s %s[%s]%s %s : %s", + String.format("%s %s%s %s <<%s>> : %s", idOf(relationship.getSource()), arrowStart, - style.getColor(), arrowEnd, idOf(relationship.getDestination()), + plantUMLRelationshipStyle.getClassSelector(), description)); } else { - boolean solid = style.getStyle() == LineStyle.Solid || false == style.getDashed(); - - String arrowStart; - String arrowEnd; - String relationshipStyle = style.getColor(); - - if (style.getThickness() != null) { - relationshipStyle += ",thickness=" + style.getThickness(); - } + String arrow; if (relationshipView.isResponse() != null && relationshipView.isResponse()) { - arrowStart = solid ? "<-" : "<."; - arrowEnd = solid ? "-" : "."; + arrow = "<--"; } else { - arrowStart = solid ? "-" : "."; - arrowEnd = solid ? "->" : ".>"; + arrow = "-->"; } - if (!isVisible(view, relationshipView)) { - relationshipStyle = "hidden"; - } +// if (!isVisible(view, relationshipView)) { +// relationshipStyle = "hidden"; +// } + + int metadataFontSize = calculateMetadataFontSize(style.getFontSize()); - // 1 .[#rrggbb,thickness=n].> 2 : "...\n... - writer.writeLine(format("%s %s[%s]%s %s : \"%s%s\"", + // 1 --> 2 : "...\n... + writer.writeLine(format("%s %s %s <<%s>> : \"%s%s\"", idOf(relationship.getSource()), - arrowStart, - relationshipStyle, - arrowEnd, + arrow, idOf(relationship.getDestination()), - style.getColor(), + plantUMLRelationshipStyle.getClassSelector(), description, - (StringUtils.isNullOrEmpty(technology) ? "" : "\\n[" + technology + "]") + (StringUtils.isNullOrEmpty(technology) ? "" : "\\n[" + technology + "]") )); } } @@ -452,156 +489,66 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi @Override protected Legend createLegend(ModelView view) { IndentingWriter writer = new IndentingWriter(); - int id = 0; writer.writeLine("@startuml"); - writer.writeLine("set separator none"); writer.writeLine(); - - writer.writeLine("skinparam {"); - writer.indent(); - writer.writeLine("shadowing false"); - writer.writeLine("arrowFontSize 15"); - writer.writeLine("defaultTextAlignment center"); - writer.writeLine("wrapWidth 100"); - writer.writeLine("maxMessageSize 100"); - Font font = view.getViewSet().getConfiguration().getBranding().getFont(); - if (font != null) { - String fontName = font.getName(); - if (!StringUtils.isNullOrEmpty(fontName)) { - writer.writeLine("defaultFontName \"" + fontName + "\""); - } - } - writer.outdent(); - writer.writeLine("}"); - + writer.writeLine("set separator none"); writer.writeLine("hide stereotype"); writer.writeLine(); - writer.writeLine("skinparam rectangle<<_transparent>> {"); - writer.indent(); - writer.writeLine("BorderColor transparent"); - writer.writeLine("BackgroundColor transparent"); - writer.writeLine("FontColor transparent"); - writer.outdent(); - writer.writeLine("}"); + writer.writeLine(""); writer.writeLine(); - Map elementStyles = new HashMap<>(); - List elements = view.getElements().stream().map(ElementView::getElement).collect(Collectors.toList()); - for (Element element : elements) { - ElementStyle elementStyle = findElementStyle(view, element); - - if (element instanceof DeploymentNode) { - // deployment node backgrounds are always white - elementStyle.setBackground("#ffffff"); - } - - if (!StringUtils.isNullOrEmpty(elementStyle.getTag()) ) { - elementStyles.put(elementStyle.getTag(), elementStyle); - }; - } - - List sortedElementStyles = elementStyles.values().stream().sorted(Comparator.comparing(ElementStyle::getTag)).collect(Collectors.toList());; - for (ElementStyle elementStyle : sortedElementStyles) { - id++; - Shape shape = elementStyle.getShape(); - String type = plantUMLShapeOf(elementStyle.getShape()); - if ("actor".equals(type)) { - type = "rectangle"; // the actor shape is not supported in this implementation - } - - String background = elementStyle.getBackground(); - String stroke = elementStyle.getStroke(); - String color = elementStyle.getColor(); - - if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { - type = "sequenceParticipant"; - } - - writer.writeLine(format("skinparam %s<<%s>> {", type, id)); - writer.indent(); - writer.writeLine(String.format("BackgroundColor %s", background)); - writer.writeLine(String.format("FontColor %s", color)); - writer.writeLine(String.format("BorderColor %s", stroke)); - - if (shape == Shape.RoundedBox) { - writer.writeLine("roundCorner 20"); - } - writer.outdent(); - writer.writeLine("}"); - - String description = elementStyle.getTag(); + plantUMLStyles.stream().sorted(Comparator.comparing(PlantUMLStyle::getName)).filter(style -> style instanceof PlantUMLElementStyle).map(style -> (PlantUMLElementStyle)style).forEach(style -> { + style.setWidth(200); + String description = style.getName(); if (description.startsWith("Element,")) { description = description.substring("Element,".length()); } description = description.replaceAll(",", ", "); String icon = ""; - if (elementStyleHasSupportedIcon(elementStyle)) { - double scale = calculateIconScale(elementStyle.getIcon()); - icon = "\\n\\n"; + if (isSupportedIcon(style.getIcon())) { + double scale = calculateIconScale(style.getIcon(), style.getFontSize() * MAX_ICON_SIZE_RATIO); + icon = "\\n\\n"; } writer.writeLine(format("%s \"==%s%s\" <<%s>>", - type, + plantUMLShapeOf(style.getShape()), description, icon, - id) + style.getClassSelector()) ); writer.writeLine(); - } - - Map relationshipStyles = new HashMap<>(); - List relationships = view.getRelationships().stream().map(RelationshipView::getRelationship).collect(Collectors.toList()); - for (Relationship relationship : relationships) { - RelationshipStyle relationshipStyle = findRelationshipStyle(view, relationship); + }); - if (!StringUtils.isNullOrEmpty(relationshipStyle.getTag())) { - relationshipStyles.put(relationshipStyle.getTag(), relationshipStyle); - } - } - - List sortedRelationshipStyles = relationshipStyles.values().stream().sorted(Comparator.comparing(RelationshipStyle::getTag)).collect(Collectors.toList());; - for (RelationshipStyle relationshipStyle : sortedRelationshipStyles) { + int id = 0; + List relationshipStyles = plantUMLStyles.stream().sorted(Comparator.comparing(PlantUMLStyle::getName)).filter(style -> style instanceof PlantUMLRelationshipStyle).map(style -> (PlantUMLRelationshipStyle)style).toList(); + for (PlantUMLRelationshipStyle relationshipStyle : relationshipStyles) { id++; - - String description = relationshipStyle.getTag(); + String description = relationshipStyle.getName(); if (description.startsWith("Relationship,")) { description = description.substring("Relationship,".length()); } description = description.replaceAll(",", ", "); - writer.writeLine(format("rectangle \".\" <<_transparent>> as %s", id)); - - boolean solid = relationshipStyle.getStyle() == LineStyle.Solid || false == relationshipStyle.getDashed(); - - String arrowStart = solid ? "-" : "."; - String arrowEnd = solid ? "->" : ".>"; - String buf = relationshipStyle.getColor(); - - if (relationshipStyle.getThickness() != null) { - buf += ",thickness=" + relationshipStyle.getThickness(); - } - - // 1 .[#rrggbb,thickness=n].> 2 : "..." - writer.writeLine(format("%s %s[%s]%s %s : \"%s\"", + // id --> id : "..." + writer.writeLine(format("rectangle \".\" <<.Element-Transparent>> as %s", id)); + writer.writeLine(format("%s --> %s <<%s>> : \"%s\"", id, - arrowStart, - buf, - arrowEnd, id, - relationshipStyle.getColor(), + relationshipStyle.getClassSelector(), description) ); writer.writeLine(); - } - - writer.writeLine(); + }; writer.writeLine("@enduml"); + plantUMLStyles.add(new PlantUMLLegendStyle()); + writeStyles(writer); + return new Legend(writer.toString()); } @@ -609,4 +556,101 @@ protected boolean renderAsSequenceDiagram(ModelView view) { return view instanceof DynamicView && "true".equalsIgnoreCase(getViewOrViewSetProperty(view, PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "false")); } -} + private String classSelectorFor(ElementStyle elementStyle) { + return "Element-" + Base64.getEncoder().encodeToString(elementStyle.getTag().getBytes()); + } + + private String classSelectorForBoundary(Element element) { + return "Boundary-" + Base64.getEncoder().encodeToString(element.getName().getBytes()); + } + + private ElementStyle findBoundaryStyle(ModelView view, Element element) { + return findElementStyle(view, element); + } + + private String classSelectorForGroup(String group) { + return "Group-" + Base64.getEncoder().encodeToString(group.getBytes()); + } + + private ElementStyle findGroupStyle(ModelView view, String group) { + String background = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_BACKGROUND_DARK : Styles.DEFAULT_BACKGROUND_LIGHT; + String stroke = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_COLOR_DARK : Styles.DEFAULT_COLOR_LIGHT; + int strokeWidth = DEFAULT_STROKE_WIDTH; + Border border = Border.Dotted; + String color = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_COLOR_DARK : Styles.DEFAULT_COLOR_LIGHT; + String icon = ""; + int fontSize = DEFAULT_FONT_SIZE; + + ElementStyle style = new ElementStyle(""); + ElementStyle elementStyleForGroup = findElementStyle(view, "Group:" + group); + ElementStyle elementStyleForAllGroups = findElementStyle(view, "Group"); + + if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getBackground())) { + background = elementStyleForGroup.getBackground(); + } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getBackground())) { + background = elementStyleForAllGroups.getBackground(); + } + style.setBackground(background); + + if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getStroke())) { + stroke = elementStyleForGroup.getStroke(); + } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getStroke())) { + stroke = elementStyleForAllGroups.getStroke(); + } + style.setStroke(stroke); + + if (elementStyleForGroup != null && elementStyleForGroup.getStrokeWidth() != null) { + strokeWidth = elementStyleForGroup.getStrokeWidth(); + } else if (elementStyleForAllGroups != null && elementStyleForAllGroups.getStrokeWidth() != null) { + strokeWidth = elementStyleForAllGroups.getStrokeWidth(); + } + style.setStrokeWidth(strokeWidth); + + if (elementStyleForGroup != null && !StringUtils.isNullOrEmpty(elementStyleForGroup.getColor())) { + color = elementStyleForGroup.getColor(); + } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getColor())) { + color = elementStyleForAllGroups.getColor(); + } + style.setColor(color); + + if (elementStyleForGroup != null && elementStyleForGroup.getBorder() != null) { + border = elementStyleForGroup.getBorder(); + } else if (elementStyleForAllGroups != null && elementStyleForAllGroups.getBorder() != null) { + border = elementStyleForAllGroups.getBorder(); + } + style.setBorder(border); + + if (elementStyleForGroup != null && isSupportedIcon(elementStyleForGroup.getIcon())) { + icon = elementStyleForGroup.getIcon(); + } else if (elementStyleForAllGroups != null && isSupportedIcon(elementStyleForAllGroups.getIcon())) { + icon = elementStyleForAllGroups.getColor(); + } + style.setIcon(icon); + + if (elementStyleForGroup != null && elementStyleForGroup.getFontSize() != null) { + fontSize = elementStyleForGroup.getFontSize(); + } else if (elementStyleForAllGroups != null && elementStyleForGroup.getFontSize() != null) { + fontSize = elementStyleForAllGroups.getFontSize(); + } + style.setFontSize(fontSize); + + return style; + } + + private int calculateMetadataFontSize(int fontSize) { + return (int)Math.floor(fontSize * METADATA_FONT_SIZE_RATIO); + } + + private String toLineStyle(ElementStyle elementStyle) { + int strokeWidth = elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH; + switch (elementStyle.getBorder()) { + case Dotted: + return (strokeWidth * 1) + "-" + (strokeWidth * 1); + case Dashed: + return (strokeWidth * 5) + "-" + (strokeWidth * 5); + default: + return "0"; + } + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot deleted file mode 100644 index 679e725e2..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Components.dot +++ /dev/null @@ -1,43 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
Internet Banking System - API Application - Components
The component diagram for the API Application.> - - 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - - subgraph cluster_11 { - margin=25 - label=<
API Application

[Container: Java and Spring MVC]> - labelloc=b - color="#444444" - fontcolor="#444444" - fillcolor="#444444" - - 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 13 [id=13,shape=rect, label=<Accounts Summary
Controller

[Component: Spring MVC Rest Controller]

Provides customers with a
summary of their bank
accounts.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 14 [id=14,shape=rect, label=<Reset Password
Controller

[Component: Spring MVC Rest Controller]

Allows users to reset their
passwords with a single use
URL.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 16 [id=16,shape=rect, label=<Mainframe Banking
System Facade

[Component: Spring Bean]

A facade onto the mainframe
banking system.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 17 [id=17,shape=rect, label=<E-mail Component
[Component: Spring Bean]

Sends e-mails to users.>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - } - - 8 -> 12 [id=32, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 8 -> 13 [id=34, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 8 -> 14 [id=35, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 9 -> 12 [id=36, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 9 -> 13 [id=38, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 9 -> 14 [id=39, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 12 -> 15 [id=40, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 13 -> 16 [id=41, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 14 -> 15 [id=42, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 14 -> 17 [id=43, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 15 -> 18 [id=44, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] - 16 -> 4 [id=46, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 17 -> 5 [id=48, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot deleted file mode 100644 index a0deff955..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-Containers.dot +++ /dev/null @@ -1,37 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
Internet Banking System - Containers
The container diagram for the Internet Banking System.> - - 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] - 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - - subgraph cluster_7 { - margin=25 - label=<
Internet Banking System

[Software System]> - labelloc=b - color="#444444" - fontcolor="#444444" - fillcolor="#444444" - - 10 [id=10,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 11 [id=11,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 10 [id=28, label=<Visits bigbank.com/ib
using

[HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 8 [id=29, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 9 [id=30, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 10 -> 8 [id=31, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] - 8 -> 11 [id=33, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 9 -> 11 [id=37, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 11 -> 18 [id=45, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] - 11 -> 4 [id=47, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 11 -> 5 [id=49, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot deleted file mode 100644 index 784eaeae4..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot +++ /dev/null @@ -1,97 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
Internet Banking System - Deployment - Development
An example development deployment scenario for the Internet Banking System.> - - subgraph cluster_50 { - margin=25 - label=<Developer Laptop
[Deployment Node: Microsoft Windows 10 or Apple macOS]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_51 { - margin=25 - label=<Web Browser
[Deployment Node: Chrome, Firefox, Safari, or Edge]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 52 [id=52,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - subgraph cluster_53 { - margin=25 - label=<Docker Container - Web Server
[Deployment Node: Docker]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_54 { - margin=25 - label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 55 [id=55,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 57 [id=57,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - } - - subgraph cluster_59 { - margin=25 - label=<Docker Container - Database Server
[Deployment Node: Docker]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_60 { - margin=25 - label=<Database Server
[Deployment Node: Oracle 12c]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 61 [id=61,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - } - - } - - subgraph cluster_63 { - margin=25 - label=<Big Bank plc
[Deployment Node: Big Bank plc data center]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_64 { - margin=25 - label=<bigbank-dev001
[Deployment Node]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 65 [id=65,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - } - - } - - 55 -> 52 [id=56, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] - 52 -> 57 [id=58, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 57 -> 61 [id=62, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] - 57 -> 65 [id=66, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot deleted file mode 100644 index 5ac04100c..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot +++ /dev/null @@ -1,152 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
Internet Banking System - Deployment - Live
An example live deployment scenario for the Internet Banking System.> - - subgraph cluster_67 { - margin=25 - label=<Customer's mobile device
[Deployment Node: Apple iOS or Android]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 68 [id=68,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - subgraph cluster_69 { - margin=25 - label=<Customer's computer
[Deployment Node: Microsoft Windows or Apple macOS]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_70 { - margin=25 - label=<Web Browser
[Deployment Node: Chrome, Firefox, Safari, or Edge]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 71 [id=71,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - } - - subgraph cluster_72 { - margin=25 - label=<Big Bank plc
[Deployment Node: Big Bank plc data center]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_73 { - margin=25 - label=<bigbank-web***
[Deployment Node: Ubuntu 16.04 LTS]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_74 { - margin=25 - label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 75 [id=75,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - } - - subgraph cluster_77 { - margin=25 - label=<bigbank-api***
[Deployment Node: Ubuntu 16.04 LTS]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_78 { - margin=25 - label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 79 [id=79,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - } - - subgraph cluster_82 { - margin=25 - label=<bigbank-db01
[Deployment Node: Ubuntu 16.04 LTS]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_83 { - margin=25 - label=<Oracle - Primary
[Deployment Node: Oracle 12c]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 84 [id=84,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - } - - subgraph cluster_86 { - margin=25 - label=<bigbank-db02
[Deployment Node: Ubuntu 16.04 LTS]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - subgraph cluster_87 { - margin=25 - label=<Oracle - Secondary
[Deployment Node: Oracle 12c]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 88 [id=88,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - } - - } - - subgraph cluster_90 { - margin=25 - label=<bigbank-prod001
[Deployment Node]> - labelloc=b - color="#888888" - fontcolor="#000000" - fillcolor="#ffffff" - - 91 [id=91,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - } - - } - - 75 -> 71 [id=76, label=<Delivers to the customer's
web browser
>, style="dashed", color="#707070", fontcolor="#707070"] - 68 -> 79 [id=80, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 71 -> 79 [id=81, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 79 -> 84 [id=85, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] - 79 -> 88 [id=89, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] - 79 -> 91 [id=92, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 84 -> 88 [id=93, label=<Replicates data to>, style="dashed", color="#707070", fontcolor="#707070",ltail=cluster_83,lhead=cluster_87] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot deleted file mode 100644 index 3e840c871..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SignIn.dot +++ /dev/null @@ -1,29 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
API Application - Dynamic
Summarises how the sign in feature works in the single-page application.> - - subgraph cluster_11 { - margin=25 - label=<
API Application

[Container: Java and Spring MVC]> - labelloc=b - color="#444444" - fontcolor="#444444" - fillcolor="#444444" - - 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - } - - 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - - 8 -> 12 [id=32, label=<1. Submits credentials to
[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 12 -> 15 [id=40, label=<2. Validates credentials
using
>, style="dashed", color="#707070", fontcolor="#707070"] - 15 -> 18 [id=44, label=<3. select * from users
where username = ?

[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] - 18 -> 15 [id=44, label=<4. Returns user data to
[SQL/TCP]>, style="dashed", color="#707070", fontcolor="#707070"] - 15 -> 12 [id=40, label=<5. Returns true if the
hashed password matches
>, style="dashed", color="#707070", fontcolor="#707070"] - 12 -> 8 [id=32, label=<6. Sends back an
authentication token to

[JSON/HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot deleted file mode 100644 index 73780f17e..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemContext.dot +++ /dev/null @@ -1,28 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
Internet Banking System - System Context
The system context diagram for the Internet Banking System.> - - subgraph "cluster_group_Big Bank plc" { - margin=25 - label=<
Big Bank plc
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 7 [id=7,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] - } - - 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] - - 1 -> 7 [id=19, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 7 -> 4 [id=20, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#707070", fontcolor="#707070"] - 7 -> 5 [id=21, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] - 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot deleted file mode 100644 index 2b691cc0d..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot +++ /dev/null @@ -1,36 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
System Landscape> - - subgraph "cluster_group_Big Bank plc" { - margin=25 - label=<
Big Bank plc
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 2 [id=2,shape=rect, label=<Customer Service
Staff

[Person]

Customer service staff within
the bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 3 [id=3,shape=rect, label=<Back Office Staff
[Person]

Administration and support
staff within the bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 6 [id=6,shape=rect, label=<ATM
[Software System]

Allows customers to withdraw
cash.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 7 [id=7,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] - } - - 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] - - 1 -> 7 [id=19, label=<Views account balances,
and makes payments using
>, style="dashed", color="#707070", fontcolor="#707070"] - 7 -> 4 [id=20, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#707070", fontcolor="#707070"] - 7 -> 5 [id=21, label=<Sends e-mail using>, style="dashed", color="#707070", fontcolor="#707070"] - 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 2 [id=23, label=<Asks questions to
[Telephone]>, style="dashed", color="#707070", fontcolor="#707070"] - 2 -> 4 [id=24, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 6 [id=25, label=<Withdraws cash using>, style="dashed", color="#707070", fontcolor="#707070"] - 6 -> 4 [id=26, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] - 3 -> 4 [id=27, label=<Uses>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot deleted file mode 100644 index 9c493cabe..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot +++ /dev/null @@ -1,75 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=LR, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
Spring PetClinic - Deployment - Live> - - subgraph cluster_5 { - margin=25 - label=<Amazon Web Services
[Deployment Node]> - labelloc=b - color="#232f3e" - fontcolor="#232f3e" - fillcolor="#ffffff" - - subgraph cluster_6 { - margin=25 - label=<US-East-1
[Deployment Node]> - labelloc=b - color="#147eba" - fontcolor="#147eba" - fillcolor="#ffffff" - - subgraph cluster_12 { - margin=25 - label=<Amazon RDS
[Deployment Node]> - labelloc=b - color="#3b48cc" - fontcolor="#3b48cc" - fillcolor="#ffffff" - - subgraph cluster_13 { - margin=25 - label=<MySQL
[Deployment Node]> - labelloc=b - color="#3b48cc" - fontcolor="#3b48cc" - fillcolor="#ffffff" - - 14 [id=14,shape=cylinder, label=<Database
[Container: Relational database schema]

Stores information regarding
the veterinarians, the
clients, and their pets.
>, style=filled, color="#b2b2b2", fillcolor="#ffffff", fontcolor="#000000"] - } - - } - - 7 [id=7,shape=rect, label=<Route 53
[Infrastructure Node]

Highly available and scalable
cloud DNS service.
>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] - 8 [id=8,shape=rect, label=<Elastic Load Balancer
[Infrastructure Node]

Automatically distributes
incoming application traffic.
>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] - subgraph cluster_9 { - margin=25 - label=<Autoscaling group
[Deployment Node]> - labelloc=b - color="#cc2264" - fontcolor="#cc2264" - fillcolor="#ffffff" - - subgraph cluster_10 { - margin=25 - label=<Amazon EC2
[Deployment Node]> - labelloc=b - color="#d86613" - fontcolor="#d86613" - fillcolor="#ffffff" - - 11 [id=11,shape=rect, label=<Web Application
[Container: Java and Spring Boot]

Allows employees to view and
manage information regarding
the veterinarians, the
clients, and their pets.
>, style=filled, color="#b2b2b2", fillcolor="#ffffff", fontcolor="#000000"] - } - - } - - } - - } - - 11 -> 14 [id=15, label=<Reads from and writes to
[MySQL Protocol/SSL]>, style="dashed", color="#707070", fontcolor="#707070"] - 7 -> 8 [id=16, label=<Forwards requests to
[HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] - 8 -> 11 [id=17, label=<Forwards requests to
[HTTPS]>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java index b19a96f36..02aa78296 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -18,45 +18,467 @@ public class DOTDiagramExporterTests extends AbstractExporterTests { @Test public void test_BigBankPlcExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/big-bank-plc.json")); DOTExporter dotWriter = new DOTExporter(); Collection diagrams = dotWriter.export(workspace); assertEquals(7, diagrams.size()); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-SystemLandscape.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
System Landscape> + + subgraph "cluster_group_Big Bank plc" { + margin=25 + label=<
Big Bank plc
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<Customer Service Staff
[Person]

Customer service staff within the
bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 3 [id=3,shape=rect, label=<Back Office Staff
[Person]

Administration and support staff
within the bank.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 6 [id=6,shape=rect, label=<ATM
[Software System]

Allows customers to withdraw
cash.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 7 [id=7,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] + } + + 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] + + 1 -> 7 [id=19, label=<Views account balances,
and makes payments using
>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 4 [id=20, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 5 [id=21, label=<Sends e-mail using>, style="dashed", color="#444444", fontcolor="#444444"] + 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 2 [id=23, label=<Asks questions to
[Telephone]>, style="dashed", color="#444444", fontcolor="#444444"] + 2 -> 4 [id=24, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 6 [id=25, label=<Withdraws cash using>, style="dashed", color="#444444", fontcolor="#444444"] + 6 -> 4 [id=26, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + 3 -> 4 [id=27, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemContext")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-SystemContext.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Internet Banking System - System Context
The system context diagram for the Internet Banking System.> + + subgraph "cluster_group_Big Bank plc" { + margin=25 + label=<
Big Bank plc
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 7 [id=7,shape=rect, label=<Internet Banking
System

[Software System]

Allows customers to view
information about their bank
accounts, and make payments.
>, style=filled, color="#0b4884", fillcolor="#1168bd", fontcolor="#ffffff"] + } + + 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] + + 1 -> 7 [id=19, label=<Views account balances,
and makes payments using
>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 4 [id=20, label=<Gets account information
from, and makes payments
using
>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 5 [id=21, label=<Sends e-mail using>, style="dashed", color="#444444", fontcolor="#444444"] + 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-Containers.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Internet Banking System - Containers
The container diagram for the Internet Banking System.> + + 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] + 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + + subgraph cluster_7 { + margin=25 + label=<
Internet Banking System

[Software System]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 10 [id=10,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 11 [id=11,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + 5 -> 1 [id=22, label=<Sends e-mails to>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 10 [id=28, label=<Visits bigbank.com/ib
using

[HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 8 [id=29, label=<Views account balances,
and makes payments using
>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 9 [id=30, label=<Views account balances,
and makes payments using
>, style="dashed", color="#444444", fontcolor="#444444"] + 10 -> 8 [id=31, label=<Delivers to the customer's
web browser
>, style="dashed", color="#444444", fontcolor="#444444"] + 8 -> 11 [id=33, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 11 [id=37, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 11 -> 18 [id=45, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] + 11 -> 4 [id=47, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 11 -> 5 [id=49, label=<Sends e-mail using>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-Components.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Internet Banking System - API Application - Components
The component diagram for the API Application.> + + 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + + subgraph cluster_11 { + margin=25 + label=<
API Application

[Container: Java and Spring MVC]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 13 [id=13,shape=rect, label=<Accounts Summary
Controller

[Component: Spring MVC Rest Controller]

Provides customers with a
summary of their bank
accounts.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 14 [id=14,shape=rect, label=<Reset Password
Controller

[Component: Spring MVC Rest Controller]

Allows users to reset their
passwords with a single use
URL.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 16 [id=16,shape=rect, label=<Mainframe Banking
System Facade

[Component: Spring Bean]

A facade onto the mainframe
banking system.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 17 [id=17,shape=rect, label=<E-mail Component
[Component: Spring Bean]

Sends e-mails to users.>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + } + + 8 -> 12 [id=32, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 8 -> 13 [id=34, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 8 -> 14 [id=35, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 12 [id=36, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 13 [id=38, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 14 [id=39, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 15 [id=40, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + 13 -> 16 [id=41, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + 14 -> 15 [id=42, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + 14 -> 17 [id=43, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + 15 -> 18 [id=44, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] + 16 -> 4 [id=46, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 17 -> 5 [id=48, label=<Sends e-mail using>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("SignIn")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-SignIn.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
API Application - Dynamic
Summarises how the sign in feature works in the single-page application.> + + subgraph cluster_11 { + margin=25 + label=<
API Application

[Container: Java and Spring MVC]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + } + + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + + 8 -> 12 [id=32, label=<1. Submits credentials to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 15 [id=40, label=<2. Validates credentials
using
>, style="dashed", color="#444444", fontcolor="#444444"] + 15 -> 18 [id=44, label=<3. select * from users
where username = ?

[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] + 18 -> 15 [id=44, label=<4. Returns user data to
[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] + 15 -> 12 [id=40, label=<5. Returns true if the
hashed password matches
>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 8 [id=32, label=<6. Sends back an
authentication token to

[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-DevelopmentDeployment.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Internet Banking System - Deployment - Development
An example development deployment scenario for the Internet Banking System.> + + subgraph cluster_50 { + margin=25 + label=<Developer Laptop
[Deployment Node: Microsoft Windows 10 or Apple macOS]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_51 { + margin=25 + label=<Web Browser
[Deployment Node: Chrome, Firefox, Safari, or Edge]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 52 [id=52,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + subgraph cluster_53 { + margin=25 + label=<Docker Container - Web Server
[Deployment Node: Docker]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_54 { + margin=25 + label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 55 [id=55,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 57 [id=57,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_59 { + margin=25 + label=<Docker Container - Database Server
[Deployment Node: Docker]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_60 { + margin=25 + label=<Database Server
[Deployment Node: Oracle 12c]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 61 [id=61,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + } + + subgraph cluster_63 { + margin=25 + label=<Big Bank plc
[Deployment Node: Big Bank plc data center]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_64 { + margin=25 + label=<bigbank-dev001
[Deployment Node]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 65 [id=65,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + } + + } + + 55 -> 52 [id=56, label=<Delivers to the customer's
web browser
>, style="dashed", color="#444444", fontcolor="#444444"] + 52 -> 57 [id=58, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 57 -> 61 [id=62, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] + 57 -> 65 [id=66, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/36141-LiveDeployment.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Internet Banking System - Deployment - Live
An example live deployment scenario for the Internet Banking System.> + + subgraph cluster_67 { + margin=25 + label=<Customer's mobile device
[Deployment Node: Apple iOS or Android]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 68 [id=68,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + subgraph cluster_69 { + margin=25 + label=<Customer's computer
[Deployment Node: Microsoft Windows or Apple macOS]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_70 { + margin=25 + label=<Web Browser
[Deployment Node: Chrome, Firefox, Safari, or Edge]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 71 [id=71,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_72 { + margin=25 + label=<Big Bank plc
[Deployment Node: Big Bank plc data center]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_73 { + margin=25 + label=<bigbank-web***
[Deployment Node: Ubuntu 16.04 LTS]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_74 { + margin=25 + label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 75 [id=75,shape=rect, label=<Web Application
[Container: Java and Spring MVC]

Delivers the static content
and the Internet banking
single page application.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_77 { + margin=25 + label=<bigbank-api***
[Deployment Node: Ubuntu 16.04 LTS]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_78 { + margin=25 + label=<Apache Tomcat
[Deployment Node: Apache Tomcat 8.x]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 79 [id=79,shape=rect, label=<API Application
[Container: Java and Spring MVC]

Provides Internet banking
functionality via a JSON/HTTPS
API.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_82 { + margin=25 + label=<bigbank-db01
[Deployment Node: Ubuntu 16.04 LTS]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_83 { + margin=25 + label=<Oracle - Primary
[Deployment Node: Oracle 12c]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 84 [id=84,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_86 { + margin=25 + label=<bigbank-db02
[Deployment Node: Ubuntu 16.04 LTS]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_87 { + margin=25 + label=<Oracle - Secondary
[Deployment Node: Oracle 12c]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 88 [id=88,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + } + + } + + subgraph cluster_90 { + margin=25 + label=<bigbank-prod001
[Deployment Node]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 91 [id=91,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + } + + } + + 75 -> 71 [id=76, label=<Delivers to the customer's
web browser
>, style="dashed", color="#444444", fontcolor="#444444"] + 68 -> 79 [id=80, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 71 -> 79 [id=81, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 79 -> 84 [id=85, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] + 79 -> 88 [id=89, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] + 79 -> 91 [id=92, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 84 -> 88 [id=93, label=<Replicates data to>, style="dashed", color="#444444", fontcolor="#444444",ltail=cluster_83,lhead=cluster_87] + + }""", diagram.getDefinition()); } @Test @Tag("IntegrationTest") public void test_AmazonWebServicesExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); ThemeUtils.loadThemes(workspace); workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); @@ -65,8 +487,83 @@ public void test_AmazonWebServicesExample() throws Exception { assertEquals(1, diagrams.size()); Diagram diagram = diagrams.stream().findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/54915-AmazonWebServicesDeployment.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=LR, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
X - Deployment - Live> + + subgraph cluster_5 { + margin=25 + label=<Amazon Web Services
[Deployment Node]> + labelloc=b + color="#232f3e" + fontcolor="#232f3e" + fillcolor="#ffffff" + + subgraph cluster_6 { + margin=25 + label=<US-East-1
[Deployment Node]> + labelloc=b + color="#147eba" + fontcolor="#147eba" + fillcolor="#ffffff" + + subgraph cluster_10 { + margin=25 + label=<Autoscaling group
[Deployment Node]> + labelloc=b + color="#cc2264" + fontcolor="#cc2264" + fillcolor="#ffffff" + + subgraph cluster_11 { + margin=25 + label=<Amazon EC2 - Ubuntu server
[Deployment Node]> + labelloc=b + color="#d86613" + fontcolor="#d86613" + fillcolor="#ffffff" + + 12 [id=12,shape=rect, label=<Web Application
[Container: Java and Spring Boot]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + subgraph cluster_14 { + margin=25 + label=<Amazon RDS
[Deployment Node]> + labelloc=b + color="#3b48cc" + fontcolor="#3b48cc" + fillcolor="#ffffff" + + subgraph cluster_15 { + margin=25 + label=<MySQL
[Deployment Node]> + labelloc=b + color="#3b48cc" + fontcolor="#3b48cc" + fillcolor="#ffffff" + + 16 [id=16,shape=cylinder, label=<Database Schema
[Container]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + 7 [id=7,shape=rect, label=<DNS router
[Infrastructure Node: Route 53]

Routes incoming requests based
upon domain name.
>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] + 8 [id=8,shape=rect, label=<Load Balancer
[Infrastructure Node: Elastic Load Balancer]

Automatically distributes
incoming application traffic.
>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] + } + + } + + 8 -> 12 [id=13, label=<Forwards requests to
[HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 16 [id=17, label=<Reads from and writes to
[MySQL Protocol/SSL]>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 8 [id=9, label=<Forwards requests to
[HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); } @Test @@ -79,16 +576,135 @@ public void test_GroupsExample() throws Exception { assertEquals(3, diagrams.size()); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
System Landscape> + + subgraph "cluster_group_Group 1" { + margin=25 + label=<
Group 1
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<B
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 2" { + margin=25 + label=<
Group 2
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + subgraph "cluster_group_Group 3" { + margin=25 + label=<
Group 3
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 4 [id=4,shape=rect, label=<D
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + 1 [id=1,shape=rect, label=<A
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + + 2 -> 3 [id=10, label=<>, style="dashed", color="#444444", fontcolor="#444444"] + 3 -> 4 [id=12, label=<>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 2 [id=9, label=<>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/groups-Containers.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
D - Containers> + + 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + + subgraph cluster_4 { + margin=25 + label=<
D

[Software System]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + subgraph "cluster_group_Group 4" { + margin=25 + label=<
Group 4
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 6 [id=6,shape=rect, label=<F
[Container]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 5 [id=5,shape=rect, label=<E
[Container]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 3 -> 5 [id=11, label=<>, style="dashed", color="#444444", fontcolor="#444444"] + 3 -> 6 [id=14, label=<>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/dot/groups-Components.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
D - F - Components> + + 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + + subgraph cluster_6 { + margin=25 + label=<
F

[Container]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + subgraph "cluster_group_Group 5" { + margin=25 + label=<
Group 5
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 8 [id=8,shape=rect, label=<H
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 7 [id=7,shape=rect, label=<G
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 3 -> 7 [id=13, label=<>, style="dashed", color="#444444", fontcolor="#444444"] + 3 -> 8 [id=15, label=<>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); } @Test @@ -118,8 +734,76 @@ public void test_NestedGroupsExample() throws Exception { Collection diagrams = exporter.export(workspace); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/dot/nested-groups.dot")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
System Landscape
Description> + + subgraph "cluster_group_Organisation 1" { + margin=25 + label=<
Organisation 1
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 3 [id=3,shape=rect, label=<Organisation 1
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + subgraph "cluster_group_Department 1" { + margin=25 + label=<
Department 1
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 5 [id=5,shape=rect, label=<Department 1
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + subgraph "cluster_group_Team 1" { + margin=25 + label=<
Team 1
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 1 [id=1,shape=rect, label=<Team 1
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Team 2" { + margin=25 + label=<
Team 2
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<Team 2
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + } + + subgraph "cluster_group_Organisation 2" { + margin=25 + label=<
Organisation 2
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 4 [id=4,shape=rect, label=<Organisation 2
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + + }""", diagram.getDefinition()); } @Test @@ -137,37 +821,39 @@ public void test_renderContainerDiagramWithExternalContainers() { containerView.add(container2); Diagram diagram = new DOTExporter().export(containerView); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + - " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + - " edge [fontname=\"Arial\"]\n" + - " label=<
Software System 1 - Containers>\n" + - "\n" + - " subgraph cluster_1 {\n" + - " margin=25\n" + - " label=<
Software System 1

[Software System]>\n" + - " labelloc=b\n" + - " color=\"#444444\"\n" + - " fontcolor=\"#444444\"\n" + - " fillcolor=\"#444444\"\n" + - "\n" + - " 2 [id=2,shape=rect, label=<Container 1
[Container]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " subgraph cluster_3 {\n" + - " margin=25\n" + - " label=<
Software System 2

[Software System]>\n" + - " labelloc=b\n" + - " color=\"#cccccc\"\n" + - " fontcolor=\"#cccccc\"\n" + - " fillcolor=\"#cccccc\"\n" + - "\n" + - " 4 [id=4,shape=rect, label=<Container 2
[Container]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " 2 -> 4 [id=5, label=<Uses>, style=\"dashed\", color=\"#707070\", fontcolor=\"#707070\"]\n" + - "}", diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Software System 1 - Containers> + + subgraph cluster_1 { + margin=25 + label=<
Software System 1

[Software System]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 2 [id=2,shape=rect, label=<Container 1
[Container]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph cluster_3 { + margin=25 + label=<
Software System 2

[Software System]> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + 4 [id=4,shape=rect, label=<Container 2
[Container]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 2 -> 4 [id=5, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); } @Test @@ -187,37 +873,39 @@ public void test_renderComponentDiagramWithExternalComponents() { componentView.add(component2); Diagram diagram = new DOTExporter().export(componentView); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + - " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + - " edge [fontname=\"Arial\"]\n" + - " label=<
Software System 1 - Container 1 - Components>\n" + - "\n" + - " subgraph cluster_2 {\n" + - " margin=25\n" + - " label=<
Container 1

[Container]>\n" + - " labelloc=b\n" + - " color=\"#444444\"\n" + - " fontcolor=\"#444444\"\n" + - " fillcolor=\"#444444\"\n" + - "\n" + - " 3 [id=3,shape=rect, label=<Component 1
[Component]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " subgraph cluster_5 {\n" + - " margin=25\n" + - " label=<
Container 2

[Container]>\n" + - " labelloc=b\n" + - " color=\"#cccccc\"\n" + - " fontcolor=\"#cccccc\"\n" + - " fillcolor=\"#cccccc\"\n" + - "\n" + - " 6 [id=6,shape=rect, label=<Component 2
[Component]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " 3 -> 6 [id=7, label=<Uses>, style=\"dashed\", color=\"#707070\", fontcolor=\"#707070\"]\n" + - "}", diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Software System 1 - Container 1 - Components> + + subgraph cluster_2 { + margin=25 + label=<
Container 1

[Container]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 3 [id=3,shape=rect, label=<Component 1
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph cluster_5 { + margin=25 + label=<
Container 2

[Container]> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + 6 [id=6,shape=rect, label=<Component 2
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 3 -> 6 [id=7, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); } @Test @@ -235,100 +923,102 @@ public void test_renderGroupStyles() { DOTExporter exporter = new DOTExporter(); Diagram diagram = exporter.export(view); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + - " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + - " edge [fontname=\"Arial\"]\n" + - " label=<
System Landscape>\n" + - "\n" + - " subgraph \"cluster_group_Group 1\" {\n" + - " margin=25\n" + - " label=<
Group 1
>\n" + - " labelloc=b\n" + - " color=\"#111111\"\n" + - " fontcolor=\"#111111\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 1 [id=1,shape=rect, label=<User 1
[Person]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_Group 2\" {\n" + - " margin=25\n" + - " label=<
Group 2
>\n" + - " labelloc=b\n" + - " color=\"#222222\"\n" + - " fontcolor=\"#222222\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 2 [id=2,shape=rect, label=<User 2
[Person]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_Group 3\" {\n" + - " margin=25\n" + - " label=<
Group 3
>\n" + - " labelloc=b\n" + - " color=\"#cccccc\"\n" + - " fontcolor=\"#cccccc\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 3 [id=3,shape=rect, label=<User 3
[Person]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - "\n" + - "}", diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
System Landscape> + + subgraph "cluster_group_Group 1" { + margin=25 + label=<
Group 1
> + labelloc=b + color="#111111" + fontcolor="#111111" + fillcolor="#ffffff" + style="dashed" + + 1 [id=1,shape=rect, label=<User 1
[Person]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 2" { + margin=25 + label=<
Group 2
> + labelloc=b + color="#222222" + fontcolor="#222222" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<User 2
[Person]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 3" { + margin=25 + label=<
Group 3
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 3 [id=3,shape=rect, label=<User 3
[Person]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + + }""", diagram.getDefinition()); workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); diagram = exporter.export(view); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + - " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + - " edge [fontname=\"Arial\"]\n" + - " label=<
System Landscape>\n" + - "\n" + - " subgraph \"cluster_group_Group 1\" {\n" + - " margin=25\n" + - " label=<
Group 1
>\n" + - " labelloc=b\n" + - " color=\"#111111\"\n" + - " fontcolor=\"#111111\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 1 [id=1,shape=rect, label=<User 1
[Person]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_Group 2\" {\n" + - " margin=25\n" + - " label=<
Group 2
>\n" + - " labelloc=b\n" + - " color=\"#222222\"\n" + - " fontcolor=\"#222222\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 2 [id=2,shape=rect, label=<User 2
[Person]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_Group 3\" {\n" + - " margin=25\n" + - " label=<
Group 3
>\n" + - " labelloc=b\n" + - " color=\"#aabbcc\"\n" + - " fontcolor=\"#aabbcc\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 3 [id=3,shape=rect, label=<User 3
[Person]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " }\n" + - "\n" + - "\n" + - "}", diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
System Landscape> + + subgraph "cluster_group_Group 1" { + margin=25 + label=<
Group 1
> + labelloc=b + color="#111111" + fontcolor="#111111" + fillcolor="#ffffff" + style="dashed" + + 1 [id=1,shape=rect, label=<User 1
[Person]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 2" { + margin=25 + label=<
Group 2
> + labelloc=b + color="#222222" + fontcolor="#222222" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<User 2
[Person]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 3" { + margin=25 + label=<
Group 3
> + labelloc=b + color="#aabbcc" + fontcolor="#aabbcc" + fillcolor="#ffffff" + style="dashed" + + 3 [id=3,shape=rect, label=<User 3
[Person]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + + }""", diagram.getDefinition()); } @Test @@ -344,18 +1034,20 @@ public void test_renderCustomView() { view.addDefaultElements(); Diagram diagram = new DOTExporter().export(view); - assertEquals("digraph {\n" + - " compound=true\n" + - " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + - " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + - " edge [fontname=\"Arial\"]\n" + - " label=<
Title
Description>\n" + - "\n" + - " 1 [id=1,shape=rect, label=<A>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - " 2 [id=2,shape=rect, label=<B
[Custom]

Description>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + - "\n" + - " 1 -> 2 [id=3, label=<Uses>, style=\"dashed\", color=\"#707070\", fontcolor=\"#707070\"]\n" + - "}", diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Title
Description> + + 1 [id=1,shape=rect, label=<A>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + 2 [id=2,shape=rect, label=<B
[Custom]

Description>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + + 1 -> 2 [id=3, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); } @Test @@ -394,7 +1086,7 @@ public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSepar " fillcolor=\"#ffffff\"\n" + " style=\"dashed\"\n" + "\n" + - " 2 [id=2,shape=rect, label=<Container 1
[Container]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " 2 [id=2,shape=rect, label=<Container 1
[Container]>, style=filled, color=\"#444444\", fillcolor=\"#ffffff\", fontcolor=\"#444444\"]\n" + " }\n" + "\n" + " subgraph \"cluster_group_Group 2\" {\n" + @@ -406,7 +1098,7 @@ public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSepar " fillcolor=\"#ffffff\"\n" + " style=\"dashed\"\n" + "\n" + - " 3 [id=3,shape=rect, label=<Container 2
[Container]>, style=filled, color=\"#9a9a9a\", fillcolor=\"#dddddd\", fontcolor=\"#000000\"]\n" + + " 3 [id=3,shape=rect, label=<Container 2
[Container]>, style=filled, color=\"#444444\", fillcolor=\"#ffffff\", fontcolor=\"#444444\"]\n" + " }\n" + "\n" + " }\n" + diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot deleted file mode 100644 index cc868ffe8..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Components.dot +++ /dev/null @@ -1,35 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
D - F - Components> - - 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - - subgraph cluster_6 { - margin=25 - label=<
F

[Container]> - labelloc=b - color="#444444" - fontcolor="#444444" - fillcolor="#444444" - - subgraph "cluster_group_Group 5" { - margin=25 - label=<
Group 5
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 8 [id=8,shape=rect, label=<H
[Component]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - 7 [id=7,shape=rect, label=<G
[Component]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - 3 -> 7 [id=13, label=<>, style="dashed", color="#707070", fontcolor="#707070"] - 3 -> 8 [id=15, label=<>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot deleted file mode 100644 index 013ecfbf2..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-Containers.dot +++ /dev/null @@ -1,35 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
D - Containers> - - 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - - subgraph cluster_4 { - margin=25 - label=<
D

[Software System]> - labelloc=b - color="#444444" - fontcolor="#444444" - fillcolor="#444444" - - subgraph "cluster_group_Group 4" { - margin=25 - label=<
Group 4
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 6 [id=6,shape=rect, label=<F
[Container]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - 5 [id=5,shape=rect, label=<E
[Container]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - 3 -> 5 [id=11, label=<>, style="dashed", color="#707070", fontcolor="#707070"] - 3 -> 6 [id=14, label=<>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot deleted file mode 100644 index 5147526da..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/groups-SystemLandscape.dot +++ /dev/null @@ -1,49 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
System Landscape> - - subgraph "cluster_group_Group 1" { - margin=25 - label=<
Group 1
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 2 [id=2,shape=rect, label=<B
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - subgraph "cluster_group_Group 2" { - margin=25 - label=<
Group 2
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - subgraph "cluster_group_Group 3" { - margin=25 - label=<
Group 3
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 4 [id=4,shape=rect, label=<D
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - } - - 1 [id=1,shape=rect, label=<A
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - - 2 -> 3 [id=10, label=<>, style="dashed", color="#707070", fontcolor="#707070"] - 3 -> 4 [id=12, label=<>, style="dashed", color="#707070", fontcolor="#707070"] - 1 -> 2 [id=9, label=<>, style="dashed", color="#707070", fontcolor="#707070"] -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot b/structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot deleted file mode 100644 index 702f73d43..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/nested-groups.dot +++ /dev/null @@ -1,69 +0,0 @@ -digraph { - compound=true - graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] - node [fontname="Arial", shape=box, margin="0.4,0.3"] - edge [fontname="Arial"] - label=<
System Landscape
Description> - - subgraph "cluster_group_Organisation 1" { - margin=25 - label=<
Organisation 1
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 3 [id=3,shape=rect, label=<Organisation 1
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - subgraph "cluster_group_Department 1" { - margin=25 - label=<
Department 1
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 5 [id=5,shape=rect, label=<Department 1
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - subgraph "cluster_group_Team 1" { - margin=25 - label=<
Team 1
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 1 [id=1,shape=rect, label=<Team 1
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - subgraph "cluster_group_Team 2" { - margin=25 - label=<
Team 2
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 2 [id=2,shape=rect, label=<Team 2
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - } - - } - - subgraph "cluster_group_Organisation 2" { - margin=25 - label=<
Organisation 2
> - labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" - - 4 [id=4,shape=rect, label=<Organisation 2
[Software System]>, style=filled, color="#9a9a9a", fillcolor="#dddddd", fontcolor="#000000"] - } - - -} \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph deleted file mode 100644 index 6a416a4d3..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/36141.ilograph +++ /dev/null @@ -1,616 +0,0 @@ -resources: - - id: "1" - name: "Personal Banking Customer" - subtitle: "[Person]" - description: "A customer of the bank, with personal bank accounts." - backgroundColor: "#08427b" - color: "#ffffff" - - - id: "2" - name: "Customer Service Staff" - subtitle: "[Person]" - description: "Customer service staff within the bank." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "3" - name: "Back Office Staff" - subtitle: "[Person]" - description: "Administration and support staff within the bank." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "4" - name: "Mainframe Banking System" - subtitle: "[Software System]" - description: "Stores all of the core banking information about customers, accounts, transactions, etc." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "5" - name: "E-mail System" - subtitle: "[Software System]" - description: "The internal Microsoft Exchange e-mail system." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "6" - name: "ATM" - subtitle: "[Software System]" - description: "Allows customers to withdraw cash." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "7" - name: "Internet Banking System" - subtitle: "[Software System]" - description: "Allows customers to view information about their bank accounts, and make payments." - backgroundColor: "#1168bd" - color: "#ffffff" - - children: - - id: "10" - name: "Web Application" - subtitle: "[Container: Java and Spring MVC]" - description: "Delivers the static content and the Internet banking single page application." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "11" - name: "API Application" - subtitle: "[Container: Java and Spring MVC]" - description: "Provides Internet banking functionality via a JSON/HTTPS API." - backgroundColor: "#438dd5" - color: "#ffffff" - - children: - - id: "12" - name: "Sign In Controller" - subtitle: "[Component: Spring MVC Rest Controller]" - description: "Allows users to sign in to the Internet Banking System." - backgroundColor: "#85bbf0" - color: "#000000" - - - id: "13" - name: "Accounts Summary Controller" - subtitle: "[Component: Spring MVC Rest Controller]" - description: "Provides customers with a summary of their bank accounts." - backgroundColor: "#85bbf0" - color: "#000000" - - - id: "14" - name: "Reset Password Controller" - subtitle: "[Component: Spring MVC Rest Controller]" - description: "Allows users to reset their passwords with a single use URL." - backgroundColor: "#85bbf0" - color: "#000000" - - - id: "15" - name: "Security Component" - subtitle: "[Component: Spring Bean]" - description: "Provides functionality related to signing in, changing passwords, etc." - backgroundColor: "#85bbf0" - color: "#000000" - - - id: "16" - name: "Mainframe Banking System Facade" - subtitle: "[Component: Spring Bean]" - description: "A facade onto the mainframe banking system." - backgroundColor: "#85bbf0" - color: "#000000" - - - id: "17" - name: "E-mail Component" - subtitle: "[Component: Spring Bean]" - description: "Sends e-mails to users." - backgroundColor: "#85bbf0" - color: "#000000" - - - id: "18" - name: "Database" - subtitle: "[Container: Oracle Database Schema]" - description: "Stores user registration information, hashed authentication credentials, access logs, etc." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "8" - name: "Single-Page Application" - subtitle: "[Container: JavaScript and Angular]" - description: "Provides all of the Internet banking functionality to customers via their web browser." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "9" - name: "Mobile App" - subtitle: "[Container: Xamarin]" - description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "50" - name: "Developer Laptop" - subtitle: "[Deployment Node: Microsoft Windows 10 or Apple macOS]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "51" - name: "Web Browser" - subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "52" - name: "Single-Page Application" - subtitle: "[Container: JavaScript and Angular]" - description: "Provides all of the Internet banking functionality to customers via their web browser." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "53" - name: "Docker Container - Web Server" - subtitle: "[Deployment Node: Docker]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "54" - name: "Apache Tomcat" - subtitle: "[Deployment Node: Apache Tomcat 8.x]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "55" - name: "Web Application" - subtitle: "[Container: Java and Spring MVC]" - description: "Delivers the static content and the Internet banking single page application." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "57" - name: "API Application" - subtitle: "[Container: Java and Spring MVC]" - description: "Provides Internet banking functionality via a JSON/HTTPS API." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "59" - name: "Docker Container - Database Server" - subtitle: "[Deployment Node: Docker]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "60" - name: "Database Server" - subtitle: "[Deployment Node: Oracle 12c]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "61" - name: "Database" - subtitle: "[Container: Oracle Database Schema]" - description: "Stores user registration information, hashed authentication credentials, access logs, etc." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "63" - name: "Big Bank plc" - subtitle: "[Deployment Node: Big Bank plc data center]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "64" - name: "bigbank-dev001" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "65" - name: "Mainframe Banking System" - subtitle: "[Software System]" - description: "Stores all of the core banking information about customers, accounts, transactions, etc." - backgroundColor: "#999999" - color: "#ffffff" - - - id: "67" - name: "Customer's mobile device" - subtitle: "[Deployment Node: Apple iOS or Android]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "68" - name: "Mobile App" - subtitle: "[Container: Xamarin]" - description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "69" - name: "Customer's computer" - subtitle: "[Deployment Node: Microsoft Windows or Apple macOS]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "70" - name: "Web Browser" - subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "71" - name: "Single-Page Application" - subtitle: "[Container: JavaScript and Angular]" - description: "Provides all of the Internet banking functionality to customers via their web browser." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "72" - name: "Big Bank plc" - subtitle: "[Deployment Node: Big Bank plc data center]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "73" - name: "bigbank-web***" - subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "74" - name: "Apache Tomcat" - subtitle: "[Deployment Node: Apache Tomcat 8.x]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "75" - name: "Web Application" - subtitle: "[Container: Java and Spring MVC]" - description: "Delivers the static content and the Internet banking single page application." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "77" - name: "bigbank-api***" - subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "78" - name: "Apache Tomcat" - subtitle: "[Deployment Node: Apache Tomcat 8.x]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "79" - name: "API Application" - subtitle: "[Container: Java and Spring MVC]" - description: "Provides Internet banking functionality via a JSON/HTTPS API." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "82" - name: "bigbank-db01" - subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "83" - name: "Oracle - Primary" - subtitle: "[Deployment Node: Oracle 12c]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "84" - name: "Database" - subtitle: "[Container: Oracle Database Schema]" - description: "Stores user registration information, hashed authentication credentials, access logs, etc." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "86" - name: "bigbank-db02" - subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "87" - name: "Oracle - Secondary" - subtitle: "[Deployment Node: Oracle 12c]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "88" - name: "Database" - subtitle: "[Container: Oracle Database Schema]" - description: "Stores user registration information, hashed authentication credentials, access logs, etc." - backgroundColor: "#438dd5" - color: "#ffffff" - - - id: "90" - name: "bigbank-prod001" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "91" - name: "Mainframe Banking System" - subtitle: "[Software System]" - description: "Stores all of the core banking information about customers, accounts, transactions, etc." - backgroundColor: "#999999" - color: "#ffffff" - -perspectives: - - name: Static Structure - relations: - - from: "1" - to: "7" - label: "Views account balances, and makes payments using" - color: "#707070" - - - from: "1" - to: "2" - label: "Asks questions to" - description: "Telephone" - color: "#707070" - - - from: "1" - to: "6" - label: "Withdraws cash using" - color: "#707070" - - - from: "2" - to: "4" - label: "Uses" - color: "#707070" - - - from: "3" - to: "4" - label: "Uses" - color: "#707070" - - - from: "5" - to: "1" - label: "Sends e-mails to" - color: "#707070" - - - from: "6" - to: "4" - label: "Uses" - color: "#707070" - - - from: "7" - to: "4" - label: "Gets account information from, and makes payments using" - color: "#707070" - - - from: "7" - to: "5" - label: "Sends e-mail using" - color: "#707070" - - - from: "1" - to: "10" - label: "Visits bigbank.com/ib using" - description: "HTTPS" - color: "#707070" - - - from: "1" - to: "8" - label: "Views account balances, and makes payments using" - color: "#707070" - - - from: "1" - to: "9" - label: "Views account balances, and makes payments using" - color: "#707070" - - - from: "10" - to: "8" - label: "Delivers to the customer's web browser" - color: "#707070" - - - from: "11" - to: "18" - label: "Reads from and writes to" - description: "SQL/TCP" - color: "#707070" - - - from: "11" - to: "4" - label: "Makes API calls to" - description: "XML/HTTPS" - color: "#707070" - - - from: "11" - to: "5" - label: "Sends e-mail using" - color: "#707070" - - - from: "8" - to: "11" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - from: "9" - to: "11" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - from: "12" - to: "15" - label: "Uses" - color: "#707070" - - - from: "13" - to: "16" - label: "Uses" - color: "#707070" - - - from: "14" - to: "15" - label: "Uses" - color: "#707070" - - - from: "14" - to: "17" - label: "Uses" - color: "#707070" - - - from: "15" - to: "18" - label: "Reads from and writes to" - description: "SQL/TCP" - color: "#707070" - - - from: "16" - to: "4" - label: "Makes API calls to" - description: "XML/HTTPS" - color: "#707070" - - - from: "17" - to: "5" - label: "Sends e-mail using" - color: "#707070" - - - from: "8" - to: "12" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - from: "8" - to: "13" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - from: "8" - to: "14" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - from: "9" - to: "12" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - from: "9" - to: "13" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - from: "9" - to: "14" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - - name: Dynamic - API Application - Dynamic - sequence: - start: "8" - steps: - - to: "12" - label: "1. Submits credentials to" - description: "JSON/HTTPS" - color: "#707070" - - - to: "15" - label: "2. Validates credentials using" - color: "#707070" - - - to: "18" - label: "3. select * from users where username = ?" - description: "SQL/TCP" - color: "#707070" - - - to: "15" - label: "4. Returns user data to" - description: "SQL/TCP" - color: "#707070" - - - to: "12" - label: "5. Returns true if the hashed password matches" - color: "#707070" - - - to: "8" - label: "6. Sends back an authentication token to" - description: "JSON/HTTPS" - color: "#707070" - - - name: Deployment - Development - relations: - - from: "52" - to: "57" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - from: "55" - to: "52" - label: "Delivers to the customer's web browser" - color: "#707070" - - from: "57" - to: "61" - label: "Reads from and writes to" - description: "SQL/TCP" - color: "#707070" - - from: "57" - to: "65" - label: "Makes API calls to" - description: "XML/HTTPS" - color: "#707070" - - name: Deployment - Live - relations: - - from: "68" - to: "79" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - from: "71" - to: "79" - label: "Makes API calls to" - description: "JSON/HTTPS" - color: "#707070" - - from: "75" - to: "71" - label: "Delivers to the customer's web browser" - color: "#707070" - - from: "79" - to: "84" - label: "Reads from and writes to" - description: "SQL/TCP" - color: "#707070" - - from: "79" - to: "88" - label: "Reads from and writes to" - description: "SQL/TCP" - color: "#707070" - - from: "79" - to: "91" - label: "Makes API calls to" - description: "XML/HTTPS" - color: "#707070" \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph b/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph deleted file mode 100644 index 5b37222b4..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/54915.ilograph +++ /dev/null @@ -1,127 +0,0 @@ -resources: - - id: "1" - name: "Spring PetClinic" - subtitle: "[Software System]" - description: "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." - backgroundColor: "#ffffff" - color: "#000000" - - children: - - id: "2" - name: "Web Application" - subtitle: "[Container: Java and Spring Boot]" - description: "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." - backgroundColor: "#ffffff" - color: "#000000" - - - id: "3" - name: "Database" - subtitle: "[Container: Relational database schema]" - description: "Stores information regarding the veterinarians, the clients, and their pets." - backgroundColor: "#ffffff" - color: "#000000" - - - id: "5" - name: "Amazon Web Services" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#232f3e" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png" - - children: - - id: "6" - name: "US-East-1" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#147eba" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png" - - children: - - id: "12" - name: "Amazon RDS" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#3b48cc" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png" - - children: - - id: "13" - name: "MySQL" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#3b48cc" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png" - - children: - - id: "14" - name: "Database" - subtitle: "[Container: Relational database schema]" - description: "Stores information regarding the veterinarians, the clients, and their pets." - backgroundColor: "#ffffff" - color: "#000000" - - - id: "9" - name: "Autoscaling group" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#cc2264" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png" - - children: - - id: "10" - name: "Amazon EC2" - subtitle: "[Deployment Node]" - backgroundColor: "#ffffff" - color: "#d86613" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png" - - children: - - id: "11" - name: "Web Application" - subtitle: "[Container: Java and Spring Boot]" - description: "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets." - backgroundColor: "#ffffff" - color: "#000000" - - - id: "7" - name: "Route 53" - subtitle: "[Infrastructure Node]" - description: "Highly available and scalable cloud DNS service." - backgroundColor: "#ffffff" - color: "#693cc5" - icon: "AWS/Networking/Route-53.svg" - - - id: "8" - name: "Elastic Load Balancer" - subtitle: "[Infrastructure Node]" - description: "Automatically distributes incoming application traffic." - backgroundColor: "#ffffff" - color: "#693cc5" - icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png" - -perspectives: - - name: Static Structure - relations: - - from: "2" - to: "3" - label: "Reads from and writes to" - description: "MySQL Protocol/SSL" - color: "#707070" - - - name: Deployment - Live - relations: - - from: "11" - to: "14" - label: "Reads from and writes to" - description: "MySQL Protocol/SSL" - color: "#707070" - - from: "7" - to: "8" - label: "Forwards requests to" - description: "HTTPS" - color: "#707070" - - from: "8" - to: "11" - label: "Forwards requests to" - description: "HTTPS" - color: "#707070" \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java index 4761312d2..596b7592c 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -18,26 +18,762 @@ public class IlographExporterTests extends AbstractExporterTests { @Test public void test_BigBankPlcExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/big-bank-plc.json")); IlographExporter ilographExporter = new IlographExporter(); WorkspaceExport export = ilographExporter.export(workspace); - String expected = readFile(new File("./src/test/java/com/structurizr/export/ilograph/36141.ilograph")); - assertEquals(expected, export.getDefinition()); + assertEquals(""" +resources: + - id: "1" + name: "Personal Banking Customer" + subtitle: "[Person]" + description: "A customer of the bank, with personal bank accounts." + backgroundColor: "#08427b" + color: "#ffffff" + + - id: "2" + name: "Customer Service Staff" + subtitle: "[Person]" + description: "Customer service staff within the bank." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "3" + name: "Back Office Staff" + subtitle: "[Person]" + description: "Administration and support staff within the bank." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "4" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "5" + name: "E-mail System" + subtitle: "[Software System]" + description: "The internal Microsoft Exchange e-mail system." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "6" + name: "ATM" + subtitle: "[Software System]" + description: "Allows customers to withdraw cash." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "7" + name: "Internet Banking System" + subtitle: "[Software System]" + description: "Allows customers to view information about their bank accounts, and make payments." + backgroundColor: "#1168bd" + color: "#ffffff" + + children: + - id: "10" + name: "Web Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Delivers the static content and the Internet banking single page application." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "11" + name: "API Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Provides Internet banking functionality via a JSON/HTTPS API." + backgroundColor: "#438dd5" + color: "#ffffff" + + children: + - id: "12" + name: "Sign In Controller" + subtitle: "[Component: Spring MVC Rest Controller]" + description: "Allows users to sign in to the Internet Banking System." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "13" + name: "Accounts Summary Controller" + subtitle: "[Component: Spring MVC Rest Controller]" + description: "Provides customers with a summary of their bank accounts." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "14" + name: "Reset Password Controller" + subtitle: "[Component: Spring MVC Rest Controller]" + description: "Allows users to reset their passwords with a single use URL." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "15" + name: "Security Component" + subtitle: "[Component: Spring Bean]" + description: "Provides functionality related to signing in, changing passwords, etc." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "16" + name: "Mainframe Banking System Facade" + subtitle: "[Component: Spring Bean]" + description: "A facade onto the mainframe banking system." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "17" + name: "E-mail Component" + subtitle: "[Component: Spring Bean]" + description: "Sends e-mails to users." + backgroundColor: "#85bbf0" + color: "#000000" + + - id: "18" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "8" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "9" + name: "Mobile App" + subtitle: "[Container: Xamarin]" + description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "50" + name: "Developer Laptop" + subtitle: "[Deployment Node: Microsoft Windows 10 or Apple macOS]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "51" + name: "Web Browser" + subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "52" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "53" + name: "Docker Container - Web Server" + subtitle: "[Deployment Node: Docker]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "54" + name: "Apache Tomcat" + subtitle: "[Deployment Node: Apache Tomcat 8.x]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "55" + name: "Web Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Delivers the static content and the Internet banking single page application." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "57" + name: "API Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Provides Internet banking functionality via a JSON/HTTPS API." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "59" + name: "Docker Container - Database Server" + subtitle: "[Deployment Node: Docker]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "60" + name: "Database Server" + subtitle: "[Deployment Node: Oracle 12c]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "61" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "63" + name: "Big Bank plc" + subtitle: "[Deployment Node: Big Bank plc data center]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "64" + name: "bigbank-dev001" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "65" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + + - id: "67" + name: "Customer's mobile device" + subtitle: "[Deployment Node: Apple iOS or Android]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "68" + name: "Mobile App" + subtitle: "[Container: Xamarin]" + description: "Provides a limited subset of the Internet banking functionality to customers via their mobile device." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "69" + name: "Customer's computer" + subtitle: "[Deployment Node: Microsoft Windows or Apple macOS]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "70" + name: "Web Browser" + subtitle: "[Deployment Node: Chrome, Firefox, Safari, or Edge]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "71" + name: "Single-Page Application" + subtitle: "[Container: JavaScript and Angular]" + description: "Provides all of the Internet banking functionality to customers via their web browser." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "72" + name: "Big Bank plc" + subtitle: "[Deployment Node: Big Bank plc data center]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "73" + name: "bigbank-web***" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "74" + name: "Apache Tomcat" + subtitle: "[Deployment Node: Apache Tomcat 8.x]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "75" + name: "Web Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Delivers the static content and the Internet banking single page application." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "77" + name: "bigbank-api***" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "78" + name: "Apache Tomcat" + subtitle: "[Deployment Node: Apache Tomcat 8.x]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "79" + name: "API Application" + subtitle: "[Container: Java and Spring MVC]" + description: "Provides Internet banking functionality via a JSON/HTTPS API." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "82" + name: "bigbank-db01" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "83" + name: "Oracle - Primary" + subtitle: "[Deployment Node: Oracle 12c]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "84" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "86" + name: "bigbank-db02" + subtitle: "[Deployment Node: Ubuntu 16.04 LTS]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "87" + name: "Oracle - Secondary" + subtitle: "[Deployment Node: Oracle 12c]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "88" + name: "Database" + subtitle: "[Container: Oracle Database Schema]" + description: "Stores user registration information, hashed authentication credentials, access logs, etc." + backgroundColor: "#438dd5" + color: "#ffffff" + + - id: "90" + name: "bigbank-prod001" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "91" + name: "Mainframe Banking System" + subtitle: "[Software System]" + description: "Stores all of the core banking information about customers, accounts, transactions, etc." + backgroundColor: "#999999" + color: "#ffffff" + +perspectives: + - name: Static Structure + relations: + - from: "1" + to: "7" + label: "Views account balances, and makes payments using" + color: "#444444" + + - from: "1" + to: "2" + label: "Asks questions to" + description: "Telephone" + color: "#444444" + + - from: "1" + to: "6" + label: "Withdraws cash using" + color: "#444444" + + - from: "2" + to: "4" + label: "Uses" + color: "#444444" + + - from: "3" + to: "4" + label: "Uses" + color: "#444444" + + - from: "5" + to: "1" + label: "Sends e-mails to" + color: "#444444" + + - from: "6" + to: "4" + label: "Uses" + color: "#444444" + + - from: "7" + to: "4" + label: "Gets account information from, and makes payments using" + color: "#444444" + + - from: "7" + to: "5" + label: "Sends e-mail using" + color: "#444444" + + - from: "1" + to: "10" + label: "Visits bigbank.com/ib using" + description: "HTTPS" + color: "#444444" + + - from: "1" + to: "8" + label: "Views account balances, and makes payments using" + color: "#444444" + + - from: "1" + to: "9" + label: "Views account balances, and makes payments using" + color: "#444444" + + - from: "10" + to: "8" + label: "Delivers to the customer's web browser" + color: "#444444" + + - from: "11" + to: "18" + label: "Reads from and writes to" + description: "SQL/TCP" + color: "#444444" + + - from: "11" + to: "4" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#444444" + + - from: "11" + to: "5" + label: "Sends e-mail using" + color: "#444444" + + - from: "8" + to: "11" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - from: "9" + to: "11" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - from: "12" + to: "15" + label: "Uses" + color: "#444444" + + - from: "13" + to: "16" + label: "Uses" + color: "#444444" + + - from: "14" + to: "15" + label: "Uses" + color: "#444444" + + - from: "14" + to: "17" + label: "Uses" + color: "#444444" + + - from: "15" + to: "18" + label: "Reads from and writes to" + description: "SQL/TCP" + color: "#444444" + + - from: "16" + to: "4" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#444444" + + - from: "17" + to: "5" + label: "Sends e-mail using" + color: "#444444" + + - from: "8" + to: "12" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - from: "8" + to: "13" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - from: "8" + to: "14" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - from: "9" + to: "12" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - from: "9" + to: "13" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - from: "9" + to: "14" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + + - name: Dynamic - API Application - Dynamic + sequence: + start: "8" + steps: + - to: "12" + label: "1. Submits credentials to" + description: "JSON/HTTPS" + color: "#444444" + + - to: "15" + label: "2. Validates credentials using" + color: "#444444" + + - to: "18" + label: "3. select * from users where username = ?" + description: "SQL/TCP" + color: "#444444" + + - to: "15" + label: "4. Returns user data to" + description: "SQL/TCP" + color: "#444444" + + - to: "12" + label: "5. Returns true if the hashed password matches" + color: "#444444" + + - to: "8" + label: "6. Sends back an authentication token to" + description: "JSON/HTTPS" + color: "#444444" + + - name: Deployment - Development + relations: + - from: "52" + to: "57" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + - from: "55" + to: "52" + label: "Delivers to the customer's web browser" + color: "#444444" + - from: "57" + to: "61" + label: "Reads from and writes to" + description: "SQL/TCP" + color: "#444444" + - from: "57" + to: "65" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#444444" + - name: Deployment - Live + relations: + - from: "68" + to: "79" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + - from: "71" + to: "79" + label: "Makes API calls to" + description: "JSON/HTTPS" + color: "#444444" + - from: "75" + to: "71" + label: "Delivers to the customer's web browser" + color: "#444444" + - from: "79" + to: "84" + label: "Reads from and writes to" + description: "SQL/TCP" + color: "#444444" + - from: "79" + to: "88" + label: "Reads from and writes to" + description: "SQL/TCP" + color: "#444444" + - from: "79" + to: "91" + label: "Makes API calls to" + description: "XML/HTTPS" + color: "#444444\"""", export.getDefinition()); } @Test @Tag("IntegrationTest") void test_AmazonWebServicesExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); workspace.getViews().getConfiguration().getStyles().addElementStyle("Amazon Web Services - Route 53").addProperty(IlographExporter.ILOGRAPH_ICON, "AWS/Networking/Route-53.svg"); ThemeUtils.loadThemes(workspace); IlographExporter ilographExporter = new IlographExporter(); WorkspaceExport export = ilographExporter.export(workspace); - String expected = readFile(new File("./src/test/java/com/structurizr/export/ilograph/54915.ilograph")); - assertEquals(expected, export.getDefinition()); + assertEquals(""" + resources: + - id: "1" + name: "X" + subtitle: "[Software System]" + backgroundColor: "#ffffff" + color: "#444444" + + children: + - id: "2" + name: "Web Application" + subtitle: "[Container: Java and Spring Boot]" + backgroundColor: "#ffffff" + color: "#444444" + + - id: "3" + name: "Database Schema" + subtitle: "[Container]" + backgroundColor: "#ffffff" + color: "#444444" + + - id: "5" + name: "Amazon Web Services" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#232f3e" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png" + + children: + - id: "6" + name: "US-East-1" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#147eba" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png" + + children: + - id: "10" + name: "Autoscaling group" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#cc2264" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png" + + children: + - id: "11" + name: "Amazon EC2 - Ubuntu server" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#d86613" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png" + + children: + - id: "12" + name: "Web Application" + subtitle: "[Container: Java and Spring Boot]" + backgroundColor: "#ffffff" + color: "#444444" + + - id: "14" + name: "Amazon RDS" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#3b48cc" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png" + + children: + - id: "15" + name: "MySQL" + subtitle: "[Deployment Node]" + backgroundColor: "#ffffff" + color: "#3b48cc" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png" + + children: + - id: "16" + name: "Database Schema" + subtitle: "[Container]" + backgroundColor: "#ffffff" + color: "#444444" + + - id: "7" + name: "DNS router" + subtitle: "[Infrastructure Node: Route 53]" + description: "Routes incoming requests based upon domain name." + backgroundColor: "#ffffff" + color: "#693cc5" + icon: "AWS/Networking/Route-53.svg" + + - id: "8" + name: "Load Balancer" + subtitle: "[Infrastructure Node: Elastic Load Balancer]" + description: "Automatically distributes incoming application traffic." + backgroundColor: "#ffffff" + color: "#693cc5" + icon: "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png" + + perspectives: + - name: Static Structure + relations: + - from: "2" + to: "3" + label: "Reads from and writes to" + description: "MySQL Protocol/SSL" + color: "#444444" + + - name: Deployment - Live + relations: + - from: "12" + to: "16" + label: "Reads from and writes to" + description: "MySQL Protocol/SSL" + color: "#444444" + - from: "7" + to: "8" + label: "Forwards requests to" + description: "HTTPS" + color: "#444444" + - from: "8" + to: "12" + label: "Forwards requests to" + description: "HTTPS" + color: "#444444\"""", export.getDefinition()); } @Test @@ -50,27 +786,29 @@ void test_renderCustomElements() { a.uses(b, "Uses"); WorkspaceExport export = new IlographExporter().export(workspace); - assertEquals("resources:\n" + - " - id: \"1\"\n" + - " name: \"A\"\n" + - " subtitle: \"\"\n" + - " backgroundColor: \"#dddddd\"\n" + - " color: \"#000000\"\n" + - "\n" + - " - id: \"2\"\n" + - " name: \"B\"\n" + - " subtitle: \"[Custom]\"\n" + - " description: \"Description\"\n" + - " backgroundColor: \"#dddddd\"\n" + - " color: \"#000000\"\n" + - "\n" + - "perspectives:\n" + - " - name: Static Structure\n" + - " relations:\n" + - " - from: \"1\"\n" + - " to: \"2\"\n" + - " label: \"Uses\"\n" + - " color: \"#707070\"\n", export.getDefinition()); + assertEquals(""" + resources: + - id: "1" + name: "A" + subtitle: "" + backgroundColor: "#ffffff" + color: "#444444" + + - id: "2" + name: "B" + subtitle: "[Custom]" + description: "Description" + backgroundColor: "#ffffff" + color: "#444444" + + perspectives: + - name: Static Structure + relations: + - from: "1" + to: "2" + label: "Uses" + color: "#444444" + """, export.getDefinition()); } @Test diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd deleted file mode 100644 index 4af05234c..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd +++ /dev/null @@ -1,48 +0,0 @@ -graph LR - linkStyle default fill:#ffffff - - subgraph diagram ["Spring PetClinic - Deployment - Live"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph 5 ["Amazon Web Services"] - style 5 fill:#ffffff,stroke:#232f3e,color:#232f3e - - subgraph 6 ["US-East-1"] - style 6 fill:#ffffff,stroke:#147eba,color:#147eba - - subgraph 12 ["Amazon RDS"] - style 12 fill:#ffffff,stroke:#3b48cc,color:#3b48cc - - subgraph 13 ["MySQL"] - style 13 fill:#ffffff,stroke:#3b48cc,color:#3b48cc - - 14[("

Database
[Container: Relational database schema]
Stores information regarding
the veterinarians, the
clients, and their pets.
")] - style 14 fill:#ffffff,stroke:#b2b2b2,color:#000000 - end - - end - - 7("
Route 53
[Infrastructure Node]
Highly available and scalable
cloud DNS service.
") - style 7 fill:#ffffff,stroke:#693cc5,color:#693cc5 - 8("
Elastic Load Balancer
[Infrastructure Node]
Automatically distributes
incoming application traffic.
") - style 8 fill:#ffffff,stroke:#693cc5,color:#693cc5 - subgraph 9 ["Autoscaling group"] - style 9 fill:#ffffff,stroke:#cc2264,color:#cc2264 - - subgraph 10 ["Amazon EC2"] - style 10 fill:#ffffff,stroke:#d86613,color:#d86613 - - 11("
Web Application
[Container: Java and Spring Boot]
Allows employees to view and
manage information regarding
the veterinarians, the
clients, and their pets.
") - style 11 fill:#ffffff,stroke:#b2b2b2,color:#000000 - end - - end - - end - - end - - 11-. "
Reads from and writes to
[MySQL Protocol/SSL]
" .->14 - 7-. "
Forwards requests to
[HTTPS]
" .->8 - 8-. "
Forwards requests to
[HTTPS]
" .->11 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java index ed49a4431..065b56220 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -6,6 +6,7 @@ import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -16,54 +17,10 @@ public class MermaidDiagramExporterTests extends AbstractExporterTests { - @Test - public void test_BigBankPlcExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); - MermaidDiagramExporter exporter = new MermaidDiagramExporter(); - - Collection diagrams = exporter.export(workspace); - assertEquals(7, diagrams.size()); - - Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd")); - assertEquals(expected, diagram.getDefinition()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd")); - assertEquals(expected, diagram.getDefinition()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd")); - assertEquals(expected, diagram.getDefinition()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-Components.mmd")); - assertEquals(expected, diagram.getDefinition()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd")); - assertEquals(expected, diagram.getDefinition()); - - diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd")); - assertEquals(expected, diagram.getDefinition()); - - diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd")); - assertEquals(expected, diagram.getDefinition()); - - // and the sequence diagram version - workspace.getViews().getConfiguration().addProperty(exporter.MERMAID_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/mermaid/36141-SignIn-sequence.mmd")); - assertEquals(expected, diagram.getDefinition()); - } - @Test @Tag("IntegrationTest") public void test_AmazonWebServicesExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); ThemeUtils.loadThemes(workspace); workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); @@ -72,8 +29,56 @@ public void test_AmazonWebServicesExample() throws Exception { assertEquals(1, diagrams.size()); Diagram diagram = diagrams.stream().findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/54915-AmazonWebServicesDeployment.mmd")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + graph LR + linkStyle default fill:#ffffff + + subgraph diagram ["X - Deployment - Live"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 5 ["Amazon Web Services"] + style 5 fill:#ffffff,stroke:#232f3e,color:#232f3e + + subgraph 6 ["US-East-1"] + style 6 fill:#ffffff,stroke:#147eba,color:#147eba + + subgraph 10 ["Autoscaling group"] + style 10 fill:#ffffff,stroke:#cc2264,color:#cc2264 + + subgraph 11 ["Amazon EC2 - Ubuntu server"] + style 11 fill:#ffffff,stroke:#d86613,color:#d86613 + + 12("
Web Application
[Container: Java and Spring Boot]
") + style 12 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + subgraph 14 ["Amazon RDS"] + style 14 fill:#ffffff,stroke:#3b48cc,color:#3b48cc + + subgraph 15 ["MySQL"] + style 15 fill:#ffffff,stroke:#3b48cc,color:#3b48cc + + 16[("
Database Schema
[Container]
")] + style 16 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + 7["
DNS router
[Infrastructure Node: Route 53]
Routes incoming requests
based upon domain name.
"] + style 7 fill:#ffffff,stroke:#693cc5,color:#693cc5 + 8["
Load Balancer
[Infrastructure Node: Elastic Load Balancer]
Automatically distributes
incoming application traffic.
"] + style 8 fill:#ffffff,stroke:#693cc5,color:#693cc5 + end + + end + + 8-. "
Forwards requests to
[HTTPS]
" .->12 + 12-. "
Reads from and writes to
[MySQL Protocol/SSL]
" .->16 + 7-. "
Forwards requests to
[HTTPS]
" .->8 + + end""", diagram.getDefinition()); } @Test @@ -86,16 +91,102 @@ public void test_GroupsExample() throws Exception { assertEquals(3, diagrams.size()); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 2["
B
[Software System]
"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph group2 ["Group 2"] + style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 3["
C
[Software System]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + subgraph group3 ["Group 3"] + style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 4["
D
[Software System]
"] + style 4 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + 1["
A
[Software System]
"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + + 2-. "
" .->3 + 3-. "
" .->4 + 1-. "
" .->2 + + end""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["D - Containers"] + style diagram fill:#ffffff,stroke:#ffffff + + 3["
C
[Software System]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + + subgraph 4 ["D"] + style 4 fill:#ffffff,stroke:#444444,color:#444444 + + subgraph group1 ["Group 4"] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 6["
F
[Container]
"] + style 6 fill:#ffffff,stroke:#444444,color:#444444 + end + + 5["
E
[Container]
"] + style 5 fill:#ffffff,stroke:#444444,color:#444444 + end + + 3-. "
" .->5 + 3-. "
" .->6 + + end""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/groups-Components.mmd")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["D - F - Components"] + style diagram fill:#ffffff,stroke:#ffffff + + 3["
C
[Software System]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + + subgraph 6 ["F"] + style 6 fill:#ffffff,stroke:#444444,color:#444444 + + subgraph group1 ["Group 5"] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 8["
H
[Component]
"] + style 8 fill:#ffffff,stroke:#444444,color:#444444 + end + + 7["
G
[Component]
"] + style 7 fill:#ffffff,stroke:#444444,color:#444444 + end + + 3-. "
" .->7 + 3-. "
" .->8 + + end""", diagram.getDefinition()); } @Test @@ -125,8 +216,50 @@ public void test_NestedGroupsExample() throws Exception { Collection diagrams = exporter.export(workspace); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/mermaid/nested-groups.mmd")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Organisation 1"] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 3["
Organisation 1
[Software System]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + subgraph group2 ["Department 1"] + style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 5["
Department 1
[Software System]
"] + style 5 fill:#ffffff,stroke:#444444,color:#444444 + subgraph group3 ["Team 1"] + style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 1["
Team 1
[Software System]
"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph group4 ["Team 2"] + style group4 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 2["
Team 2
[Software System]
"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + end + + subgraph group5 ["Organisation 2"] + style group5 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 4["
Organisation 2
[Software System]
"] + style 4 fill:#ffffff,stroke:#444444,color:#444444 + end + + + end""", diagram.getDefinition()); } @Test @@ -145,28 +278,29 @@ public void test_renderContainerDiagramWithExternalContainers() { Diagram diagram = new MermaidDiagramExporter().export(containerView); assertEquals(""" -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["Software System 1 - Containers"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph 1 ["Software System 1"] - style 1 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - - 2["
Container 1
[Container]
"] - style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph 3 ["Software System 2"] - style 3 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - - 4["
Container 2
[Container]
"] - style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - 2-. "
Uses
" .->4 - end""", diagram.getDefinition()); + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Software System 1 - Containers"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 1 ["Software System 1"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + + 2["
Container 1
[Container]
"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph 3 ["Software System 2"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + + 4["
Container 2
[Container]
"] + style 4 fill:#ffffff,stroke:#444444,color:#444444 + end + + 2-. "
Uses
" .->4 + + end""", diagram.getDefinition()); } @Test @@ -187,28 +321,29 @@ public void test_renderComponentDiagramWithExternalComponents() { Diagram diagram = new MermaidDiagramExporter().export(componentView); assertEquals(""" -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["Software System 1 - Container 1 - Components"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph 2 ["Container 1"] - style 2 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - - 3["
Component 1
[Component]
"] - style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph 5 ["Container 2"] - style 5 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - - 6["
Component 2
[Component]
"] - style 6 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - 3-. "
Uses
" .->6 - end""", diagram.getDefinition()); + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Software System 1 - Container 1 - Components"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 2 ["Container 1"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + + 3["
Component 1
[Component]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph 5 ["Container 2"] + style 5 fill:#ffffff,stroke:#444444,color:#444444 + + 6["
Component 2
[Component]
"] + style 6 fill:#ffffff,stroke:#444444,color:#444444 + end + + 3-. "
Uses
" .->6 + + end""", diagram.getDefinition()); } @Test @@ -227,69 +362,69 @@ public void test_renderGroupStyles() { MermaidDiagramExporter exporter = new MermaidDiagramExporter(); Diagram diagram = exporter.export(view); assertEquals(""" -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["System Landscape"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph group1 ["Group 1"] - style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 - - 1["
User 1
[Person]
"] - style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph group2 ["Group 2"] - style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5 - - 2["
User 2
[Person]
"] - style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph group3 ["Group 3"] - style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 3["
User 3
[Person]
"] - style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - - end""", diagram.getDefinition()); + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 + + 1["
User 1
[Person]
"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph group2 ["Group 2"] + style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5 + + 2["
User 2
[Person]
"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph group3 ["Group 3"] + style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 3["
User 3
[Person]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + end + + + end""", diagram.getDefinition()); workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); diagram = exporter.export(view); assertEquals(""" -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["System Landscape"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph group1 ["Group 1"] - style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 - - 1["
User 1
[Person]
"] - style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph group2 ["Group 2"] - style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5 - - 2["
User 2
[Person]
"] - style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph group3 ["Group 3"] - style group3 fill:#ffffff,stroke:#aabbcc,color:#aabbcc,stroke-dasharray:5 - - 3["
User 3
[Person]
"] - style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - - end""", diagram.getDefinition()); + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 + + 1["
User 1
[Person]
"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph group2 ["Group 2"] + style group2 fill:#ffffff,stroke:#222222,color:#222222,stroke-dasharray:5 + + 2["
User 2
[Person]
"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph group3 ["Group 3"] + style group3 fill:#ffffff,stroke:#aabbcc,color:#aabbcc,stroke-dasharray:5 + + 3["
User 3
[Person]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + end + + + end""", diagram.getDefinition()); } @Test @@ -305,19 +440,21 @@ public void test_renderCustomView() { view.addDefaultElements(); Diagram diagram = new MermaidDiagramExporter().export(view); - assertEquals("graph TB\n" + - " linkStyle default fill:#ffffff\n" + - "\n" + - " subgraph diagram [\"Title\"]\n" + - " style diagram fill:#ffffff,stroke:#ffffff\n" + - "\n" + - " 1[\"
A
\"]\n" + - " style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - " 2[\"
B
[Custom]
Description
\"]\n" + - " style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000\n" + - "\n" + - " 1-. \"
Uses
\" .->2\n" + - " end", diagram.getDefinition()); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Title"] + style diagram fill:#ffffff,stroke:#ffffff + + 1["
A
"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + 2["
B
[Custom]
Description
"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + + 1-. "
Uses
" .->2 + + end""", diagram.getDefinition()); } } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd deleted file mode 100644 index 62d5a7742..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Components.mmd +++ /dev/null @@ -1,26 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["D - F - Components"] - style diagram fill:#ffffff,stroke:#ffffff - - 3["
C
[Software System]
"] - style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - - subgraph 6 ["F"] - style 6 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - - subgraph group1 ["Group 5"] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 8["
H
[Component]
"] - style 8 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - 7["
G
[Component]
"] - style 7 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - 3-. "
" .->7 - 3-. "
" .->8 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd deleted file mode 100644 index 768970480..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-Containers.mmd +++ /dev/null @@ -1,26 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["D - Containers"] - style diagram fill:#ffffff,stroke:#ffffff - - 3["
C
[Software System]
"] - style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - - subgraph 4 ["D"] - style 4 fill:#ffffff,stroke:#9a9a9a,color:#9a9a9a - - subgraph group1 ["Group 4"] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 6["
F
[Container]
"] - style 6 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - 5["
E
[Container]
"] - style 5 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - 3-. "
" .->5 - 3-. "
" .->6 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd deleted file mode 100644 index 65efa9926..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/groups-SystemLandscape.mmd +++ /dev/null @@ -1,34 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["System Landscape"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph group1 ["Group 1"] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 2["
B
[Software System]
"] - style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph group2 ["Group 2"] - style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 3["
C
[Software System]
"] - style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph group3 ["Group 3"] - style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 4["
D
[Software System]
"] - style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - end - - 1["
A
[Software System]
"] - style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 - - 2-. "
" .->3 - 3-. "
" .->4 - 1-. "
" .->2 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd deleted file mode 100644 index 74a0cfe4d..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/nested-groups.mmd +++ /dev/null @@ -1,43 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["System Landscape"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph group1 ["Organisation 1"] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 3["
Organisation 1
[Software System]
"] - style 3 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph group2 ["Department 1"] - style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 5["
Department 1
[Software System]
"] - style 5 fill:#dddddd,stroke:#9a9a9a,color:#000000 - subgraph group3 ["Team 1"] - style group3 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 1["
Team 1
[Software System]
"] - style 1 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - subgraph group4 ["Team 2"] - style group4 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 2["
Team 2
[Software System]
"] - style 2 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - end - - end - - subgraph group5 ["Organisation 2"] - style group5 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 4["
Organisation 2
[Software System]
"] - style 4 fill:#dddddd,stroke:#9a9a9a,color:#000000 - end - - - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index b544dbdcf..02cf8273a 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -6,6 +6,7 @@ import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -18,7 +19,7 @@ public class C4PlantUMLDiagramExporterTests extends AbstractExporterTests { @Test public void test_BigBankPlcExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/big-bank-plc.json")); workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "true"); C4PlantUMLExporter exporter = new C4PlantUMLExporter(); @@ -26,45 +27,465 @@ public void test_BigBankPlcExample() throws Exception { assertEquals(7, diagrams.size()); Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape + + set separator none + top to bottom direction + + + + !include + !include + + AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { + Person(CustomerServiceStaff, "Customer Service Staff", $descr="Customer service staff within the bank.", $tags="Person,Bank Staff", $link="") + Person(BackOfficeStaff, "Back Office Staff", $descr="Administration and support staff within the bank.", $tags="Person,Bank Staff", $link="") + System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + System(ATM, "ATM", $descr="Allows customers to withdraw cash.", $tags="Software System,Existing System", $link="") + System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") + } + + Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") + + Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") + Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") + Rel(PersonalBankingCustomer, CustomerServiceStaff, "Asks questions to", $techn="Telephone", $tags="Relationship", $link="") + Rel(CustomerServiceStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") + Rel(PersonalBankingCustomer, ATM, "Withdraws cash using", $techn="", $tags="Relationship", $link="") + Rel(ATM, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") + Rel(BackOfficeStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Internet Banking System - System Context\\nThe system context diagram for the Internet Banking System. + + set separator none + top to bottom direction + + + + !include + !include + + AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { + System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") + } + + Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") + + Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") + Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Internet Banking System - Containers\\nThe container diagram for the Internet Banking System. + + set separator none + top to bottom direction + + + + !include + !include + !include + + AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + + AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="solid") + Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") + System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + + System_Boundary("InternetBankingSystem_boundary", "Internet Banking System", $tags="Software System") { + Container(InternetBankingSystem.WebApplication, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") + Container(InternetBankingSystem.APIApplication, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") + ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") + } + + Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") + Rel(PersonalBankingCustomer, InternetBankingSystem.WebApplication, "Visits bigbank.com/ib using", $techn="HTTPS", $tags="Relationship", $link="") + Rel(PersonalBankingCustomer, InternetBankingSystem.SinglePageApplication, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") + Rel(PersonalBankingCustomer, InternetBankingSystem.MobileApp, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.WebApplication, InternetBankingSystem.SinglePageApplication, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication, InternetBankingSystem.Database, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Internet Banking System - API Application - Components\\nThe component diagram for the API Application. + + set separator none + top to bottom direction + + + + !include + !include + !include + !include + + AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + + AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") + System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") + ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + + Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { + Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.AccountsSummaryController, "Accounts Summary Controller", $techn="Spring MVC Rest Controller", $descr="Provides customers with a summary of their bank accounts.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.ResetPasswordController, "Reset Password Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to reset their passwords with a single use URL.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, "Mainframe Banking System Facade", $techn="Spring Bean", $descr="A facade onto the mainframe banking system.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.EmailComponent, "E-mail Component", $techn="Spring Bean", $descr="Sends e-mails to users.", $tags="Component", $link="") + } + + Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "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.SinglePageApplication, InternetBankingSystem.APIApplication.ResetPasswordController, "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.AccountsSummaryController, "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.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="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.EmailComponent, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); 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.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title API Application - Dynamic\\nSummarises how the sign in feature works in the single-page application. + + set separator none + top to bottom direction + + + + !include + !include + !include + !include + + AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + + AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") + Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { + Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") + } + + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + + Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1. Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2. Validates credentials using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3. select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4. Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "5. Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "6. Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Internet Banking System - Deployment - Development\\nAn example development deployment scenario for the Internet Banking System. + + set separator none + top to bottom direction + + + + !include + !include + !include + !include + + AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Element", $bgColor="#ffffff", $borderColor="#444444", $fontColor="#444444", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + Deployment_Node(Development.DeveloperLaptop, "Developer Laptop", $type="Microsoft Windows 10 or Apple macOS", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { + Container(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + } + + Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer, "Docker Container - Web Server", $type="Docker", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { + Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") + Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") + } + + } + + Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer, "Docker Container - Database Server", $type="Docker", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer, "Database Server", $type="Oracle 12c", $descr="", $tags="Element", $link="") { + ContainerDb(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + } + + } + + } + + Deployment_Node(Development.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { + Deployment_Node(Development.BigBankplc.bigbankdev001, "bigbank-dev001", $type="", $descr="", $tags="Element", $link="") { + System(Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + } + + } + + Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") + Rel(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); 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()); + assertEquals(""" + @startuml + title Internet Banking System - Deployment - Live\\nAn example live deployment scenario for the Internet Banking System. + + set separator none + top to bottom direction + + + + !include + !include + !include + !include + + AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#444444", $fontColor="#444444", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Element", $bgColor="#ffffff", $borderColor="#444444", $fontColor="#444444", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + Deployment_Node(Live.Customersmobiledevice, "Customer's mobile device", $type="Apple iOS or Android", $descr="", $tags="Element", $link="") { + Container(Live.Customersmobiledevice.MobileApp_1, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") + } + + Deployment_Node(Live.Customerscomputer, "Customer's computer", $type="Microsoft Windows or Apple macOS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.Customerscomputer.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { + Container(Live.Customerscomputer.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + } + + } + + Deployment_Node(Live.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankweb, "bigbank-web*** (x4)", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankweb.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { + Container(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") + } + + } + + Deployment_Node(Live.BigBankplc.bigbankapi, "bigbank-api*** (x8)", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankapi.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { + Container(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") + } + + } + + Deployment_Node(Live.BigBankplc.bigbankdb01, "bigbank-db01", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { + Deployment_Node(Live.BigBankplc.bigbankdb01.OraclePrimary, "Oracle - Primary", $type="Oracle 12c", $descr="", $tags="Element", $link="") { + ContainerDb(Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + } + + } + + Deployment_Node(Live.BigBankplc.bigbankdb02, "bigbank-db02", $type="Ubuntu 16.04 LTS", $descr="", $tags="Failover", $link="") { + Deployment_Node(Live.BigBankplc.bigbankdb02.OracleSecondary, "Oracle - Secondary", $type="Oracle 12c", $descr="", $tags="Failover", $link="") { + ContainerDb(Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + } + + } + + Deployment_Node(Live.BigBankplc.bigbankprod001, "bigbank-prod001", $type="", $descr="", $tags="Element", $link="") { + System(Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") + } + + } + + Rel(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, Live.Customerscomputer.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") + Rel(Live.Customersmobiledevice.MobileApp_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(Live.Customerscomputer.WebBrowser.SinglePageApplication_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") + Rel(Live.BigBankplc.bigbankdb01.OraclePrimary, Live.BigBankplc.bigbankdb02.OracleSecondary, "Replicates data to", $techn="", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); // and the sequence diagram version workspace.getViews().getConfiguration().addProperty(exporter.PLANTUML_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()); + assertEquals(""" + @startuml + title API Application - Dynamic\\nSummarises how the sign in feature works in the single-page application. + + set separator none + + + + !include + + AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") + ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + + Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "Validates credentials using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @Tag("IntegrationTest") public void test_AmazonWebServicesExampleWithoutTags() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); ThemeUtils.loadThemes(workspace); workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); workspace.getViews().getViews().forEach(v -> v.addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "false")); @@ -74,14 +495,60 @@ public void test_AmazonWebServicesExampleWithoutTags() throws Exception { assertEquals(1, diagrams.size()); Diagram diagram = diagrams.stream().findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title X - Deployment - Live + + set separator none + left to right direction + + + + !include + !include + !include + !include + + Deployment_Node(Live.AmazonWebServices, "Amazon Web Services", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1, "US-East-1", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup, "Autoscaling group", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver, "Amazon EC2 - Ubuntu server", $type="", $descr="", $tags="", $link="") { + Container(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1, "Web Application", $techn="Java and Spring Boot", $descr="", $tags="", $link="") + } + + } + + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS, "Amazon RDS", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL, "MySQL", $type="", $descr="", $tags="", $link="") { + ContainerDb(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1, "Database Schema", $techn="", $descr="", $tags="", $link="") + } + + } + + Deployment_Node(Live.AmazonWebServices.USEast1.DNSrouter, "DNS router", $type="Route 53", $descr="Routes incoming requests based upon domain name.", $tags="", $link="") + Deployment_Node(Live.AmazonWebServices.USEast1.LoadBalancer, "Load Balancer", $type="Elastic Load Balancer", $descr="Automatically distributes incoming application traffic.", $tags="", $link="") + } + + } + + Rel(Live.AmazonWebServices.USEast1.LoadBalancer, Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1, "Forwards requests to", $techn="HTTPS", $tags="", $link="") + Rel(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1, Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1, "Reads from and writes to", $techn="MySQL Protocol/SSL", $tags="", $link="") + Rel(Live.AmazonWebServices.USEast1.DNSrouter, Live.AmazonWebServices.USEast1.LoadBalancer, "Forwards requests to", $techn="HTTPS", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @Tag("IntegrationTest") public void test_AmazonWebServicesExampleWithTags() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); ThemeUtils.loadThemes(workspace); workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "true"); @@ -91,30 +558,193 @@ public void test_AmazonWebServicesExampleWithTags() throws Exception { assertEquals(1, diagrams.size()); Diagram diagram = diagrams.stream().findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title X - Deployment - Live + + set separator none + left to right direction + + + + !include + !include + !include + !include + + AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png{scale=0.1}", $shadowing="", $borderStyle="solid") + AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png{scale=0.1}", $shadowing="", $borderStyle="solid") + AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-route-53.png{scale=0.1}", $shadowing="", $borderStyle="solid") + AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png{scale=0.1}", $shadowing="", $borderStyle="solid") + AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") + AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png{scale=0.1}", $shadowing="", $borderStyle="solid") + AddElementTag("Application", $bgColor="#ffffff", $borderColor="#444444", $fontColor="#444444", $sprite="", $shadowing="", $borderStyle="solid") + AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png{scale=0.15}", $shadowing="", $borderStyle="solid") + AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") + AddElementTag("Database", $bgColor="#ffffff", $borderColor="#444444", $fontColor="#444444", $sprite="", $shadowing="", $borderStyle="solid") + + AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) + + Deployment_Node(Live.AmazonWebServices, "Amazon Web Services", $type="", $descr="", $tags="Amazon Web Services - Cloud", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1, "US-East-1", $type="", $descr="", $tags="Amazon Web Services - Region", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup, "Autoscaling group", $type="", $descr="", $tags="Amazon Web Services - Auto Scaling", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver, "Amazon EC2 - Ubuntu server", $type="", $descr="", $tags="Amazon Web Services - EC2", $link="") { + Container(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1, "Web Application", $techn="Java and Spring Boot", $descr="", $tags="Application", $link="") + } + + } + + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS, "Amazon RDS", $type="", $descr="", $tags="Amazon Web Services - RDS", $link="") { + Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL, "MySQL", $type="", $descr="", $tags="Amazon Web Services - RDS MySQL instance", $link="") { + ContainerDb(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1, "Database Schema", $techn="", $descr="", $tags="Database", $link="") + } + + } + + Deployment_Node(Live.AmazonWebServices.USEast1.DNSrouter, "DNS router", $type="Route 53", $descr="Routes incoming requests based upon domain name.", $tags="Amazon Web Services - Route 53", $link="") + Deployment_Node(Live.AmazonWebServices.USEast1.LoadBalancer, "Load Balancer", $type="Elastic Load Balancer", $descr="Automatically distributes incoming application traffic.", $tags="Amazon Web Services - Elastic Load Balancing", $link="") + } + + } + + Rel(Live.AmazonWebServices.USEast1.LoadBalancer, Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1, "Forwards requests to", $techn="HTTPS", $tags="Relationship", $link="") + Rel(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1, Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1, "Reads from and writes to", $techn="MySQL Protocol/SSL", $tags="Relationship", $link="") + Rel(Live.AmazonWebServices.USEast1.DNSrouter, Live.AmazonWebServices.USEast1.LoadBalancer, "Forwards requests to", $techn="HTTPS", $tags="Relationship", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test public void test_GroupsExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); - ThemeUtils.loadThemes(workspace); C4PlantUMLExporter exporter = new C4PlantUMLExporter(); Collection diagrams = exporter.export(workspace); assertEquals(3, diagrams.size()); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape + + set separator none + top to bottom direction + + + + !include + !include + + AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Group 1", $tags="Group 1") { + System(B, "B", $descr="", $tags="", $link="") + } + + AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_2, "Group 2", $tags="Group 2") { + System(C, "C", $descr="", $tags="", $link="") + AddBoundaryTag("Group 2/Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_3, "Group 3", $tags="Group 2/Group 3") { + System(D, "D", $descr="", $tags="", $link="") + } + + } + + System(A, "A", $descr="", $tags="", $link="") + + Rel(B, C, "", $techn="", $tags="", $link="") + Rel(C, D, "", $techn="", $tags="", $link="") + Rel(A, B, "", $techn="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title D - Containers + + set separator none + top to bottom direction + + + + !include + !include + !include + + System(C, "C", $descr="", $tags="", $link="") + + System_Boundary("D_boundary", "D", $tags="") { + AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Group 4", $tags="Group 4") { + Container(D.F, "F", $techn="", $descr="", $tags="", $link="") + } + + Container(D.E, "E", $techn="", $descr="", $tags="", $link="") + } + + Rel(C, D.E, "", $techn="", $tags="", $link="") + Rel(C, D.F, "", $techn="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title D - F - Components + + set separator none + top to bottom direction + + + + !include + !include + !include + + System(C, "C", $descr="", $tags="", $link="") + + Container_Boundary("D.F_boundary", "F", $tags="") { + AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Group 5", $tags="Group 5") { + Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") + } + + Component(D.F.G, "G", $techn="", $descr="", $tags="", $link="") + } + + Rel(C, D.F.G, "", $techn="", $tags="", $link="") + Rel(C, D.F.H, "", $techn="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -144,8 +774,52 @@ public void test_NestedGroupsExample() throws Exception { Collection diagrams = exporter.export(workspace); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Organisation 1", $tags="Organisation 1") { + System(Organisation1, "Organisation 1", $descr="", $tags="", $link="") + AddBoundaryTag("Organisation 1/Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_2, "Department 1", $tags="Organisation 1/Department 1") { + System(Department1, "Department 1", $descr="", $tags="", $link="") + AddBoundaryTag("Organisation 1/Department 1/Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_3, "Team 1", $tags="Organisation 1/Department 1/Team 1") { + System(Team1, "Team 1", $descr="", $tags="", $link="") + } + + AddBoundaryTag("Organisation 1/Department 1/Team 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_4, "Team 2", $tags="Organisation 1/Department 1/Team 2") { + System(Team2, "Team 2", $descr="", $tags="", $link="") + } + + } + + } + + AddBoundaryTag("Organisation 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_5, "Organisation 2", $tags="Organisation 2") { + System(Organisation2, "Organisation 2", $descr="", $tags="", $link="") + } + + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -163,8 +837,40 @@ public void test_NestedGroupsExample_WithDotAsGroupSeparator() throws Exception Collection diagrams = exporter.export(workspace); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Organisation 1", $tags="Organisation 1") { + AddBoundaryTag("Organisation 1.Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_2, "Department 1", $tags="Organisation 1.Department 1") { + AddBoundaryTag("Organisation 1.Department 1.Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_3, "Team 1", $tags="Organisation 1.Department 1.Team 1") { + System(Team1, "Team 1", $descr="", $tags="", $link="") + } + + } + + } + + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -182,20 +888,88 @@ public void test_renderGroupStyles() throws Exception { C4PlantUMLExporter exporter = new C4PlantUMLExporter() { @Override - protected double calculateIconScale(String iconUrl) { + protected double calculateIconScale(String iconUrl, int maxIconSize) { return 1.0; } }; Diagram diagram = exporter.export(view); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape + + set separator none + top to bottom direction + + + + !include + !include + + AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed") + Boundary(group_1, "Group 1", $tags="Group 1") { + Person(User1, "User 1", $descr="", $tags="", $link="") + } + + AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed") + Boundary(group_2, "Group 2", $tags="Group 2") { + Person(User2, "User 2", $descr="", $tags="", $link="") + } + + AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_3, "Group 3", $tags="Group 3") { + Person(User3, "User 3", $descr="", $tags="", $link="") + } + + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); diagram = exporter.export(view); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape + + set separator none + top to bottom direction + + + + !include + !include + + AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed") + Boundary(group_1, "Group 1", $tags="Group 1") { + Person(User1, "User 1", $descr="", $tags="", $link="") + } + + AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed") + Boundary(group_2, "Group 2", $tags="Group 2") { + Person(User2, "User 2", $descr="", $tags="", $link="") + } + + AddBoundaryTag("Group 3", $borderColor="#aabbcc", $fontColor="#aabbcc", $borderStyle="dashed") + Boundary(group_3, "Group 3", $tags="Group 3") { + Person(User3, "User 3", $descr="", $tags="", $link="") + } + + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -214,28 +988,37 @@ public void test_renderContainerDiagramWithExternalContainers() { containerView.add(container2); Diagram diagram = new C4PlantUMLExporter().export(containerView); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Software System 1 - Containers\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "!include \n" + - "\n" + - "System_Boundary(\"SoftwareSystem1_boundary\", \"Software System 1\", $tags=\"\") {\n" + - " Container(SoftwareSystem1.Container1, \"Container 1\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "System_Boundary(\"SoftwareSystem2_boundary\", \"Software System 2\", $tags=\"\") {\n" + - " Container(SoftwareSystem2.Container2, \"Container 2\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "Rel(SoftwareSystem1.Container1, SoftwareSystem2.Container2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System 1 - Containers + + set separator none + top to bottom direction + + + + !include + !include + !include + + System_Boundary("SoftwareSystem1_boundary", "Software System 1", $tags="") { + Container(SoftwareSystem1.Container1, "Container 1", $techn="", $descr="", $tags="", $link="") + } + + System_Boundary("SoftwareSystem2_boundary", "Software System 2", $tags="") { + Container(SoftwareSystem2.Container2, "Container 2", $techn="", $descr="", $tags="", $link="") + } + + Rel(SoftwareSystem1.Container1, SoftwareSystem2.Container2, "Uses", $techn="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -256,28 +1039,37 @@ public void test_renderComponentDiagramWithExternalComponents() { componentView.add(component2); Diagram diagram = new C4PlantUMLExporter().export(componentView); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Software System 1 - Container 1 - Components\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "!include \n" + - "\n" + - "Container_Boundary(\"SoftwareSystem1.Container1_boundary\", \"Container 1\", $tags=\"\") {\n" + - " Component(SoftwareSystem1.Container1.Component1, \"Component 1\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "Container_Boundary(\"SoftwareSystem2.Container2_boundary\", \"Container 2\", $tags=\"\") {\n" + - " Component(SoftwareSystem2.Container2.Component2, \"Component 2\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "Rel(SoftwareSystem1.Container1.Component1, SoftwareSystem2.Container2.Component2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System 1 - Container 1 - Components + + set separator none + top to bottom direction + + + + !include + !include + !include + + Container_Boundary("SoftwareSystem1.Container1_boundary", "Container 1", $tags="") { + Component(SoftwareSystem1.Container1.Component1, "Component 1", $techn="", $descr="", $tags="", $link="") + } + + Container_Boundary("SoftwareSystem2.Container2_boundary", "Container 2", $tags="") { + Component(SoftwareSystem2.Container2.Component2, "Component 2", $techn="", $descr="", $tags="", $link="") + } + + Rel(SoftwareSystem1.Container1.Component1, SoftwareSystem2.Container2.Component2, "Uses", $techn="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -291,20 +1083,28 @@ public void test_renderDiagramWithElementUrls() { view.addDefaultElements(); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "System(SoftwareSystem, \"Software System\", $descr=\"\", $tags=\"\", $link=\"https://structurizr.com\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + System(SoftwareSystem, "Software System", $descr="", $tags="", $link="https://structurizr.com") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -318,21 +1118,30 @@ public void test_renderDiagramWithIncludes() { view.addProperty(C4PlantUMLExporter.PLANTUML_INCLUDES_PROPERTY, "styles.puml"); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "!include styles.puml\n" + - "\n" + - "System(SoftwareSystem, \"Software System\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + !include styles.puml + + System(SoftwareSystem, "Software System", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -345,20 +1154,28 @@ public void test_renderDiagramWithNewLineCharacterInElementName() { view.addDefaultElements(); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "System(SoftwareSystem, \"Software\\nSystem\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + System(SoftwareSystem, "Software\\nSystem", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -371,23 +1188,31 @@ public void test_renderInfrastructureNodeWithTechnology() { view.addDefaultElements(); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Deployment - Default\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "!include \n" + - "\n" + - "Deployment_Node(Default.Deploymentnode, \"Deployment node\", $type=\"\", $descr=\"\", $tags=\"\", $link=\"\") {\n" + - " Deployment_Node(Default.Deploymentnode.Infrastructurenode, \"Infrastructure node\", $type=\"technology\", $descr=\"description\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Deployment - Default\\nview description + + set separator none + top to bottom direction + + + + !include + !include + !include + + Deployment_Node(Default.Deploymentnode, "Deployment node", $type="", $descr="", $tags="", $link="") { + Deployment_Node(Default.Deploymentnode.Infrastructurenode, "Infrastructure node", $type="technology", $descr="description", $tags="", $link="") + } + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -412,9 +1237,43 @@ public void test_printProperties() throws Exception { view.addDefaultElements(); Diagram diagram = new C4PlantUMLExporter().export(view); - - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title SoftwareSystem - Containers + + set separator none + top to bottom direction + + + + !include + !include + !include + + System_Boundary("SoftwareSystem_boundary", "SoftwareSystem", $tags="") { + WithoutPropertyHeader() + AddProperty("IP","127.0.0.1") + AddProperty("Region","East") + Container(SoftwareSystem.Container1, "Container 1", $techn="", $descr="", $tags="", $link="") + WithoutPropertyHeader() + AddProperty("IP","127.0.0.2") + AddProperty("Region","West") + Container(SoftwareSystem.Container2, "Container 2", $techn="", $descr="", $tags="", $link="") + } + + WithoutPropertyHeader() + AddProperty("Prop1","Value1") + AddProperty("Prop2","Value2") + Rel(SoftwareSystem.Container1, SoftwareSystem.Container2, "", $techn="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -432,9 +1291,35 @@ public void test_deploymentViewPrintProperties() throws Exception { deploymentView.addDefaultElements(); Diagram diagram = new C4PlantUMLExporter().export(deploymentView); - - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Deployment - Default + + set separator none + top to bottom direction + + + + !include + !include + !include + + WithoutPropertyHeader() + AddProperty("Prop1","Value1") + Deployment_Node(Default.Deploymentnode, "Deployment node", $type="", $descr="", $tags="", $link="") { + WithoutPropertyHeader() + AddProperty("Prop2","Value2") + Deployment_Node(Default.Deploymentnode.Infrastructurenode, "Infrastructure node", $type="technology", $descr="description", $tags="", $link="") + } + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -449,77 +1334,109 @@ public void test_legendAndStereotypes() { view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "true"); view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "false"); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + System(Name, "Name", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); // legend (true) and stereotypes (true) view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "true"); view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "true"); diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(false)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + System(Name, "Name", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + show stereotypes + @enduml""", diagram.getDefinition()); // legend (false) and stereotypes (false) view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "false"); view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "false"); diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "hide stereotypes\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + System(Name, "Name", $descr="", $tags="", $link="") + + SHOW_LEGEND(false) + hide stereotypes + @enduml""", diagram.getDefinition()); // legend (false) and stereotypes (true) view.addProperty(C4PlantUMLExporter.C4PLANTUML_LEGEND_PROPERTY, "false"); view.addProperty(C4PlantUMLExporter.C4PLANTUML_STEREOTYPES_PROPERTY, "true"); diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "System(Name, \"Name\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "show stereotypes\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + System(Name, "Name", $descr="", $tags="", $link="") + + SHOW_LEGEND(false) + show stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -546,26 +1463,34 @@ public void test_renderContainerShapes() throws Exception { workspace.getViews().getConfiguration().getStyles().addElementStyle("Robot").shape(Shape.Robot); Diagram diagram = new C4PlantUMLExporter().export(containerView); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Software System - Containers\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "!include \n" + - "\n" + - "System_Boundary(\"SoftwareSystem_boundary\", \"Software System\", $tags=\"\") {\n" + - " Container(SoftwareSystem.DefaultContainer, \"Default Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - " ContainerDb(SoftwareSystem.CylinderContainer, \"Cylinder Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - " ContainerQueue(SoftwareSystem.PipeContainer, \"Pipe Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - " Container(SoftwareSystem.RobotContainer, \"Robot Container\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System - Containers + + set separator none + top to bottom direction + + + + !include + !include + !include + + System_Boundary("SoftwareSystem_boundary", "Software System", $tags="") { + Container(SoftwareSystem.DefaultContainer, "Default Container", $techn="", $descr="", $tags="", $link="") + ContainerDb(SoftwareSystem.CylinderContainer, "Cylinder Container", $techn="", $descr="", $tags="", $link="") + ContainerQueue(SoftwareSystem.PipeContainer, "Pipe Container", $techn="", $descr="", $tags="", $link="") + Container(SoftwareSystem.RobotContainer, "Robot Container", $techn="", $descr="", $tags="", $link="") + } + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -595,26 +1520,34 @@ public void test_renderComponentShapes() throws Exception { workspace.getViews().getConfiguration().getStyles().addElementStyle("Robot").shape(Shape.Robot); Diagram diagram = new C4PlantUMLExporter().export(componentView); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Software System - Container - Components\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "!include \n" + - "\n" + - "Container_Boundary(\"SoftwareSystem.Container_boundary\", \"Container\", $tags=\"\") {\n" + - " Component(SoftwareSystem.Container.DefaultComponent, \"Default Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - " ComponentDb(SoftwareSystem.Container.CylinderComponent, \"Cylinder Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - " ComponentQueue(SoftwareSystem.Container.PipeComponent, \"Pipe Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - " Component(SoftwareSystem.Container.RobotComponent, \"Robot Component\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System - Container - Components + + set separator none + top to bottom direction + + + + !include + !include + !include + + Container_Boundary("SoftwareSystem.Container_boundary", "Container", $tags="") { + Component(SoftwareSystem.Container.DefaultComponent, "Default Component", $techn="", $descr="", $tags="", $link="") + ComponentDb(SoftwareSystem.Container.CylinderComponent, "Cylinder Component", $techn="", $descr="", $tags="", $link="") + ComponentQueue(SoftwareSystem.Container.PipeComponent, "Pipe Component", $techn="", $descr="", $tags="", $link="") + Component(SoftwareSystem.Container.RobotComponent, "Robot Component", $techn="", $descr="", $tags="", $link="") + } + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -626,23 +1559,29 @@ public void testFont() { workspace.getViews().getConfiguration().getBranding().setFont(new Font("Courier")); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "skinparam {\n" + - " defaultFontName \"Courier\"\n" + - "}\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "Person(User, \"User\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition().toString()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + Person(User, "User", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @@ -655,20 +1594,28 @@ public void stdlib_false() { view.addProperty(C4PlantUMLExporter.C4PLANTUML_STANDARD_LIBRARY_PROPERTY, "false"); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml\n" + - "!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml\n" + - "\n" + - "Person(User, \"User\", $descr=\"\", $tags=\"\", $link=\"\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition().toString()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml + !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml + + Person(User, "User", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition().toString()); } @@ -682,23 +1629,31 @@ public void componentWithoutTechnology() { view.addAllElements(); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Name - Name - Components\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "!include \n" + - "\n" + - "Container_Boundary(\"Name.Name_boundary\", \"Name\", $tags=\"\") {\n" + - " Component(Name.Name.Name, \"Name\", $techn=\"\", $descr=\"Description\", $tags=\"\", $link=\"\")\n" + - "}\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Name - Name - Components\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + !include + + Container_Boundary("Name.Name_boundary", "Name", $tags="") { + Component(Name.Name.Name, "Name", $techn="", $descr="Description", $tags="", $link="") + } + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -711,22 +1666,30 @@ public void borderStyling() { workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.ELEMENT).stroke("green").border(Border.Dashed).strokeWidth(2); Diagram diagram = new C4PlantUMLExporter().export(view); - assertEquals("@startuml\n" + - "set separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "!include \n" + - "!include \n" + - "\n" + - "AddElementTag(\"Element\", $bgColor=\"#dddddd\", $borderColor=\"#008000\", $fontColor=\"#000000\", $sprite=\"\", $shadowing=\"\", $borderStyle=\"dashed\", $borderThickness=\"2\")\n" + - "\n" + - "System(Name, \"Name\", $descr=\"\", $tags=\"Element\", $link=\"\")\n" + - "\n" + - "\n" + - "SHOW_LEGEND(true)\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + + + + !include + !include + + AddElementTag("Element", $bgColor="#ffffff", $borderColor="#008000", $fontColor="#444444", $sprite="", $shadowing="", $borderStyle="dashed", $borderThickness="2") + + System(Name, "Name", $descr="", $tags="Element", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); } @Test @@ -740,18 +1703,25 @@ public void elementWithUrl() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml + title System Landscape\\nDescription + set separator none - title System Landscape - top to bottom direction - + + + !include !include - + System(Name, "Name", $descr="", $tags="", $link="https://example.com") - - + SHOW_LEGEND(true) + hide stereotypes @enduml""", diagram.getDefinition()); } diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyleTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyleTests.java new file mode 100644 index 000000000..0abedaa56 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLBoundaryStyleTests.java @@ -0,0 +1,29 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.view.Border; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlantUMLBoundaryStyleTests { + + @Test + void test() { + PlantUMLBoundaryStyle style = new PlantUMLBoundaryStyle("Name", "#ffffff", "#444444", "#ff0000", 4, Border.Dotted, 24, false); + + assertEquals(""" + // Name + .Boundary-TmFtZQ== { + BackgroundColor: #ffffff; + LineColor: #ff0000; + LineStyle: 4-4; + LineThickness: 4; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + """, style.toString()); + } + +} diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLElementStyleTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLElementStyleTests.java new file mode 100644 index 000000000..dee150a42 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLElementStyleTests.java @@ -0,0 +1,32 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.view.Border; +import com.structurizr.view.Shape; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlantUMLElementStyleTests { + + @Test + void test() { + PlantUMLElementStyle style = new PlantUMLElementStyle("Name", Shape.RoundedBox, 450, "#ffffff", "#444444", "#ff0000", 4, Border.Dotted, 24, "icon", false); + + assertEquals(""" + // Name + .Element-TmFtZQ== { + BackgroundColor: #ffffff; + LineColor: #ff0000; + LineStyle: 4-4; + LineThickness: 4; + RoundCorner: 20; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + """, style.toString()); + } + +} diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLGroupStyleTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLGroupStyleTests.java new file mode 100644 index 000000000..dd146d0c2 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLGroupStyleTests.java @@ -0,0 +1,31 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.view.Border; +import com.structurizr.view.LineStyle; +import com.structurizr.view.RelationshipStyle; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlantUMLGroupStyleTests { + + @Test + void test() { + PlantUMLGroupStyle style = new PlantUMLGroupStyle("Name", "#ffffff", "#444444", "#ff0000", 4, Border.Dotted, 24, false); + + assertEquals(""" + // Name + .Group-TmFtZQ== { + BackgroundColor: #ffffff; + LineColor: #ff0000; + LineStyle: 4-4; + LineThickness: 4; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + """, style.toString()); + } + +} diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLLegendStyleTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLLegendStyleTests.java new file mode 100644 index 000000000..d56e3e786 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLLegendStyleTests.java @@ -0,0 +1,23 @@ +package com.structurizr.export.plantuml; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlantUMLLegendStyleTests { + + @Test + void test() { + PlantUMLLegendStyle style = new PlantUMLLegendStyle(); + + assertEquals(""" + // transparent element for relationships in legend + .Element-Transparent { + BackgroundColor: transparent; + LineColor: transparent; + FontColor: transparent; + } + """, style.toString()); + } + +} diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyleTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyleTests.java new file mode 100644 index 000000000..c73cee2c8 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRelationshipStyleTests.java @@ -0,0 +1,58 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.view.LineStyle; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlantUMLRelationshipStyleTests { + + @Test + void solid() { + PlantUMLRelationshipStyle style = new PlantUMLRelationshipStyle("Relationship", "#ff0000", LineStyle.Solid, 3, 24); + + assertEquals(""" + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 3; + LineStyle: 0; + LineColor: #ff0000; + FontColor: #ff0000; + FontSize: 24; + } + """, style.toString()); + } + + @Test + void dashed() { + PlantUMLRelationshipStyle style = new PlantUMLRelationshipStyle("Relationship", "#ff0000", LineStyle.Dashed, 3, 24); + + assertEquals(""" + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 3; + LineStyle: 15-15; + LineColor: #ff0000; + FontColor: #ff0000; + FontSize: 24; + } + """, style.toString()); + } + + @Test + void dotted() { + PlantUMLRelationshipStyle style = new PlantUMLRelationshipStyle("Relationship", "#ff0000", LineStyle.Dotted, 3, 24); + + assertEquals(""" + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 3; + LineStyle: 3-3; + LineColor: #ff0000; + FontColor: #ff0000; + FontSize: 24; + } + """, style.toString()); + } + +} diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRootStyleTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRootStyleTests.java new file mode 100644 index 000000000..b59703243 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/PlantUMLRootStyleTests.java @@ -0,0 +1,53 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.view.LineStyle; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlantUMLRootStyleTests { + + @Test + void noFont() { + PlantUMLRootStyle style = new PlantUMLRootStyle( + "#ffffff", + "#444444", + null); + + assertEquals(""" + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + """, style.toString()); + + style = new PlantUMLRootStyle( + "#ffffff", + "#444444", + ""); + + assertEquals(""" + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + """, style.toString()); + } + + @Test + void font() { + PlantUMLRootStyle style = new PlantUMLRootStyle( + "#ffffff", + "#444444", + "Courier"); + + assertEquals(""" + root { + BackgroundColor: #ffffff; + FontColor: #444444; + FontName: Courier; + } + """, style.toString()); + } + +} diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 136e48cb9..8a6e1ff66 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -11,109 +11,991 @@ import java.io.File; import java.util.Collection; +import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class StructurizrPlantUMLDiagramExporterTests extends AbstractExporterTests { @Test - public void test_BigBankPlcExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); - workspace.getViews().getConfiguration().addProperty(StructurizrPlantUMLExporter.PLANTUML_ANIMATION_PROPERTY, "true"); + public void systemLandscapeView_NoStyling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A", "Description."); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B", "Description."); + a.uses(b, "Description", "Technology"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==A\\n[Software System]\\n\\nDescription." <> as A + rectangle "==B\\n[Software System]\\n\\nDescription." <> as B + + A --> B <> : "Description\\n[Technology]" + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + + + rectangle "==Element" <> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <> : "Relationship" + + @enduml""", diagram.getLegend().getDefinition()); + } + + @Test + public void systemLandscapeView_NoStyling_Dark() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A", "Description."); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B", "Description."); + a.uses(b, "Description", "Technology"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addDefaultElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Dark); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title System Landscape\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==A\\n[Software System]\\n\\nDescription." <> as A + rectangle "==B\\n[Software System]\\n\\nDescription." <> as B + + A --> B <> : "Description\\n[Technology]" + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + + + rectangle "==Element" <> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <> : "Relationship" + + @enduml""", diagram.getLegend().getDefinition()); + } + + @Test + public void systemContextView_NoStyling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A", "Description."); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B", "Description."); + a.uses(b, "Description", "Technology"); + + SystemContextView view = workspace.getViews().createSystemContextView(a, "key", "Description"); + view.addDefaultElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title A - System Context\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==A\\n[Software System]\\n\\nDescription." <> as A + rectangle "==B\\n[Software System]\\n\\nDescription." <> as B + + A --> B <> : "Description\\n[Technology]" + + @enduml""", diagram.getDefinition()); + } + + @Test + public void containerView_NoStyling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("Software System A", "Description."); + Container container1 = a.addContainer("Container 1", "Description", "Technology"); + Container container2 = a.addContainer("Container 2", "Description", "Technology"); + container1.uses(container2, "Description", "Technology"); + + ContainerView view = workspace.getViews().createContainerView(a, "key", "Description"); + view.addDefaultElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title Software System A - Containers\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Software System A\\n[Software System]" <> { + rectangle "==Container 1\\n[Container: Technology]\\n\\nDescription" <> as SoftwareSystemA.Container1 + rectangle "==Container 2\\n[Container: Technology]\\n\\nDescription" <> as SoftwareSystemA.Container2 + } + + SoftwareSystemA.Container1 --> SoftwareSystemA.Container2 <> : "Description\\n[Technology]" + + @enduml""", diagram.getDefinition()); + } + + @Test + public void componentView_NoStyling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("Software System", "Description."); + Container container = a.addContainer("Container", "Description", "Technology"); + Component component1 = container.addComponent("Component 1", "Description", "Technology"); + Component component2 = container.addComponent("Component 2", "Description", "Technology"); + component1.uses(component2, "Description", "Technology"); + + ComponentView view = workspace.getViews().createComponentView(container, "key", "Description"); + view.addDefaultElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title Software System - Container - Components\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Container\\n[Container: Technology]" <> { + rectangle "==Component 1\\n[Component: Technology]\\n\\nDescription" <> as SoftwareSystem.Container.Component1 + rectangle "==Component 2\\n[Component: Technology]\\n\\nDescription" <> as SoftwareSystem.Container.Component2 + } + + SoftwareSystem.Container.Component1 --> SoftwareSystem.Container.Component2 <> : "Description\\n[Technology]" + + @enduml""", diagram.getDefinition()); + } + + @Test + public void deploymentView_NoStyling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + DeploymentNode node1 = workspace.getModel().addDeploymentNode("Node 1"); + node1.addDeploymentNode("Node 2").add(a); + node1.addDeploymentNode("Node 3").addInfrastructureNode("Infrastructure Node"); + + DeploymentView view = workspace.getViews().createDeploymentView("deployment", "Default"); + view.addDefaultElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title Deployment - Default\\nDefault + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Node 1\\n[Deployment Node]" <> as Default.Node1 { + rectangle "Node 2\\n[Deployment Node]" <> as Default.Node1.Node2 { + rectangle "==A\\n[Software System]" <> as Default.Node1.Node2.A_1 + } + + rectangle "Node 3\\n[Deployment Node]" <> as Default.Node1.Node3 { + rectangle "==Infrastructure Node\\n[Infrastructure Node]" <> as Default.Node1.Node3.InfrastructureNode + } + + } + + @enduml""", diagram.getDefinition()); + } + + @Test + public void dynamicView_CollaborationStyle_NoStyling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + + a.uses(b, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(a, b); StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); - Collection diagrams = exporter.export(workspace); - assertEquals(7, diagrams.size()); - - Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml")); - assertEquals(expected, diagram.getDefinition()); - assertEquals(0, diagram.getFrames().size()); - - //assertEquals("", diagram.getLegend().getDefinition()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml")); - assertEquals(expected, diagram.getDefinition()); - assertEquals(4, diagram.getFrames().size()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml")); - assertEquals(expected, diagram.getDefinition()); - assertEquals(6, diagram.getFrames().size()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml")); - assertEquals(expected, diagram.getDefinition()); - assertEquals(4, diagram.getFrames().size()); - - diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml")); - assertEquals(expected, diagram.getDefinition()); - assertEquals(6, diagram.getFrames().size()); - - diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml")); - assertEquals(expected, diagram.getDefinition()); - assertEquals(3, diagram.getFrames().size()); - - diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml")); - assertEquals(expected, diagram.getDefinition()); - assertEquals(5, diagram.getFrames().size()); - - // and the sequence diagram version - workspace.getViews().getConfiguration().addProperty(exporter.PLANTUML_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/structurizr/36141-SignIn-sequence.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Dynamic\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==A\\n[Software System]" <> as A + rectangle "==B\\n[Software System]" <> as B + + A --> B <> : "1. Uses" + + @enduml""", diagram.getDefinition()); } @Test - @Tag("IntegrationTest") - public void test_AmazonWebServicesExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-54915-workspace.json")); - ThemeUtils.loadThemes(workspace); - workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); + public void dynamicView_CollaborationStyle_Frames() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + SoftwareSystem c = workspace.getModel().addSoftwareSystem("C"); + + a.uses(b, "Uses"); + b.uses(c, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(a, b); + view.add(b, c); + view.addProperty(StructurizrPlantUMLExporter.PLANTUML_ANIMATION_PROPERTY, "true"); StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); - Collection diagrams = exporter.export(workspace); - assertEquals(1, diagrams.size()); + List frames = exporter.export(view).getFrames(); + assertEquals(2, frames.size()); - Diagram diagram = diagrams.stream().findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Dynamic\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==A\\n[Software System]" <> as A + rectangle "==B\\n[Software System]" <> as B + rectangle "==C\\n[Software System]" <> as C + hide C + + A --> B <> : "1. Uses" + B --> C <> : "2. Uses" + + @enduml""", frames.get(0).getDefinition()); + + assertEquals(""" + @startuml + title Dynamic\\nDescription + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==A\\n[Software System]" <> as A + hide A + rectangle "==B\\n[Software System]" <> as B + rectangle "==C\\n[Software System]" <> as C + + A --> B <> : "1. Uses" + B --> C <> : "2. Uses" + + @enduml""", frames.get(1).getDefinition()); + } + + @Test + public void dynamicView_SequenceStyle_NoStyling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + + a.uses(b, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(a, b); + view.addProperty(StructurizrPlantUMLExporter.PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "true"); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title Dynamic\\nDescription + + set separator none + hide stereotype + + + + participant "A\\n[Software System]" as A <> #ffffff + participant "B\\n[Software System]" as B <> #ffffff + + A -> B <> : Uses + + @enduml""", diagram.getDefinition()); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml")); - assertEquals(expected, diagram.getLegend().getDefinition()); + assertEquals(""" + @startuml + + set separator none + hide stereotype + + + + rectangle "==Element" <> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <> : "Relationship" + + @enduml""", diagram.getLegend().getDefinition()); } @Test - public void test_GroupsExample() throws Exception { + public void groups() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); - ThemeUtils.loadThemes(workspace); StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); Collection diagrams = exporter.export(workspace); assertEquals(3, diagrams.size()); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "Group 1" <> as groupR3JvdXAgMQ== { + rectangle "==B\\n[Software System]" <> as B + } + + rectangle "Group 2" <> as groupR3JvdXAgMg== { + rectangle "==C\\n[Software System]" <> as C + rectangle "Group 3" <> as groupR3JvdXAgMi9Hcm91cCAz { + rectangle "==D\\n[Software System]" <> as D + } + + } + + rectangle "==A\\n[Software System]" <> as A + + B --> C <> : "" + C --> D <> : "" + A --> B <> : "" + + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title D - Containers + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "==C\\n[Software System]" <> as C + + rectangle "D\\n[Software System]" <> { + rectangle "Group 4" <> as groupR3JvdXAgNA== { + rectangle "==F\\n[Container]" <> as D.F + } + + rectangle "==E\\n[Container]" <> as D.E + } + + C --> D.E <> : "" + C --> D.F <> : "" + + @enduml""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title D - F - Components + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "==C\\n[Software System]" <> as C + + rectangle "F\\n[Container]" <> { + rectangle "Group 5" <> as groupR3JvdXAgNQ== { + rectangle "==H\\n[Component]" <> as D.F.H + } + + rectangle "==G\\n[Component]" <> as D.F.G + } + + C --> D.F.G <> : "" + C --> D.F.H <> : "" + + @enduml""", diagram.getDefinition()); } @Test - public void test_NestedGroupsExample() throws Exception { + public void nestedGroups() { Workspace workspace = new Workspace("Name", "Description"); workspace.getModel().addProperty("structurizr.groupSeparator", "/"); @@ -132,7 +1014,7 @@ public void test_NestedGroupsExample() throws Exception { SoftwareSystem e = workspace.getModel().addSoftwareSystem("Department 1"); e.setGroup("Organisation 1/Department 1"); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape"); view.addAllElements(); workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Organisation 1/Department 1/Team 1").color("#ff0000"); @@ -142,44 +1024,115 @@ public void test_NestedGroupsExample() throws Exception { Collection diagrams = exporter.export(workspace); Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml")); - assertEquals(expected, diagram.getDefinition()); - } - - @Test - public void test_renderGroupStyles() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - workspace.getModel().addPerson("User 1").setGroup("Group 1"); - workspace.getModel().addPerson("User 2").setGroup("Group 2"); - workspace.getModel().addPerson("User 3").setGroup("Group 3"); - - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", ""); - view.addDefaultElements(); - - workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 1").color("#111111").icon("https://example.com/icon1.png"); - workspace.getViews().getConfiguration().getStyles().addElementStyle("Group:Group 2").color("#222222").icon("https://example.com/icon2.png"); - - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter() { - @Override - protected double calculateIconScale(String iconUrl) { - return 1.0; - } - }; - - Diagram diagram = exporter.export(view); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml")); - assertEquals(expected, diagram.getDefinition()); - - workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); - - diagram = exporter.export(view); - expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title System Landscape + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Organisation 1" <> as groupT3JnYW5pc2F0aW9uIDE= { + rectangle "==Organisation 1\\n[Software System]" <> as Organisation1 + rectangle "Department 1" <> as groupT3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAx { + rectangle "==Department 1\\n[Software System]" <> as Department1 + rectangle "Team 1" <> as groupT3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMQ== { + rectangle "==Team 1\\n[Software System]" <> as Team1 + } + + rectangle "Team 2" <> as groupT3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMg== { + rectangle "==Team 2\\n[Software System]" <> as Team2 + } + + } + + } + + rectangle "Organisation 2" <> as groupT3JnYW5pc2F0aW9uIDI= { + rectangle "==Organisation 2\\n[Software System]" <> as Organisation2 + } + + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderContainerDiagramWithExternalContainers() { - Workspace workspace = new Workspace("Name", "Description"); + public void containerDiagramWithExternalContainers() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); @@ -192,59 +1145,79 @@ public void test_renderContainerDiagramWithExternalContainers() { containerView.add(container2); Diagram diagram = new StructurizrPlantUMLExporter().export(containerView); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Software System 1 - Containers\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "skinparam {\n" + - " arrowFontSize 10\n" + - " defaultTextAlignment center\n" + - " wrapWidth 200\n" + - " maxMessageSize 100\n" + - "}\n" + - "\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BorderColor #9a9a9a\n" + - " FontColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BorderColor #9a9a9a\n" + - " FontColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "\n" + - "rectangle \"Software System 1\\n[Software System]\" <> {\n" + - " rectangle \"==Container 1\\n[Container]\" <> as SoftwareSystem1.Container1\n" + - "}\n" + - "\n" + - "rectangle \"Software System 2\\n[Software System]\" <> {\n" + - " rectangle \"==Container 2\\n[Container]\" <> as SoftwareSystem2.Container2\n" + - "}\n" + - "\n" + - "SoftwareSystem1.Container1 .[#707070,thickness=2].> SoftwareSystem2.Container2 : \"Uses\"\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System 1 - Containers + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Software System 1\\n[Software System]" <> { + rectangle "==Container 1\\n[Container]" <> as SoftwareSystem1.Container1 + } + + rectangle "Software System 2\\n[Software System]" <> { + rectangle "==Container 2\\n[Container]" <> as SoftwareSystem2.Container2 + } + + SoftwareSystem1.Container1 --> SoftwareSystem2.Container2 <> : "Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderComponentDiagramWithExternalComponents() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); + public void componentDiagramWithExternalComponents() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -264,13 +1237,81 @@ public void test_renderComponentDiagramWithExternalComponents() throws Exception componentView.add(component3); Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System 1 - Container 1 - Components + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Container 1\\n[Container]" <> { + rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 + } + + rectangle "Container 2\\n[Container]" <> { + rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 + } + + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderComponentDiagramWithExternalComponentsAndSoftwareSystemBoundariesIncluded() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); + public void componentDiagramWithExternalComponentsAndSoftwareSystemBoundariesIncluded() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -291,13 +1332,109 @@ public void test_renderComponentDiagramWithExternalComponentsAndSoftwareSystemBo componentView.addProperty("structurizr.softwareSystemBoundaries", "true"); Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System 1 - Container 1 - Components + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Software System 1\\n[Software System]" <> { + rectangle "Container 1\\n[Container]" <> { + rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 + } + + } + + rectangle "Software System 2\\n[Software System]" <> { + rectangle "Container 2\\n[Container]" <> { + rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 + } + + } + + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderDynamicDiagramWithExternalContainers() { - Workspace workspace = new Workspace("Name", "Description"); + public void dynamicView_ExternalContainers() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); @@ -309,59 +1446,79 @@ public void test_renderDynamicDiagramWithExternalContainers() { dynamicView.add(container1, container2); Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); - assertEquals("@startuml\n" + - "set separator none\n" + - "title Software System 1 - Dynamic\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "skinparam {\n" + - " arrowFontSize 10\n" + - " defaultTextAlignment center\n" + - " wrapWidth 200\n" + - " maxMessageSize 100\n" + - "}\n" + - "\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BorderColor #9a9a9a\n" + - " FontColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<> {\n" + - " BorderColor #9a9a9a\n" + - " FontColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "\n" + - "rectangle \"Software System 1\\n[Software System]\" <> {\n" + - " rectangle \"==Container 1\\n[Container]\" <> as SoftwareSystem1.Container1\n" + - "}\n" + - "\n" + - "rectangle \"Software System 2\\n[Software System]\" <> {\n" + - " rectangle \"==Container 2\\n[Container]\" <> as SoftwareSystem2.Container2\n" + - "}\n" + - "\n" + - "SoftwareSystem1.Container1 .[#707070,thickness=2].> SoftwareSystem2.Container2 : \"1. Uses\"\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Software System 1 - Dynamic + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Software System 1\\n[Software System]" <> { + rectangle "==Container 1\\n[Container]" <> as SoftwareSystem1.Container1 + } + + rectangle "Software System 2\\n[Software System]" <> { + rectangle "==Container 2\\n[Container]" <> as SoftwareSystem2.Container2 + } + + SoftwareSystem1.Container1 --> SoftwareSystem2.Container2 <> : "1. Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderDynamicDiagramWithExternalComponents() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); + public void dynamicView_ExternalComponents() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -380,13 +1537,81 @@ public void test_renderDynamicDiagramWithExternalComponents() throws Exception { dynamicView.add(component2, component3); Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Container 1 - Dynamic + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Container 1\\n[Container]" <> { + rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 + } + + rectangle "Container 2\\n[Container]" <> { + rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 + } + + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1. Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2. Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderDynamicDiagramWithExternalComponentsAndSoftwareSystemBoundariesIncluded() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); + public void dynamicView_ExternalComponentsAndSoftwareSystemBoundariesIncluded() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); @@ -406,341 +1631,401 @@ public void test_renderDynamicDiagramWithExternalComponentsAndSoftwareSystemBoun dynamicView.addProperty("structurizr.softwareSystemBoundaries", "true"); Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title Container 1 - Dynamic + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Software System 1\\n[Software System]" <> { + rectangle "Container 1\\n[Container]" <> { + rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 + } + + } + + rectangle "Software System 2\\n[Software System]" <> { + rectangle "Container 2\\n[Container]" <> { + rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 + } + + } + + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1. Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2. Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderDiagramWithElementUrls() { - Workspace workspace = new Workspace("Name", "Description"); + public void elementUrls() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); softwareSystem.setUrl("https://structurizr.com"); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); view.addDefaultElements(); Diagram diagram = new StructurizrPlantUMLExporter().export(view); - assertTrue(diagram.getDefinition().contains("rectangle \"==Software System\\n[Software System]\" <> as SoftwareSystem [[https://structurizr.com]]\n")); + assertTrue(diagram.getDefinition().contains("as SoftwareSystem [[https://structurizr.com]]")); } @Test - public void test_renderDiagramWithIncludes() { - Workspace workspace = new Workspace("Name", "Description"); - workspace.getModel().addSoftwareSystem("Software System"); + public void elementInstanceUrl() { + Workspace workspace = new Workspace("Name"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + a.setUrl("https://example.com/url1"); + SoftwareSystemInstance aInstance = workspace.getModel().addDeploymentNode("Node").add(a); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); - view.addDefaultElements(); + DeploymentView view = workspace.getViews().createDeploymentView("deployment", "Default"); + view.add(aInstance); - view.getViewSet().getConfiguration().addProperty(StructurizrPlantUMLExporter.PLANTUML_INCLUDES_PROPERTY, "styles.puml"); + assertTrue(new StructurizrPlantUMLExporter().export(view).getDefinition().contains("as Default.Node.A_1 [[https://example.com/url1]]")); - Diagram diagram = new StructurizrPlantUMLExporter().export(view); - assertTrue(diagram.getDefinition().contains("!include styles.puml\n")); + aInstance.setUrl("https://example.com/url2"); + assertTrue(new StructurizrPlantUMLExporter().export(view).getDefinition().contains("as Default.Node.A_1 [[https://example.com/url2]]")); } @Test - public void test_renderDiagramWithNewLineCharacterInElementName() { - Workspace workspace = new Workspace("Name", "Description"); + public void newLineCharacterInElementName() { + Workspace workspace = new Workspace("Name"); workspace.getModel().addSoftwareSystem("Software\nSystem"); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); view.addDefaultElements(); Diagram diagram = new StructurizrPlantUMLExporter().export(view); - assertTrue(diagram.getDefinition().contains("rectangle \"==Software\\nSystem\\n[Software System]\" <> as SoftwareSystem")); + assertEquals(""" + @startuml + title System Landscape + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==Software\\nSystem\\n[Software System]" <> as SoftwareSystem + + @enduml""", diagram.getDefinition()); } @Test - public void test_renderCustomView() { - Workspace workspace = new Workspace("Name", "Description"); + public void customView() { + Workspace workspace = new Workspace("Name"); Model model = workspace.getModel(); CustomElement a = model.addCustomElement("A"); CustomElement b = model.addCustomElement("B", "Custom", "Description"); a.uses(b, "Uses"); - CustomView view = workspace.getViews().createCustomView("key", "Title", "Description"); + CustomView view = workspace.getViews().createCustomView("key", "Title"); view.addDefaultElements(); Diagram diagram = new StructurizrPlantUMLExporter().export(view); - assertEquals("@startuml\nset separator none\n" + - "title Title\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "skinparam {\n" + - " arrowFontSize 10\n" + - " defaultTextAlignment center\n" + - " wrapWidth 200\n" + - " maxMessageSize 100\n" + - "}\n" + - "\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<<1>> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "skinparam rectangle<<2>> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "\n" + - "rectangle \"==A\" <<1>> as 1\n" + - "rectangle \"==B\\n[Custom]\\n\\nDescription\" <<2>> as 2\n" + - "\n" + - "1 .[#707070,thickness=2].> 2 : \"Uses\"\n" + - "@enduml", diagram.getDefinition()); + assertEquals(""" + @startuml + title Title + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "==A" <> as 1 + rectangle "==B\\n[Custom]\\n\\nDescription" <> as 2 + + 1 --> 2 <> : "Uses" + + @enduml""", diagram.getDefinition()); } @Test void renderWorkspaceWithUnicodeElementName() { - Workspace workspace = new Workspace("Name", "Description"); + Workspace workspace = new Workspace("Name"); workspace.getModel().addPerson("Пользователь"); workspace.getViews().createSystemLandscapeView("key", "Description").addDefaultElements(); String diagramDefinition = new StructurizrPlantUMLExporter().export(workspace).stream().findFirst().get().getDefinition(); - assertTrue(diagramDefinition.contains("skinparam rectangle<<Пользователь>> {")); - assertTrue(diagramDefinition.contains("rectangle \"==Пользователь\\n[Person]\" <<Пользователь>> as Пользователь")); - } - - @Test - public void testLegend() { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - - CustomElement a = model.addCustomElement("A"); - a.addTags("Tag 1"); - CustomElement b = model.addCustomElement("B"); - b.addTags("Tag 2"); - a.uses(b, "...").addTags("Tag 3"); - b.uses(a, "...").addTags("Tag 4"); - - CustomView view = workspace.getViews().createCustomView("key", "Title", "Description"); - view.addDefaultElements(); - - Diagram diagram = new StructurizrPlantUMLExporter().export(view); - assertEquals("@startuml\nset separator none\n" + - "\n" + - "skinparam {\n" + - " shadowing false\n" + - " arrowFontSize 15\n" + - " defaultTextAlignment center\n" + - " wrapWidth 100\n" + - " maxMessageSize 100\n" + - "}\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<<_transparent>> {\n" + - " BorderColor transparent\n" + - " BackgroundColor transparent\n" + - " FontColor transparent\n" + - "}\n" + - "\n" + - "skinparam rectangle<<1>> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - "}\n" + - "rectangle \"==Element\" <<1>>\n" + - "\n" + - "rectangle \".\" <<_transparent>> as 2\n" + - "2 .[#707070,thickness=2].> 2 : \"Relationship\"\n" + - "\n" + - "\n" + - "@enduml", diagram.getLegend().getDefinition()); - - workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag 1").background("#ff0000").color("#ffffff").shape(Shape.RoundedBox); - workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag 2").background("#00ff00").color("#ffffff").shape(Shape.Hexagon); - workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag 3").color("#0000ff"); - workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Tag 4").color("#ff00ff").thickness(3).style(LineStyle.Solid); - - diagram = new StructurizrPlantUMLExporter().export(view); - assertEquals("@startuml\nset separator none\n" + - "\n" + - "skinparam {\n" + - " shadowing false\n" + - " arrowFontSize 15\n" + - " defaultTextAlignment center\n" + - " wrapWidth 100\n" + - " maxMessageSize 100\n" + - "}\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<<_transparent>> {\n" + - " BorderColor transparent\n" + - " BackgroundColor transparent\n" + - " FontColor transparent\n" + - "}\n" + - "\n" + - "skinparam rectangle<<1>> {\n" + - " BackgroundColor #ff0000\n" + - " FontColor #ffffff\n" + - " BorderColor #b20000\n" + - " roundCorner 20\n" + - "}\n" + - "rectangle \"==Tag 1\" <<1>>\n" + - "\n" + - "skinparam hexagon<<2>> {\n" + - " BackgroundColor #00ff00\n" + - " FontColor #ffffff\n" + - " BorderColor #00b200\n" + - "}\n" + - "hexagon \"==Tag 2\" <<2>>\n" + - "\n" + - "rectangle \".\" <<_transparent>> as 3\n" + - "3 .[#0000ff,thickness=2].> 3 : \"Tag 3\"\n" + - "\n" + - "rectangle \".\" <<_transparent>> as 4\n" + - "4 -[#ff00ff,thickness=3]-> 4 : \"Tag 4\"\n" + - "\n" + - "\n" + - "@enduml", diagram.getLegend().getDefinition()); - } - - @Test - public void staticDiagramsAreUnchangedWhenSequenceDiagramsAreEnabled() { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - - model.addSoftwareSystem("Software System").setGroup("Group"); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); - view.addAllElements(); - - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); - Diagram diagram; - String expected = """ + assertEquals(""" @startuml + title System Landscape\\nDescription + set separator none - title System Landscape - top to bottom direction - - skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 - } - hide stereotype - - skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false - } - - rectangle "Group" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Software System\\n[Software System]" <> as SoftwareSystem - } - - - @enduml"""; - - diagram = exporter.export(view); - assertEquals(expected, diagram.getDefinition()); - - workspace.getViews().getConfiguration().addProperty(StructurizrPlantUMLExporter.PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "true"); - - diagram = exporter.export(view); - assertEquals(expected, diagram.getDefinition()); + + + + rectangle "==Пользователь\\n[Person]" <> as Пользователь + + @enduml""", diagramDefinition); } @Test - public void testFont() { - Workspace workspace = new Workspace("Name", "Description"); + public void font() { + Workspace workspace = new Workspace("Name"); workspace.getModel().addPerson("User"); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); view.addAllElements(); workspace.getViews().getConfiguration().getBranding().setFont(new Font("Courier")); Diagram diagram = new StructurizrPlantUMLExporter().export(view); - assertEquals("@startuml\nset separator none\n" + - "title System Landscape\n" + - "\n" + - "top to bottom direction\n" + - "\n" + - "skinparam {\n" + - " arrowFontSize 10\n" + - " defaultTextAlignment center\n" + - " wrapWidth 200\n" + - " maxMessageSize 100\n" + - " defaultFontName \"Courier\"\n" + - "}\n" + - "\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - " shadowing false\n" + - "}\n" + - "\n" + - "rectangle \"==User\\n[Person]\" <> as User\n" + - "\n" + - "@enduml", diagram.getDefinition().toString()); - - assertEquals("@startuml\nset separator none\n" + - "\n" + - "skinparam {\n" + - " shadowing false\n" + - " arrowFontSize 15\n" + - " defaultTextAlignment center\n" + - " wrapWidth 100\n" + - " maxMessageSize 100\n" + - " defaultFontName \"Courier\"\n" + - "}\n" + - "hide stereotype\n" + - "\n" + - "skinparam rectangle<<_transparent>> {\n" + - " BorderColor transparent\n" + - " BackgroundColor transparent\n" + - " FontColor transparent\n" + - "}\n" + - "\n" + - "skinparam rectangle<<1>> {\n" + - " BackgroundColor #dddddd\n" + - " FontColor #000000\n" + - " BorderColor #9a9a9a\n" + - "}\n" + - "rectangle \"==Element\" <<1>>\n" + - "\n" + - "\n" + - "@enduml", diagram.getLegend().getDefinition()); + assertTrue(diagram.getDefinition().contains(""" + + + rectangle "Group 1" <> as groupR3JvdXAgMQ== { + rectangle "==A\\n[Software System]" <> as A + } + + rectangle "Group 2" <> as groupR3JvdXAgMg== { + rectangle "==B\\n[Software System]" <> as B + } + + + A --> B <> : "1. Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void dynamicView_SoftwareSystemScopedWithGroups() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); + public void dynamicView_SoftwareSystemScopedWithGroups() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystemA = workspace.getModel().addSoftwareSystem("A"); Container containerA = softwareSystemA.addContainer("A"); containerA.setGroup("Group 1"); @@ -749,18 +2034,112 @@ public void dynamicView_SoftwareSystemScopedWithGroups() throws Exception { containerB.setGroup("Group 2"); containerA.uses(containerB, "Uses"); - DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key"); view.add(containerA, containerB); StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); Diagram diagram = exporter.export(view); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + @startuml + title A - Dynamic + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "A\\n[Software System]" <> { + rectangle "Group 1" <> as groupR3JvdXAgMQ== { + rectangle "==A\\n[Container]" <> as A.A + } + + } + + rectangle "B\\n[Software System]" <> { + rectangle "Group 2" <> as groupR3JvdXAgMg== { + rectangle "==B\\n[Container]" <> as B.B + } + + } + + A.A --> B.B <> : "1. Uses" + + @enduml""", diagram.getDefinition()); } @Test - public void dynamicView_ContainerScopedWithGroups() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); + public void dynamicView_ContainerScopedWithGroups() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystemA = workspace.getModel().addSoftwareSystem("A"); Container containerA = softwareSystemA.addContainer("A"); Component componentA = containerA.addComponent("A"); @@ -771,91 +2150,107 @@ public void dynamicView_ContainerScopedWithGroups() throws Exception { componentB.setGroup("Group 2"); componentA.uses(componentB, "Uses"); - DynamicView view = workspace.getViews().createDynamicView(containerA, "key", "Description"); + DynamicView view = workspace.getViews().createDynamicView(containerA, "key"); view.add(componentA, componentB); StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); Diagram diagram = exporter.export(view); - String expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml")); - assertEquals(expected, diagram.getDefinition()); - } - - @Test - public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSeparator() { - Workspace workspace = new Workspace("Name", ""); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); - Container container1 = softwareSystem.addContainer("Container 1"); - container1.setGroup("Group 1"); - Container container2 = softwareSystem.addContainer("Container 2"); - container2.setGroup("Group 2"); - - ContainerView view = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); - view.addAllElements(); - - String expectedResult = """ + assertEquals(""" @startuml + title A - Dynamic + set separator none - title Software System - Containers - top to bottom direction - - skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 - } - hide stereotype - - skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false - } - skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false - } - skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false + + + + rectangle "A\\n[Container]" <> { + rectangle "Group 1" <> as groupR3JvdXAgMQ== { + rectangle "==A\\n[Component]" <> as A.A.A + } + } - - rectangle "Software System\\n[Software System]" <> { - rectangle "Group 1" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Container 1\\n[Container]" <> as SoftwareSystem.Container1 - } - - rectangle "Group 2" <> as group2 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Container 2\\n[Container]" <> as SoftwareSystem.Container2 - } - + + rectangle "B\\n[Container]" <> { + rectangle "Group 2" <> as groupR3JvdXAgMg== { + rectangle "==B\\n[Component]" <> as B.B.B + } + } - - @enduml"""; - - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); - Diagram diagram = exporter.export(view); - assertEquals(expectedResult, diagram.getDefinition()); - - // this should be the same - workspace.getModel().addProperty("structurizr.groupSeparator", "/"); - exporter = new StructurizrPlantUMLExporter(); - diagram = exporter.export(view); - assertEquals(expectedResult, diagram.getDefinition()); + + A.A.A --> B.B.B <> : "1. Uses" + + @enduml""", diagram.getDefinition()); } @Test @@ -873,212 +2268,907 @@ public void deploymentView_WithGroups() { softwareSystemInstance.setGroup("Group 2"); infrastructureNode2.setGroup("Group 2"); - DeploymentView view = workspace.getViews().createDeploymentView("key", "Description"); + DeploymentView view = workspace.getViews().createDeploymentView("key"); view.add(infrastructureNode1); view.add(infrastructureNode2); view.add(softwareSystemInstance); - String expectedResult = """ + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(""" @startuml + title Deployment - Default + set separator none - title Deployment - Default - top to bottom direction - - skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 - } - hide stereotype - - skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false - } - skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false - } - skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false - } - skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false - } - - rectangle "Group 1" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "Server 1\\n[Deployment Node]" <> as Default.Server1 { - rectangle "Group 2" <> as group2 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Infrastructure Node 2\\n[Infrastructure Node]" <> as Default.Server1.InfrastructureNode2 - rectangle "==Software System\\n[Software System]" <> as Default.Server1.SoftwareSystem_1 + + + + rectangle "Group 1" <> as groupR3JvdXAgMQ== { + rectangle "Server 1\\n[Deployment Node]" <> as Default.Server1 { + rectangle "Group 2" <> as groupR3JvdXAgMg== { + rectangle "==Infrastructure Node 2\\n[Infrastructure Node]" <> as Default.Server1.InfrastructureNode2 + rectangle "==Software System\\n[Software System]" <> as Default.Server1.SoftwareSystem_1 } - - rectangle "==Infrastructure Node 1\\n[Infrastructure Node]" <> as Default.Server1.InfrastructureNode1 + + rectangle "==Infrastructure Node 1\\n[Infrastructure Node]" <> as Default.Server1.InfrastructureNode1 } - + } - - @enduml"""; - - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); - Diagram diagram = exporter.export(view); - assertEquals(expectedResult, diagram.getDefinition()); - - // this should be the same - workspace.getModel().addProperty("structurizr.groupSeparator", "/"); - exporter = new StructurizrPlantUMLExporter(); - diagram = exporter.export(view); - assertEquals(expectedResult, diagram.getDefinition()); + + @enduml""", diagram.getDefinition()); } @Test - public void test_ElementInstanceUrl() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); - a.setUrl("https://example.com/url1"); - SoftwareSystemInstance aInstance = workspace.getModel().addDeploymentNode("Node").add(a); - - DeploymentView view = workspace.getViews().createDeploymentView("deployment", "Default"); - view.add(aInstance); + void light_group() { + Workspace workspace = new Workspace("Name"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + softwareSystem.setGroup("Name"); - assertEquals(""" -@startuml -set separator none -title Deployment - Default - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} - -rectangle "Node\\n[Deployment Node]" <> as Default.Node { - rectangle "==A\\n[Software System]" <> as Default.Node.A_1 [[https://example.com/url1]] -} - -@enduml""", new StructurizrPlantUMLExporter().export(view).getDefinition()); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.add(softwareSystem); - aInstance.setUrl("https://example.com/url2"); + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); assertEquals(""" -@startuml -set separator none -title Deployment - Default - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} - -rectangle "Node\\n[Deployment Node]" <> as Default.Node { - rectangle "==A\\n[Software System]" <> as Default.Node.A_1 [[https://example.com/url2]] -} - -@enduml""", new StructurizrPlantUMLExporter().export(view).getDefinition()); - + @startuml + title System Landscape + + set separator none + top to bottom direction + hide stereotype + + + + rectangle "Name" <> as groupTmFtZQ== { + rectangle "==Name\\n[Software System]" <> as Name + } + + + @enduml""", diagram.getDefinition()); } @Test - void groupAndSoftwareSystemNameAreTheSame() { - Workspace workspace = new Workspace("Name", "Description"); + void dark_group() { + Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); softwareSystem.setGroup("Name"); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); view.add(softwareSystem); - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Dark); Diagram diagram = exporter.export(view); assertEquals(""" @startuml + title System Landscape + set separator none - title System Landscape - top to bottom direction - - skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 + hide stereotype + + + + rectangle "Name" <> as groupTmFtZQ== { + rectangle "==Name\\n[Software System]" <> as Name } - + + + @enduml""", diagram.getDefinition()); + } + + @Test + @Tag("IntegrationTest") + public void amazonWebServicesExample_Light() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); + ThemeUtils.loadThemes(workspace); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); + Collection diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + assertEquals(""" + @startuml + title X - Deployment - Live + + set separator none + left to right direction + skinparam ranksep 60 + skinparam nodesep 30 hide stereotype - - skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false + + + + rectangle "Amazon Web Services\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices { + rectangle "US-East-1\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1 { + rectangle "Autoscaling group\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2 - Ubuntu server\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { + rectangle "==Web Application\\n[Container: Java and Spring Boot]" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 + } + + } + + rectangle "Amazon RDS\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { + database "==Database Schema\\n[Container]" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 + } + + } + + rectangle "==DNS router\\n[Infrastructure Node: Route 53]\\n\\n\\n\\nRoutes incoming requests based upon domain name." <> as Live.AmazonWebServices.USEast1.DNSrouter + rectangle "==Load Balancer\\n[Infrastructure Node: Elastic Load Balancer]\\n\\n\\n\\nAutomatically distributes incoming application traffic." <> as Live.AmazonWebServices.USEast1.LoadBalancer + } + } - - rectangle "Name" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Name\\n[Software System]" <> as Name + + Live.AmazonWebServices.USEast1.LoadBalancer --> Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 <> : "Forwards requests to\\n[HTTPS]" + Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 --> Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 <> : "Reads from and writes to\\n[MySQL Protocol/SSL]" + Live.AmazonWebServices.USEast1.DNSrouter --> Live.AmazonWebServices.USEast1.LoadBalancer <> : "Forwards requests to\\n[HTTPS]" + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + + + rectangle "==Amazon Web Services - Auto Scaling\\n\\n" <> + + rectangle "==Amazon Web Services - Cloud\\n\\n" <> + + rectangle "==Amazon Web Services - EC2\\n\\n" <> + + rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n" <> + + rectangle "==Amazon Web Services - RDS\\n\\n" <> + + rectangle "==Amazon Web Services - RDS MySQL instance\\n\\n" <> + + rectangle "==Amazon Web Services - Region\\n\\n" <> + + rectangle "==Amazon Web Services - Route 53\\n\\n" <> + + rectangle "==Application" <> + + database "==Database" <> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <> : "Relationship" + + @enduml""", diagram.getLegend().getDefinition()); + } + + @Test + @Tag("IntegrationTest") + public void amazonWebServicesExample_Dark() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); + ThemeUtils.loadThemes(workspace); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Dark); + Collection diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + assertEquals(""" + @startuml + title X - Deployment - Live + + set separator none + left to right direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "Amazon Web Services\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices { + rectangle "US-East-1\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1 { + rectangle "Autoscaling group\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2 - Ubuntu server\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { + rectangle "==Web Application\\n[Container: Java and Spring Boot]" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 + } + + } + + rectangle "Amazon RDS\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { + database "==Database Schema\\n[Container]" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 + } + + } + + rectangle "==DNS router\\n[Infrastructure Node: Route 53]\\n\\n\\n\\nRoutes incoming requests based upon domain name." <> as Live.AmazonWebServices.USEast1.DNSrouter + rectangle "==Load Balancer\\n[Infrastructure Node: Elastic Load Balancer]\\n\\n\\n\\nAutomatically distributes incoming application traffic." <> as Live.AmazonWebServices.USEast1.LoadBalancer + } + } - - + + Live.AmazonWebServices.USEast1.LoadBalancer --> Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 <> : "Forwards requests to\\n[HTTPS]" + Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 --> Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 <> : "Reads from and writes to\\n[MySQL Protocol/SSL]" + Live.AmazonWebServices.USEast1.DNSrouter --> Live.AmazonWebServices.USEast1.LoadBalancer <> : "Forwards requests to\\n[HTTPS]" + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + + + rectangle "==Amazon Web Services - Auto Scaling\\n\\n" <> + + rectangle "==Amazon Web Services - Cloud\\n\\n" <> + + rectangle "==Amazon Web Services - EC2\\n\\n" <> + + rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n" <> + + rectangle "==Amazon Web Services - RDS\\n\\n" <> + + rectangle "==Amazon Web Services - RDS MySQL instance\\n\\n" <> + + rectangle "==Amazon Web Services - Region\\n\\n" <> + + rectangle "==Amazon Web Services - Route 53\\n\\n" <> + + rectangle "==Application" <> + + database "==Database" <> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <> : "Relationship" + + @enduml""", diagram.getLegend().getDefinition()); } } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml deleted file mode 100644 index 7bce7a631..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Components.puml +++ /dev/null @@ -1,52 +0,0 @@ -@startuml -set separator none -title Internet Banking System - API Application - Components - -top to bottom direction - -!include -!include -!include -!include - -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") - -System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") -System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") -Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") -Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") -ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - -Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { - Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.AccountsSummaryController, "Accounts Summary Controller", $techn="Spring MVC Rest Controller", $descr="Provides customers with a summary of their bank accounts.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.ResetPasswordController, "Reset Password Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to reset their passwords with a single use URL.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, "Mainframe Banking System Facade", $techn="Spring Bean", $descr="A facade onto the mainframe banking system.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.EmailComponent, "E-mail Component", $techn="Spring Bean", $descr="Sends e-mails to users.", $tags="Component", $link="") -} - -Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "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.SinglePageApplication, InternetBankingSystem.APIApplication.ResetPasswordController, "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.AccountsSummaryController, "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.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="SQL/TCP", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.EmailComponent, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml deleted file mode 100644 index 789bfdee0..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-Containers.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startuml -set separator none -title Internet Banking System - Containers - -top to bottom direction - -!include -!include -!include - -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="solid") - -Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") -System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") -System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") - -System_Boundary("InternetBankingSystem_boundary", "Internet Banking System", $tags="Software System") { - Container(InternetBankingSystem.WebApplication, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") - Container(InternetBankingSystem.APIApplication, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") - ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") - Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") -} - -Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") -Rel(PersonalBankingCustomer, InternetBankingSystem.WebApplication, "Visits bigbank.com/ib using", $techn="HTTPS", $tags="Relationship", $link="") -Rel(PersonalBankingCustomer, InternetBankingSystem.SinglePageApplication, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") -Rel(PersonalBankingCustomer, InternetBankingSystem.MobileApp, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.WebApplication, InternetBankingSystem.SinglePageApplication, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication, InternetBankingSystem.Database, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication, MainframeBankingSystem, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml deleted file mode 100644 index 0158ada25..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-DevelopmentDeployment.puml +++ /dev/null @@ -1,55 +0,0 @@ -@startuml -set separator none -title Internet Banking System - Deployment - Development - -top to bottom direction - -!include -!include -!include -!include - -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -Deployment_Node(Development.DeveloperLaptop, "Developer Laptop", $type="Microsoft Windows 10 or Apple macOS", $descr="", $tags="Element", $link="") { - Deployment_Node(Development.DeveloperLaptop.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { - Container(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") - } - - Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer, "Docker Container - Web Server", $type="Docker", $descr="", $tags="Element", $link="") { - Deployment_Node(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { - Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") - Container(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") - } - - } - - Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer, "Docker Container - Database Server", $type="Docker", $descr="", $tags="Element", $link="") { - Deployment_Node(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer, "Database Server", $type="Oracle 12c", $descr="", $tags="Element", $link="") { - ContainerDb(Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - } - - } - -} - -Deployment_Node(Development.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { - Deployment_Node(Development.BigBankplc.bigbankdev001, "bigbank-dev001", $type="", $descr="", $tags="Element", $link="") { - System(Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") - } - -} - -Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1, Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") -Rel(Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1, Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1, Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml deleted file mode 100644 index 177f25979..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml +++ /dev/null @@ -1,77 +0,0 @@ -@startuml -set separator none -title Internet Banking System - Deployment - Live - -top to bottom direction - -!include -!include -!include -!include - -AddElementTag("Failover", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Element", $bgColor="#ffffff", $borderColor="#888888", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Mobile App", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -Deployment_Node(Live.Customersmobiledevice, "Customer's mobile device", $type="Apple iOS or Android", $descr="", $tags="Element", $link="") { - Container(Live.Customersmobiledevice.MobileApp_1, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") -} - -Deployment_Node(Live.Customerscomputer, "Customer's computer", $type="Microsoft Windows or Apple macOS", $descr="", $tags="Element", $link="") { - Deployment_Node(Live.Customerscomputer.WebBrowser, "Web Browser", $type="Chrome, Firefox, Safari, or Edge", $descr="", $tags="Element", $link="") { - Container(Live.Customerscomputer.WebBrowser.SinglePageApplication_1, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") - } - -} - -Deployment_Node(Live.BigBankplc, "Big Bank plc", $type="Big Bank plc data center", $descr="", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankweb, "bigbank-web*** (x4)", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankweb.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { - Container(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, "Web Application", $techn="Java and Spring MVC", $descr="Delivers the static content and the Internet banking single page application.", $tags="Container", $link="") - } - - } - - Deployment_Node(Live.BigBankplc.bigbankapi, "bigbank-api*** (x8)", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankapi.ApacheTomcat, "Apache Tomcat", $type="Apache Tomcat 8.x", $descr="", $tags="Element", $link="") { - Container(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "API Application", $techn="Java and Spring MVC", $descr="Provides Internet banking functionality via a JSON/HTTPS API.", $tags="Container", $link="") - } - - } - - Deployment_Node(Live.BigBankplc.bigbankdb01, "bigbank-db01", $type="Ubuntu 16.04 LTS", $descr="", $tags="Element", $link="") { - Deployment_Node(Live.BigBankplc.bigbankdb01.OraclePrimary, "Oracle - Primary", $type="Oracle 12c", $descr="", $tags="Element", $link="") { - ContainerDb(Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - } - - } - - Deployment_Node(Live.BigBankplc.bigbankdb02, "bigbank-db02", $type="Ubuntu 16.04 LTS", $descr="", $tags="Failover", $link="") { - Deployment_Node(Live.BigBankplc.bigbankdb02.OracleSecondary, "Oracle - Secondary", $type="Oracle 12c", $descr="", $tags="Failover", $link="") { - ContainerDb(Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - } - - } - - Deployment_Node(Live.BigBankplc.bigbankprod001, "bigbank-prod001", $type="", $descr="", $tags="Element", $link="") { - System(Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") - } - -} - -Rel(Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1, Live.Customerscomputer.WebBrowser.SinglePageApplication_1, "Delivers to the customer's web browser", $techn="", $tags="Relationship", $link="") -Rel(Live.Customersmobiledevice.MobileApp_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(Live.Customerscomputer.WebBrowser.SinglePageApplication_1, Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1, "Reads from and writes to", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1, Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1, "Makes API calls to", $techn="XML/HTTPS", $tags="Relationship", $link="") -Rel(Live.BigBankplc.bigbankdb01.OraclePrimary, Live.BigBankplc.bigbankdb02.OracleSecondary, "Replicates data to", $techn="", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml deleted file mode 100644 index 306185a43..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml +++ /dev/null @@ -1,26 +0,0 @@ -@startuml -set separator none -title API Application - Dynamic - -!include - -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") -Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") -Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") -ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - -Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "Validates credentials using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml deleted file mode 100644 index 2ed68bacd..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn.puml +++ /dev/null @@ -1,36 +0,0 @@ -@startuml -set separator none -title API Application - Dynamic - -top to bottom direction - -!include -!include -!include -!include - -AddElementTag("Component", $bgColor="#85bbf0", $borderColor="#5d82a8", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Database", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Web Browser", $bgColor="#438dd5", $borderColor="#2e6295", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") - -Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { - Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") -} - -Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") -ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - -Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1. Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2. Validates credentials using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3. select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4. Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "5. Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "6. Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml deleted file mode 100644 index 5151c41db..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemContext.puml +++ /dev/null @@ -1,31 +0,0 @@ -@startuml -set separator none -title Internet Banking System - System Context - -top to bottom direction - -!include -!include - -AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { - System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") - System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") - System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") -} - -Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") - -Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") -Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml deleted file mode 100644 index c49923d47..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SystemLandscape.puml +++ /dev/null @@ -1,40 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -!include -!include - -AddElementTag("Software System", $bgColor="#1168bd", $borderColor="#0b4884", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Person,Bank Staff", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Software System,Existing System", $bgColor="#999999", $borderColor="#6b6b6b", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Person,Customer", $bgColor="#08427b", $borderColor="#052e56", $fontColor="#ffffff", $sprite="", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -AddBoundaryTag("Big Bank plc", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_1, "Big Bank plc", $tags="Big Bank plc") { - Person(CustomerServiceStaff, "Customer Service Staff", $descr="Customer service staff within the bank.", $tags="Person,Bank Staff", $link="") - Person(BackOfficeStaff, "Back Office Staff", $descr="Administration and support staff within the bank.", $tags="Person,Bank Staff", $link="") - System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") - System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") - System(ATM, "ATM", $descr="Allows customers to withdraw cash.", $tags="Software System,Existing System", $link="") - System(InternetBankingSystem, "Internet Banking System", $descr="Allows customers to view information about their bank accounts, and make payments.", $tags="Software System", $link="") -} - -Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") - -Rel(PersonalBankingCustomer, InternetBankingSystem, "Views account balances, and makes payments using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem, MainframeBankingSystem, "Gets account information from, and makes payments using", $techn="", $tags="Relationship", $link="") -Rel(InternetBankingSystem, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="") -Rel(EmailSystem, PersonalBankingCustomer, "Sends e-mails to", $techn="", $tags="Relationship", $link="") -Rel(PersonalBankingCustomer, CustomerServiceStaff, "Asks questions to", $techn="Telephone", $tags="Relationship", $link="") -Rel(CustomerServiceStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") -Rel(PersonalBankingCustomer, ATM, "Withdraws cash using", $techn="", $tags="Relationship", $link="") -Rel(ATM, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") -Rel(BackOfficeStaff, MainframeBankingSystem, "Uses", $techn="", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml deleted file mode 100644 index da23c6199..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithTags.puml +++ /dev/null @@ -1,52 +0,0 @@ -@startuml -set separator none -title Spring PetClinic - Deployment - Live - -left to right direction - -!include -!include -!include -!include - -AddElementTag("Container,Application", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - RDS", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Auto Scaling", $bgColor="#ffffff", $borderColor="#cc2264", $fontColor="#cc2264", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Route 53", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-route-53.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - EC2", $bgColor="#ffffff", $borderColor="#d86613", $fontColor="#d86613", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Region", $bgColor="#ffffff", $borderColor="#147eba", $fontColor="#147eba", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Elastic Load Balancing", $bgColor="#ffffff", $borderColor="#693cc5", $fontColor="#693cc5", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png{scale=0.1}", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - RDS MySQL instance", $bgColor="#ffffff", $borderColor="#3b48cc", $fontColor="#3b48cc", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png{scale=0.15}", $shadowing="", $borderStyle="solid") -AddElementTag("Container,Database", $bgColor="#ffffff", $borderColor="#b2b2b2", $fontColor="#000000", $sprite="", $shadowing="", $borderStyle="solid") -AddElementTag("Amazon Web Services - Cloud", $bgColor="#ffffff", $borderColor="#232f3e", $fontColor="#232f3e", $sprite="img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png{scale=0.21428571428571427}", $shadowing="", $borderStyle="solid") - -AddRelTag("Relationship", $textColor="#707070", $lineColor="#707070", $lineStyle = "") - -Deployment_Node(Live.AmazonWebServices, "Amazon Web Services", $type="", $descr="", $tags="Amazon Web Services - Cloud", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1, "US-East-1", $type="", $descr="", $tags="Amazon Web Services - Region", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS, "Amazon RDS", $type="", $descr="", $tags="Amazon Web Services - RDS", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL, "MySQL", $type="", $descr="", $tags="Amazon Web Services - RDS MySQL instance", $link="") { - ContainerDb(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Database", $techn="Relational database schema", $descr="Stores information regarding the veterinarians, the clients, and their pets.", $tags="Container,Database", $link="") - } - - } - - Deployment_Node(Live.AmazonWebServices.USEast1.Route53, "Route 53", $type="", $descr="Highly available and scalable cloud DNS service.", $tags="Amazon Web Services - Route 53", $link="") - Deployment_Node(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Elastic Load Balancer", $type="", $descr="Automatically distributes incoming application traffic.", $tags="Amazon Web Services - Elastic Load Balancing", $link="") - Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup, "Autoscaling group", $type="", $descr="", $tags="Amazon Web Services - Auto Scaling", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2, "Amazon EC2", $type="", $descr="", $tags="Amazon Web Services - EC2", $link="") { - Container(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Web Application", $techn="Java and Spring Boot", $descr="Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", $tags="Container,Application", $link="") - } - - } - - } - -} - -Rel(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Reads from and writes to", $techn="MySQL Protocol/SSL", $tags="Relationship", $link="") -Rel(Live.AmazonWebServices.USEast1.Route53, Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Forwards requests to", $techn="HTTPS", $tags="Relationship", $link="") -Rel(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Forwards requests to", $techn="HTTPS", $tags="Relationship", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml deleted file mode 100644 index f6a6505d3..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/54915-AmazonWebServicesDeployment-WithoutTags.puml +++ /dev/null @@ -1,39 +0,0 @@ -@startuml -set separator none -title Spring PetClinic - Deployment - Live - -left to right direction - -!include -!include -!include -!include - -Deployment_Node(Live.AmazonWebServices, "Amazon Web Services", $type="", $descr="", $tags="", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1, "US-East-1", $type="", $descr="", $tags="", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS, "Amazon RDS", $type="", $descr="", $tags="", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL, "MySQL", $type="", $descr="", $tags="", $link="") { - ContainerDb(Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Database", $techn="Relational database schema", $descr="Stores information regarding the veterinarians, the clients, and their pets.", $tags="", $link="") - } - - } - - Deployment_Node(Live.AmazonWebServices.USEast1.Route53, "Route 53", $type="", $descr="Highly available and scalable cloud DNS service.", $tags="", $link="") - Deployment_Node(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Elastic Load Balancer", $type="", $descr="Automatically distributes incoming application traffic.", $tags="", $link="") - Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup, "Autoscaling group", $type="", $descr="", $tags="", $link="") { - Deployment_Node(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2, "Amazon EC2", $type="", $descr="", $tags="", $link="") { - Container(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Web Application", $techn="Java and Spring Boot", $descr="Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", $tags="", $link="") - } - - } - - } - -} - -Rel(Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1, "Reads from and writes to", $techn="MySQL Protocol/SSL", $tags="", $link="") -Rel(Live.AmazonWebServices.USEast1.Route53, Live.AmazonWebServices.USEast1.ElasticLoadBalancer, "Forwards requests to", $techn="HTTPS", $tags="", $link="") -Rel(Live.AmazonWebServices.USEast1.ElasticLoadBalancer, Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1, "Forwards requests to", $techn="HTTPS", $tags="", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml deleted file mode 100644 index 180f8c3aa..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-1.puml +++ /dev/null @@ -1,28 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -!include -!include - -AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed") -Boundary(group_1, "Group 1", $tags="Group 1") { - Person(User1, "User 1", $descr="", $tags="", $link="") -} - -AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed") -Boundary(group_2, "Group 2", $tags="Group 2") { - Person(User2, "User 2", $descr="", $tags="", $link="") -} - -AddBoundaryTag("Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_3, "Group 3", $tags="Group 3") { - Person(User3, "User 3", $descr="", $tags="", $link="") -} - - - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml deleted file mode 100644 index ff23c0a76..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/group-styles-2.puml +++ /dev/null @@ -1,28 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -!include -!include - -AddBoundaryTag("Group 1", $borderColor="#111111", $fontColor="#111111", $borderStyle="dashed") -Boundary(group_1, "Group 1", $tags="Group 1") { - Person(User1, "User 1", $descr="", $tags="", $link="") -} - -AddBoundaryTag("Group 2", $borderColor="#222222", $fontColor="#222222", $borderStyle="dashed") -Boundary(group_2, "Group 2", $tags="Group 2") { - Person(User2, "User 2", $descr="", $tags="", $link="") -} - -AddBoundaryTag("Group 3", $borderColor="#aabbcc", $fontColor="#aabbcc", $borderStyle="dashed") -Boundary(group_3, "Group 3", $tags="Group 3") { - Person(User3, "User 3", $descr="", $tags="", $link="") -} - - - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml deleted file mode 100644 index ff496fc56..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Components.puml +++ /dev/null @@ -1,26 +0,0 @@ -@startuml -set separator none -title D - F - Components - -top to bottom direction - -!include -!include -!include - -System(C, "C", $descr="", $tags="", $link="") - -Container_Boundary("D.F_boundary", "F", $tags="") { - AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_1, "Group 5", $tags="Group 5") { - Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") - } - - Component(D.F.G, "G", $techn="", $descr="", $tags="", $link="") -} - -Rel(C, D.F.G, "", $techn="", $tags="", $link="") -Rel(C, D.F.H, "", $techn="", $tags="", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml deleted file mode 100644 index 2f92cc66f..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-Containers.puml +++ /dev/null @@ -1,26 +0,0 @@ -@startuml -set separator none -title D - Containers - -top to bottom direction - -!include -!include -!include - -System(C, "C", $descr="", $tags="", $link="") - -System_Boundary("D_boundary", "D", $tags="") { - AddBoundaryTag("Group 4", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_1, "Group 4", $tags="Group 4") { - Container(D.F, "F", $techn="", $descr="", $tags="", $link="") - } - - Container(D.E, "E", $techn="", $descr="", $tags="", $link="") -} - -Rel(C, D.E, "", $techn="", $tags="", $link="") -Rel(C, D.F, "", $techn="", $tags="", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml deleted file mode 100644 index 0e045ac3d..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/groups-SystemLandscape.puml +++ /dev/null @@ -1,32 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -!include -!include - -AddBoundaryTag("Group 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_1, "Group 1", $tags="Group 1") { - System(B, "B", $descr="", $tags="", $link="") -} - -AddBoundaryTag("Group 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_2, "Group 2", $tags="Group 2") { - System(C, "C", $descr="", $tags="", $link="") - AddBoundaryTag("Group 2/Group 3", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_3, "Group 3", $tags="Group 2/Group 3") { - System(D, "D", $descr="", $tags="", $link="") - } - -} - -System(A, "A", $descr="", $tags="", $link="") - -Rel(B, C, "", $techn="", $tags="", $link="") -Rel(C, D, "", $techn="", $tags="", $link="") -Rel(A, B, "", $techn="", $tags="", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml deleted file mode 100644 index 25c92424c..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups-with-dot-separator.puml +++ /dev/null @@ -1,26 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -!include -!include - -AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_1, "Organisation 1", $tags="Organisation 1") { - AddBoundaryTag("Organisation 1.Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_2, "Department 1", $tags="Organisation 1.Department 1") { - AddBoundaryTag("Organisation 1.Department 1.Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_3, "Team 1", $tags="Organisation 1.Department 1.Team 1") { - System(Team1, "Team 1", $descr="", $tags="", $link="") - } - - } - -} - - - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml deleted file mode 100644 index 17b40ad7c..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/nested-groups.puml +++ /dev/null @@ -1,38 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -!include -!include - -AddBoundaryTag("Organisation 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_1, "Organisation 1", $tags="Organisation 1") { - System(Organisation1, "Organisation 1", $descr="", $tags="", $link="") - AddBoundaryTag("Organisation 1/Department 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_2, "Department 1", $tags="Organisation 1/Department 1") { - System(Department1, "Department 1", $descr="", $tags="", $link="") - AddBoundaryTag("Organisation 1/Department 1/Team 1", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_3, "Team 1", $tags="Organisation 1/Department 1/Team 1") { - System(Team1, "Team 1", $descr="", $tags="", $link="") - } - - AddBoundaryTag("Organisation 1/Department 1/Team 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_4, "Team 2", $tags="Organisation 1/Department 1/Team 2") { - System(Team2, "Team 2", $descr="", $tags="", $link="") - } - - } - -} - -AddBoundaryTag("Organisation 2", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") -Boundary(group_5, "Organisation 2", $tags="Organisation 2") { - System(Organisation2, "Organisation 2", $descr="", $tags="", $link="") -} - - - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml deleted file mode 100644 index 3a505ee33..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-containerView.puml +++ /dev/null @@ -1,28 +0,0 @@ -@startuml -set separator none -title SoftwareSystem - Containers - -top to bottom direction - -!include -!include -!include - -System_Boundary("SoftwareSystem_boundary", "SoftwareSystem", $tags="") { - WithoutPropertyHeader() - AddProperty("IP","127.0.0.1") - AddProperty("Region","East") - Container(SoftwareSystem.Container1, "Container 1", $techn="", $descr="", $tags="", $link="") - WithoutPropertyHeader() - AddProperty("IP","127.0.0.2") - AddProperty("Region","West") - Container(SoftwareSystem.Container2, "Container 2", $techn="", $descr="", $tags="", $link="") -} - -WithoutPropertyHeader() -AddProperty("Prop1","Value1") -AddProperty("Prop2","Value2") -Rel(SoftwareSystem.Container1, SoftwareSystem.Container2, "", $techn="", $tags="", $link="") - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml deleted file mode 100644 index f96e7c97a..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/c4plantuml/printProperties-deploymentView.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -set separator none -title Deployment - Default - -top to bottom direction - -!include -!include -!include - -WithoutPropertyHeader() -AddProperty("Prop1","Value1") -Deployment_Node(Default.Deploymentnode, "Deployment node", $type="", $descr="", $tags="", $link="") { - WithoutPropertyHeader() - AddProperty("Prop2","Value2") - Deployment_Node(Default.Deploymentnode.Infrastructurenode, "Infrastructure node", $type="technology", $descr="description", $tags="", $link="") -} - - -SHOW_LEGEND(true) -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml deleted file mode 100644 index 09c551dda..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Components.puml +++ /dev/null @@ -1,118 +0,0 @@ -@startuml -set separator none -title Internet Banking System - API Application - Components - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam database<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BorderColor #2e6295 - FontColor #2e6295 - shadowing false -} - -rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem -rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem -rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication -rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp -database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database - -rectangle "API Application\n[Container: Java and Spring MVC]" <> { - rectangle "==Sign In Controller\n[Component: Spring MVC Rest Controller]\n\nAllows users to sign in to the Internet Banking System." <> as InternetBankingSystem.APIApplication.SignInController - rectangle "==Accounts Summary Controller\n[Component: Spring MVC Rest Controller]\n\nProvides customers with a summary of their bank accounts." <> as InternetBankingSystem.APIApplication.AccountsSummaryController - rectangle "==Reset Password Controller\n[Component: Spring MVC Rest Controller]\n\nAllows users to reset their passwords with a single use URL." <> as InternetBankingSystem.APIApplication.ResetPasswordController - rectangle "==Security Component\n[Component: Spring Bean]\n\nProvides functionality related to signing in, changing passwords, etc." <> as InternetBankingSystem.APIApplication.SecurityComponent - rectangle "==Mainframe Banking System Facade\n[Component: Spring Bean]\n\nA facade onto the mainframe banking system." <> as InternetBankingSystem.APIApplication.MainframeBankingSystemFacade - rectangle "==E-mail Component\n[Component: Spring Bean]\n\nSends e-mails to users." <> as InternetBankingSystem.APIApplication.EmailComponent -} - -InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.AccountsSummaryController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.AccountsSummaryController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication.ResetPasswordController : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.APIApplication.SignInController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "Uses" -InternetBankingSystem.APIApplication.AccountsSummaryController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.MainframeBankingSystemFacade : "Uses" -InternetBankingSystem.APIApplication.ResetPasswordController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "Uses" -InternetBankingSystem.APIApplication.ResetPasswordController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.EmailComponent : "Uses" -InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "Reads from and writes to\n[SQL/TCP]" -InternetBankingSystem.APIApplication.MainframeBankingSystemFacade .[#707070,thickness=2].> MainframeBankingSystem : "Makes API calls to\n[XML/HTTPS]" -InternetBankingSystem.APIApplication.EmailComponent .[#707070,thickness=2].> EmailSystem : "Sends e-mail using" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml deleted file mode 100644 index dab94a130..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-Containers.puml +++ /dev/null @@ -1,94 +0,0 @@ -@startuml -set separator none -title Internet Banking System - Containers - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam database<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam person<> { - BackgroundColor #08427b - FontColor #ffffff - BorderColor #052e56 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BorderColor #0b4884 - FontColor #0b4884 - shadowing false -} - -person "==Personal Banking Customer\n[Person]\n\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer -rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem -rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem - -rectangle "Internet Banking System\n[Software System]" <> { - rectangle "==Web Application\n[Container: Java and Spring MVC]\n\nDelivers the static content and the Internet banking single page application." <> as InternetBankingSystem.WebApplication - rectangle "==API Application\n[Container: Java and Spring MVC]\n\nProvides Internet banking functionality via a JSON/HTTPS API." <> as InternetBankingSystem.APIApplication - database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database - rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication - rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp -} - -EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "Sends e-mails to" -PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.WebApplication : "Visits bigbank.com/ib using\n[HTTPS]" -PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.SinglePageApplication : "Views account balances, and makes payments using" -PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem.MobileApp : "Views account balances, and makes payments using" -InternetBankingSystem.WebApplication .[#707070,thickness=2].> InternetBankingSystem.SinglePageApplication : "Delivers to the customer's web browser" -InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.MobileApp .[#707070,thickness=2].> InternetBankingSystem.APIApplication : "Makes API calls to\n[JSON/HTTPS]" -InternetBankingSystem.APIApplication .[#707070,thickness=2].> InternetBankingSystem.Database : "Reads from and writes to\n[SQL/TCP]" -InternetBankingSystem.APIApplication .[#707070,thickness=2].> MainframeBankingSystem : "Makes API calls to\n[XML/HTTPS]" -InternetBankingSystem.APIApplication .[#707070,thickness=2].> EmailSystem : "Sends e-mail using" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml deleted file mode 100644 index b09bc9df7..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-DevelopmentDeployment.puml +++ /dev/null @@ -1,130 +0,0 @@ -@startuml -set separator none -title Internet Banking System - Deployment - Development - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam database<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} - -rectangle "Developer Laptop\n[Deployment Node: Microsoft Windows 10 or Apple macOS]" <> as Development.DeveloperLaptop { - rectangle "Web Browser\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Development.DeveloperLaptop.WebBrowser { - rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 - } - - rectangle "Docker Container - Web Server\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerWebServer { - rectangle "Apache Tomcat\n[Deployment Node: Apache Tomcat 8.x]" <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat { - rectangle "==Web Application\n[Container: Java and Spring MVC]\n\nDelivers the static content and the Internet banking single page application." <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 - rectangle "==API Application\n[Container: Java and Spring MVC]\n\nProvides Internet banking functionality via a JSON/HTTPS API." <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 - } - - } - - rectangle "Docker Container - Database Server\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer { - rectangle "Database Server\n[Deployment Node: Oracle 12c]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer { - database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 - } - - } - -} - -rectangle "Big Bank plc\n[Deployment Node: Big Bank plc data center]" <> as Development.BigBankplc { - rectangle "bigbank-dev001\n[Deployment Node]" <> as Development.BigBankplc.bigbankdev001 { - rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 - } - -} - -Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 : "Delivers to the customer's web browser" -Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 : "Makes API calls to\n[JSON/HTTPS]" -Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 : "Reads from and writes to\n[SQL/TCP]" -Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 : "Makes API calls to\n[XML/HTTPS]" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml deleted file mode 100644 index 3f6d368ca..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-LiveDeployment.puml +++ /dev/null @@ -1,192 +0,0 @@ -@startuml -set separator none -title Internet Banking System - Deployment - Live - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam database<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam database<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #888888 - shadowing false -} - -rectangle "Customer's mobile device\n[Deployment Node: Apple iOS or Android]" <> as Live.Customersmobiledevice { - rectangle "==Mobile App\n[Container: Xamarin]\n\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as Live.Customersmobiledevice.MobileApp_1 -} - -rectangle "Customer's computer\n[Deployment Node: Microsoft Windows or Apple macOS]" <> as Live.Customerscomputer { - rectangle "Web Browser\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Live.Customerscomputer.WebBrowser { - rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as Live.Customerscomputer.WebBrowser.SinglePageApplication_1 - } - -} - -rectangle "Big Bank plc\n[Deployment Node: Big Bank plc data center]" <> as Live.BigBankplc { - rectangle "bigbank-web*** (x4)\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankweb { - rectangle "Apache Tomcat\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankweb.ApacheTomcat { - rectangle "==Web Application\n[Container: Java and Spring MVC]\n\nDelivers the static content and the Internet banking single page application." <> as Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 - } - - } - - rectangle "bigbank-api*** (x8)\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankapi { - rectangle "Apache Tomcat\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankapi.ApacheTomcat { - rectangle "==API Application\n[Container: Java and Spring MVC]\n\nProvides Internet banking functionality via a JSON/HTTPS API." <> as Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 - } - - } - - rectangle "bigbank-db01\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb01 { - rectangle "Oracle - Primary\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb01.OraclePrimary { - database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 - } - - } - - rectangle "bigbank-db02\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb02 { - rectangle "Oracle - Secondary\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb02.OracleSecondary { - database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 - } - - } - - rectangle "bigbank-prod001\n[Deployment Node]" <> as Live.BigBankplc.bigbankprod001 { - rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 - } - -} - -Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 .[#707070,thickness=2].> Live.Customerscomputer.WebBrowser.SinglePageApplication_1 : "Delivers to the customer's web browser" -Live.Customersmobiledevice.MobileApp_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 : "Makes API calls to\n[JSON/HTTPS]" -Live.Customerscomputer.WebBrowser.SinglePageApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 : "Makes API calls to\n[JSON/HTTPS]" -Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 : "Reads from and writes to\n[SQL/TCP]" -Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 : "Reads from and writes to\n[SQL/TCP]" -Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 .[#707070,thickness=2].> Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 : "Makes API calls to\n[XML/HTTPS]" -Live.BigBankplc.bigbankdb01.OraclePrimary .[#707070,thickness=2].> Live.BigBankplc.bigbankdb02.OracleSecondary : "Replicates data to" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml deleted file mode 100644 index eb84e8096..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn-sequence.puml +++ /dev/null @@ -1,49 +0,0 @@ -@startuml -set separator none -title API Application - Dynamic - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam sequenceParticipant<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam sequenceParticipant<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam sequenceParticipant<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam sequenceParticipant<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} - -participant "Single-Page Application\n[Container: JavaScript and Angular]" as InternetBankingSystem.SinglePageApplication <> #438dd5 -participant "Sign In Controller\n[Component: Spring MVC Rest Controller]" as InternetBankingSystem.APIApplication.SignInController <> #85bbf0 -participant "Security Component\n[Component: Spring Bean]" as InternetBankingSystem.APIApplication.SecurityComponent <> #85bbf0 -database "Database\n[Container: Oracle Database Schema]" as InternetBankingSystem.Database <> #438dd5 -InternetBankingSystem.SinglePageApplication -[#707070]> InternetBankingSystem.APIApplication.SignInController : Submits credentials to -InternetBankingSystem.APIApplication.SignInController -[#707070]> InternetBankingSystem.APIApplication.SecurityComponent : Validates credentials using -InternetBankingSystem.APIApplication.SecurityComponent -[#707070]> InternetBankingSystem.Database : select * from users where username = ? -InternetBankingSystem.APIApplication.SecurityComponent <-[#707070]- InternetBankingSystem.Database : Returns user data to -InternetBankingSystem.APIApplication.SignInController <-[#707070]- InternetBankingSystem.APIApplication.SecurityComponent : Returns true if the hashed password matches -InternetBankingSystem.SinglePageApplication <-[#707070]- InternetBankingSystem.APIApplication.SignInController : Sends back an authentication token to -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml deleted file mode 100644 index f70ca4a45..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SignIn.puml +++ /dev/null @@ -1,62 +0,0 @@ -@startuml -set separator none -title API Application - Dynamic - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam database<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #438dd5 - FontColor #ffffff - BorderColor #2e6295 - shadowing false -} -skinparam rectangle<> { - BorderColor #2e6295 - FontColor #2e6295 - shadowing false -} - -rectangle "API Application\n[Container: Java and Spring MVC]" <> { - rectangle "==Sign In Controller\n[Component: Spring MVC Rest Controller]\n\nAllows users to sign in to the Internet Banking System." <> as InternetBankingSystem.APIApplication.SignInController - rectangle "==Security Component\n[Component: Spring Bean]\n\nProvides functionality related to signing in, changing passwords, etc." <> as InternetBankingSystem.APIApplication.SecurityComponent -} - -rectangle "==Single-Page Application\n[Container: JavaScript and Angular]\n\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication -database "==Database\n[Container: Oracle Database Schema]\n\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database - -InternetBankingSystem.SinglePageApplication .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SignInController : "1. Submits credentials to\n[JSON/HTTPS]" -InternetBankingSystem.APIApplication.SignInController .[#707070,thickness=2].> InternetBankingSystem.APIApplication.SecurityComponent : "2. Validates credentials using" -InternetBankingSystem.APIApplication.SecurityComponent .[#707070,thickness=2].> InternetBankingSystem.Database : "3. select * from users where username = ?\n[SQL/TCP]" -InternetBankingSystem.APIApplication.SecurityComponent <.[#707070,thickness=2]. InternetBankingSystem.Database : "4. Returns user data to\n[SQL/TCP]" -InternetBankingSystem.APIApplication.SignInController <.[#707070,thickness=2]. InternetBankingSystem.APIApplication.SecurityComponent : "5. Returns true if the hashed password matches" -InternetBankingSystem.SinglePageApplication <.[#707070,thickness=2]. InternetBankingSystem.APIApplication.SignInController : "6. Sends back an authentication token to\n[JSON/HTTPS]" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml deleted file mode 100644 index a3a399968..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemContext.puml +++ /dev/null @@ -1,59 +0,0 @@ -@startuml -set separator none -title Internet Banking System - System Context - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #1168bd - FontColor #ffffff - BorderColor #0b4884 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam person<> { - BackgroundColor #08427b - FontColor #ffffff - BorderColor #052e56 - shadowing false -} - -rectangle "Big Bank plc" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem - rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem - rectangle "==Internet Banking System\n[Software System]\n\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem -} - -person "==Personal Banking Customer\n[Person]\n\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer - -PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem : "Views account balances, and makes payments using" -InternetBankingSystem .[#707070,thickness=2].> MainframeBankingSystem : "Gets account information from, and makes payments using" -InternetBankingSystem .[#707070,thickness=2].> EmailSystem : "Sends e-mail using" -EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "Sends e-mails to" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml deleted file mode 100644 index 89d127718..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/36141-SystemLandscape.puml +++ /dev/null @@ -1,85 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam person<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam person<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam rectangle<> { - BackgroundColor #1168bd - FontColor #ffffff - BorderColor #0b4884 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #999999 - FontColor #ffffff - BorderColor #6b6b6b - shadowing false -} -skinparam person<> { - BackgroundColor #08427b - FontColor #ffffff - BorderColor #052e56 - shadowing false -} - -rectangle "Big Bank plc" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - person "==Customer Service Staff\n[Person]\n\nCustomer service staff within the bank." <> as CustomerServiceStaff - person "==Back Office Staff\n[Person]\n\nAdministration and support staff within the bank." <> as BackOfficeStaff - rectangle "==Mainframe Banking System\n[Software System]\n\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem - rectangle "==E-mail System\n[Software System]\n\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem - rectangle "==ATM\n[Software System]\n\nAllows customers to withdraw cash." <> as ATM - rectangle "==Internet Banking System\n[Software System]\n\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem -} - -person "==Personal Banking Customer\n[Person]\n\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer - -PersonalBankingCustomer .[#707070,thickness=2].> InternetBankingSystem : "Views account balances, and makes payments using" -InternetBankingSystem .[#707070,thickness=2].> MainframeBankingSystem : "Gets account information from, and makes payments using" -InternetBankingSystem .[#707070,thickness=2].> EmailSystem : "Sends e-mail using" -EmailSystem .[#707070,thickness=2].> PersonalBankingCustomer : "Sends e-mails to" -PersonalBankingCustomer .[#707070,thickness=2].> CustomerServiceStaff : "Asks questions to\n[Telephone]" -CustomerServiceStaff .[#707070,thickness=2].> MainframeBankingSystem : "Uses" -PersonalBankingCustomer .[#707070,thickness=2].> ATM : "Withdraws cash using" -ATM .[#707070,thickness=2].> MainframeBankingSystem : "Uses" -BackOfficeStaff .[#707070,thickness=2].> MainframeBankingSystem : "Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml deleted file mode 100644 index 12bb6a1ce..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment-Legend.puml +++ /dev/null @@ -1,102 +0,0 @@ -@startuml -set separator none - -skinparam { - shadowing false - arrowFontSize 15 - defaultTextAlignment center - wrapWidth 100 - maxMessageSize 100 -} -hide stereotype - -skinparam rectangle<<_transparent>> { - BorderColor transparent - BackgroundColor transparent - FontColor transparent -} - -skinparam rectangle<<1>> { - BackgroundColor #ffffff - FontColor #cc2264 - BorderColor #cc2264 - roundCorner 20 -} -rectangle "==Amazon Web Services - Auto Scaling\n\n" <<1>> - -skinparam rectangle<<2>> { - BackgroundColor #ffffff - FontColor #232f3e - BorderColor #232f3e - roundCorner 20 -} -rectangle "==Amazon Web Services - Cloud\n\n" <<2>> - -skinparam rectangle<<3>> { - BackgroundColor #ffffff - FontColor #d86613 - BorderColor #d86613 - roundCorner 20 -} -rectangle "==Amazon Web Services - EC2\n\n" <<3>> - -skinparam rectangle<<4>> { - BackgroundColor #ffffff - FontColor #693cc5 - BorderColor #693cc5 - roundCorner 20 -} -rectangle "==Amazon Web Services - Elastic Load Balancing\n\n" <<4>> - -skinparam rectangle<<5>> { - BackgroundColor #ffffff - FontColor #3b48cc - BorderColor #3b48cc - roundCorner 20 -} -rectangle "==Amazon Web Services - RDS\n\n" <<5>> - -skinparam rectangle<<6>> { - BackgroundColor #ffffff - FontColor #3b48cc - BorderColor #3b48cc - roundCorner 20 -} -rectangle "==Amazon Web Services - RDS MySQL instance\n\n" <<6>> - -skinparam rectangle<<7>> { - BackgroundColor #ffffff - FontColor #147eba - BorderColor #147eba - roundCorner 20 -} -rectangle "==Amazon Web Services - Region\n\n" <<7>> - -skinparam rectangle<<8>> { - BackgroundColor #ffffff - FontColor #693cc5 - BorderColor #693cc5 - roundCorner 20 -} -rectangle "==Amazon Web Services - Route 53\n\n" <<8>> - -skinparam rectangle<<9>> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #b2b2b2 - roundCorner 20 -} -rectangle "==Container, Application" <<9>> - -skinparam database<<10>> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #b2b2b2 -} -database "==Container, Database" <<10>> - -rectangle "." <<_transparent>> as 11 -11 .[#707070,thickness=2].> 11 : "Relationship" - - -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml deleted file mode 100644 index f1753149b..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/54915-AmazonWebServicesDeployment.puml +++ /dev/null @@ -1,113 +0,0 @@ -@startuml -set separator none -title Spring PetClinic - Deployment - Live - -left to right direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #d86613 - BorderColor #d86613 - roundCorner 20 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #3b48cc - BorderColor #3b48cc - roundCorner 20 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #232f3e - BorderColor #232f3e - roundCorner 20 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #cc2264 - BorderColor #cc2264 - roundCorner 20 - shadowing false -} -skinparam database<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #b2b2b2 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #693cc5 - BorderColor #693cc5 - roundCorner 20 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #3b48cc - BorderColor #3b48cc - roundCorner 20 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #693cc5 - BorderColor #693cc5 - roundCorner 20 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #147eba - BorderColor #147eba - roundCorner 20 - shadowing false -} -skinparam rectangle<> { - BackgroundColor #ffffff - FontColor #000000 - BorderColor #b2b2b2 - roundCorner 20 - shadowing false -} - -rectangle "Amazon Web Services\n[Deployment Node]\n\n" <> as Live.AmazonWebServices { - rectangle "US-East-1\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1 { - rectangle "Amazon RDS\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { - rectangle "MySQL\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { - database "==Database\n[Container: Relational database schema]\n\nStores information regarding the veterinarians, the clients, and their pets." <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1 - } - - } - - rectangle "==Route 53\n[Infrastructure Node]\n\nHighly available and scalable cloud DNS service.\n\n" <> as Live.AmazonWebServices.USEast1.Route53 - rectangle "==Elastic Load Balancer\n[Infrastructure Node]\n\nAutomatically distributes incoming application traffic.\n\n" <> as Live.AmazonWebServices.USEast1.ElasticLoadBalancer - rectangle "Autoscaling group\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { - rectangle "Amazon EC2\n[Deployment Node]\n\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2 { - rectangle "==Web Application\n[Container: Java and Spring Boot]\n\nAllows employees to view and manage information regarding the veterinarians, the clients, and their pets." <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1 - } - - } - - } - -} - -Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1 .[#707070,thickness=2].> Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.Database_1 : "Reads from and writes to\n[MySQL Protocol/SSL]" -Live.AmazonWebServices.USEast1.Route53 .[#707070,thickness=2].> Live.AmazonWebServices.USEast1.ElasticLoadBalancer : "Forwards requests to\n[HTTPS]" -Live.AmazonWebServices.USEast1.ElasticLoadBalancer .[#707070,thickness=2].> Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2.WebApplication_1 : "Forwards requests to\n[HTTPS]" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml deleted file mode 100644 index 524f39cc9..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-1.puml +++ /dev/null @@ -1,56 +0,0 @@ -@startuml -set separator none -title Software System 1 - Container 1 - Components - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "Container 1\n[Container]" <> { - rectangle "==Component 1\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\n[Component]" <> as SoftwareSystem1.Container1.Component2 -} - -rectangle "Container 2\n[Container]" <> { - rectangle "==Component 3\n[Component]" <> as SoftwareSystem2.Container2.Component3 -} - -SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "Uses" -SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml deleted file mode 100644 index 4888c4988..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/component-view-with-external-components-2.puml +++ /dev/null @@ -1,62 +0,0 @@ -@startuml -set separator none -title Software System 1 - Container 1 - Components - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "Software System 1\n[Software System]" <> { - rectangle "Container 1\n[Container]" <> { - rectangle "==Component 1\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\n[Component]" <> as SoftwareSystem1.Container1.Component2 - } - - } - -rectangle "Software System 2\n[Software System]" <> { - rectangle "Container 2\n[Container]" <> { - rectangle "==Component 3\n[Component]" <> as SoftwareSystem2.Container2.Component3 - } - - } - -SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "Uses" -SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml deleted file mode 100644 index 304b863f3..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-container-scoped-with-groups.puml +++ /dev/null @@ -1,62 +0,0 @@ -@startuml -set separator none -title A - Dynamic - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "A\n[Container]" <> { - rectangle "Group 1" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==A\n[Component]" <> as A.A.A - } - -} - -rectangle "B\n[Container]" <> { - rectangle "Group 2" <> as group2 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==B\n[Component]" <> as B.B.B - } - -} - -A.A.A .[#707070,thickness=2].> B.B.B : "1. Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml deleted file mode 100644 index a430197fb..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-software-system-scoped-with-groups.puml +++ /dev/null @@ -1,62 +0,0 @@ -@startuml -set separator none -title A - Dynamic - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<
> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "A\n[Software System]" <> { - rectangle "Group 1" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==A\n[Container]" <> as A.A - } - -} - -rectangle "B\n[Software System]" <> { - rectangle "Group 2" <> as group2 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==B\n[Container]" <> as B.B - } - -} - -A.A .[#707070,thickness=2].> B.B : "1. Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml deleted file mode 100644 index 8dc424024..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-unscoped-with-groups.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startuml -set separator none -title Dynamic - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} - -rectangle "Group 1" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==A\n[Software System]" <> as A -} - -rectangle "Group 2" <> as group2 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==B\n[Software System]" <> as B -} - -A .[#707070,thickness=2].> B : "1. Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml deleted file mode 100644 index ba9a23a8a..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-1.puml +++ /dev/null @@ -1,56 +0,0 @@ -@startuml -set separator none -title Container 1 - Dynamic - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "Container 1\n[Container]" <> { - rectangle "==Component 1\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\n[Component]" <> as SoftwareSystem1.Container1.Component2 -} - -rectangle "Container 2\n[Container]" <> { - rectangle "==Component 3\n[Component]" <> as SoftwareSystem2.Container2.Component3 -} - -SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "1. Uses" -SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "2. Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml deleted file mode 100644 index 588a26fb6..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/dynamic-view-with-external-components-2.puml +++ /dev/null @@ -1,62 +0,0 @@ -@startuml -set separator none -title Container 1 - Dynamic - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "Software System 1\n[Software System]" <> { - rectangle "Container 1\n[Container]" <> { - rectangle "==Component 1\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\n[Component]" <> as SoftwareSystem1.Container1.Component2 - } - - } - -rectangle "Software System 2\n[Software System]" <> { - rectangle "Container 2\n[Container]" <> { - rectangle "==Component 3\n[Component]" <> as SoftwareSystem2.Container2.Component3 - } - - } - -SoftwareSystem1.Container1.Component1 .[#707070,thickness=2].> SoftwareSystem1.Container1.Component2 : "1. Uses" -SoftwareSystem1.Container1.Component2 .[#707070,thickness=2].> SoftwareSystem2.Container2.Component3 : "2. Uses" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml deleted file mode 100644 index 997f1738d..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-1.puml +++ /dev/null @@ -1,60 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} - -rectangle "Group 1\n\n" <> as group1 { - skinparam RectangleBorderColor<> #111111 - skinparam RectangleFontColor<> #111111 - skinparam RectangleBorderStyle<> dashed - - rectangle "==User 1\n[Person]" <> as User1 -} - -rectangle "Group 2\n\n" <> as group2 { - skinparam RectangleBorderColor<> #222222 - skinparam RectangleFontColor<> #222222 - skinparam RectangleBorderStyle<> dashed - - rectangle "==User 2\n[Person]" <> as User2 -} - -rectangle "Group 3" <> as group3 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==User 3\n[Person]" <> as User3 -} - - -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml deleted file mode 100644 index 82c0a1b75..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/group-styles-2.puml +++ /dev/null @@ -1,60 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} - -rectangle "Group 1\n\n" <> as group1 { - skinparam RectangleBorderColor<> #111111 - skinparam RectangleFontColor<> #111111 - skinparam RectangleBorderStyle<> dashed - - rectangle "==User 1\n[Person]" <> as User1 -} - -rectangle "Group 2\n\n" <> as group2 { - skinparam RectangleBorderColor<> #222222 - skinparam RectangleFontColor<> #222222 - skinparam RectangleBorderStyle<> dashed - - rectangle "==User 2\n[Person]" <> as User2 -} - -rectangle "Group 3" <> as group3 { - skinparam RectangleBorderColor<> #aabbcc - skinparam RectangleFontColor<> #aabbcc - skinparam RectangleBorderStyle<> dashed - - rectangle "==User 3\n[Person]" <> as User3 -} - - -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml deleted file mode 100644 index 3a899a68f..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Components.puml +++ /dev/null @@ -1,58 +0,0 @@ -@startuml -set separator none -title D - F - Components - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "==C\n[Software System]" <> as C - -rectangle "F\n[Container]" <> { - rectangle "Group 5" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==H\n[Component]" <> as D.F.H - } - - rectangle "==G\n[Component]" <> as D.F.G -} - -C .[#707070,thickness=2].> D.F.G : "" -C .[#707070,thickness=2].> D.F.H : "" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml deleted file mode 100644 index 6b63e7509..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-Containers.puml +++ /dev/null @@ -1,58 +0,0 @@ -@startuml -set separator none -title D - Containers - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BorderColor #9a9a9a - FontColor #9a9a9a - shadowing false -} - -rectangle "==C\n[Software System]" <> as C - -rectangle "D\n[Software System]" <> { - rectangle "Group 4" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==F\n[Container]" <> as D.F - } - - rectangle "==E\n[Container]" <> as D.E -} - -C .[#707070,thickness=2].> D.E : "" -C .[#707070,thickness=2].> D.F : "" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml deleted file mode 100644 index 09aa97ddd..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/groups-SystemLandscape.puml +++ /dev/null @@ -1,72 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction -skinparam ranksep 60 -skinparam nodesep 30 - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} - -rectangle "Group 1" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==B\n[Software System]" <> as B -} - -rectangle "Group 2" <> as group2 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==C\n[Software System]" <> as C - rectangle "Group 3" <> as group3 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==D\n[Software System]" <> as D - } - -} - -rectangle "==A\n[Software System]" <> as A - -B .[#707070,thickness=2].> C : "" -C .[#707070,thickness=2].> D : "" -A .[#707070,thickness=2].> B : "" -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml b/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml deleted file mode 100644 index 04e94c3e8..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/structurizr/nested-groups.puml +++ /dev/null @@ -1,88 +0,0 @@ -@startuml -set separator none -title System Landscape - -top to bottom direction - -skinparam { - arrowFontSize 10 - defaultTextAlignment center - wrapWidth 200 - maxMessageSize 100 -} - -hide stereotype - -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} -skinparam rectangle<> { - BackgroundColor #dddddd - FontColor #000000 - BorderColor #9a9a9a - shadowing false -} - -rectangle "Organisation 1" <> as group1 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Organisation 1\n[Software System]" <> as Organisation1 - rectangle "Department 1" <> as group2 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Department 1\n[Software System]" <> as Department1 - rectangle "Team 1" <> as group3 { - skinparam RectangleBorderColor<> #ff0000 - skinparam RectangleFontColor<> #ff0000 - skinparam RectangleBorderStyle<> dashed - - rectangle "==Team 1\n[Software System]" <> as Team1 - } - - rectangle "Team 2" <> as group4 { - skinparam RectangleBorderColor<> #0000ff - skinparam RectangleFontColor<> #0000ff - skinparam RectangleBorderStyle<> dashed - - rectangle "==Team 2\n[Software System]" <> as Team2 - } - - } - -} - -rectangle "Organisation 2" <> as group5 { - skinparam RectangleBorderColor<> #cccccc - skinparam RectangleFontColor<> #cccccc - skinparam RectangleBorderStyle<> dashed - - rectangle "==Organisation 2\n[Software System]" <> as Organisation2 -} - - -@enduml \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd deleted file mode 100644 index 240ee181c..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd +++ /dev/null @@ -1,13 +0,0 @@ -title API Application - Dynamic - SignIn - -participant <>\nSingle-Page Application as Single-Page Application -participant <>\nSign In Controller as Sign In Controller -participant <>\nSecurity Component as Security Component -participant <>\nDatabase as Database - -Single-Page Application->Sign In Controller: Submits credentials to -Sign In Controller->Security Component: Validates credentials using -Security Component->Database: select * from users where username = ? -Database-->Security Component: Returns user data to -Security Component-->Sign In Controller: Returns true if the hashed password matches -Sign In Controller-->Single-Page Application: Sends back an authentication token to \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java index e0a4c5229..25319e27f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java @@ -17,15 +17,28 @@ public class WebSequenceDiagramsExporterTests extends AbstractExporterTests { @Test public void test_BigBankPlcExample() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/structurizr-36141-workspace.json")); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/big-bank-plc.json")); WebSequenceDiagramsExporter exporter = new WebSequenceDiagramsExporter(); Collection diagrams = exporter.export(workspace); assertEquals(1, diagrams.size()); Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); - String expected = readFile(new File("./src/test/java/com/structurizr/export/websequencediagrams/36141-SignIn.wsd")); - assertEquals(expected, diagram.getDefinition()); + assertEquals(""" + title API Application - Dynamic - SignIn + + participant <>\\nSingle-Page Application as Single-Page Application + participant <>\\nSign In Controller as Sign In Controller + participant <>\\nSecurity Component as Security Component + participant <>\\nDatabase as Database + + Single-Page Application->Sign In Controller: Submits credentials to + Sign In Controller->Security Component: Validates credentials using + Security Component->Database: select * from users where username = ? + Database-->Security Component: Returns user data to + Security Component-->Sign In Controller: Returns true if the hashed password matches + Sign In Controller-->Single-Page Application: Sends back an authentication token to + """, diagram.getDefinition()); } @Test @@ -42,12 +55,14 @@ public void test_dynamicViewThatDoeNotOverrideRelationshipDescriptions() throws Collection diagrams = exporter.export(workspace); Diagram diagram = diagrams.iterator().next(); - assertEquals("title Dynamic - key\n" + - "\n" + - "participant <>\\nA as A\n" + - "participant <>\\nB as B\n" + - "\n" + - "A->B: Uses", diagram.getDefinition()); + assertEquals(""" + title Dynamic - key + + participant <>\\nA as A + participant <>\\nB as B + + A->B: Uses + """, diagram.getDefinition()); } } \ No newline at end of file diff --git a/structurizr-export/src/test/resources/amazon-web-services.dsl b/structurizr-export/src/test/resources/amazon-web-services.dsl new file mode 100644 index 000000000..316375363 --- /dev/null +++ b/structurizr-export/src/test/resources/amazon-web-services.dsl @@ -0,0 +1,83 @@ +workspace "Amazon Web Services Example" "An example AWS deployment architecture." { + + !identifiers hierarchical + + model { + x = softwaresystem "X" { + wa = container "Web Application" { + technology "Java and Spring Boot" + tags "Application" + } + db = container "Database Schema" { + tags "Database" + } + + wa -> db "Reads from and writes to" "MySQL Protocol/SSL" + } + + live = deploymentEnvironment "Live" { + deploymentNode "Amazon Web Services" { + tags "Amazon Web Services - Cloud" + + region = deploymentNode "US-East-1" { + tags "Amazon Web Services - Region" + + dns = infrastructureNode "DNS router" { + technology "Route 53" + description "Routes incoming requests based upon domain name." + tags "Amazon Web Services - Route 53" + } + + lb = infrastructureNode "Load Balancer" { + technology "Elastic Load Balancer" + description "Automatically distributes incoming application traffic." + tags "Amazon Web Services - Elastic Load Balancing" + dns -> this "Forwards requests to" "HTTPS" + } + + deploymentNode "Autoscaling group" { + tags "Amazon Web Services - Auto Scaling" + + deploymentNode "Amazon EC2 - Ubuntu server" { + tags "Amazon Web Services - EC2" + + webApplicationInstance = containerInstance x.wa { + lb -> this "Forwards requests to" "HTTPS" + } + } + } + + deploymentNode "Amazon RDS" { + tags "Amazon Web Services - RDS" + + deploymentNode "MySQL" { + tags "Amazon Web Services - RDS MySQL instance" + + databaseInstance = containerInstance x.db + } + } + + } + } + } + } + + views { + deployment x live "AmazonWebServicesDeployment" { + include * + autolayout lr + } + + styles { + element "Application" { + shape roundedbox + } + element "Database" { + shape cylinder + } + } + + themes https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json + } + +} \ No newline at end of file diff --git a/structurizr-export/src/test/resources/amazon-web-services.json b/structurizr-export/src/test/resources/amazon-web-services.json new file mode 100644 index 000000000..5214dea5f --- /dev/null +++ b/structurizr-export/src/test/resources/amazon-web-services.json @@ -0,0 +1,257 @@ +{ + "configuration" : { }, + "description" : "An example AWS deployment architecture.", + "documentation" : { }, + "id" : 0, + "model" : { + "deploymentNodes" : [ { + "children" : [ { + "children" : [ { + "children" : [ { + "containerInstances" : [ { + "containerId" : "2", + "deploymentGroups" : [ "Default" ], + "environment" : "Live", + "id" : "12", + "instanceId" : 1, + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.d846537e-73a4-40fa-b64a-0abf118a5241.53a89646-c975-4c46-a3d5-422634503b7e.webapplicationinstance" + }, + "relationships" : [ { + "description" : "Reads from and writes to", + "destinationId" : "16", + "id" : "17", + "linkedRelationshipId" : "4", + "sourceId" : "12", + "technology" : "MySQL Protocol/SSL" + } ], + "tags" : "Container Instance" + } ], + "environment" : "Live", + "id" : "11", + "instances" : "1", + "name" : "Amazon EC2 - Ubuntu server", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.d846537e-73a4-40fa-b64a-0abf118a5241.53a89646-c975-4c46-a3d5-422634503b7e" + }, + "tags" : "Element,Deployment Node,Amazon Web Services - EC2" + } ], + "environment" : "Live", + "id" : "10", + "instances" : "1", + "name" : "Autoscaling group", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.d846537e-73a4-40fa-b64a-0abf118a5241" + }, + "tags" : "Element,Deployment Node,Amazon Web Services - Auto Scaling" + }, { + "children" : [ { + "containerInstances" : [ { + "containerId" : "3", + "deploymentGroups" : [ "Default" ], + "environment" : "Live", + "id" : "16", + "instanceId" : 1, + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.989c5763-5a99-4014-9a7e-397dbc6f755e.91b9b9b5-2ca3-4bcb-8395-9d9f4d49de40.databaseinstance" + }, + "tags" : "Container Instance" + } ], + "environment" : "Live", + "id" : "15", + "instances" : "1", + "name" : "MySQL", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.989c5763-5a99-4014-9a7e-397dbc6f755e.91b9b9b5-2ca3-4bcb-8395-9d9f4d49de40" + }, + "tags" : "Element,Deployment Node,Amazon Web Services - RDS MySQL instance" + } ], + "environment" : "Live", + "id" : "14", + "instances" : "1", + "name" : "Amazon RDS", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.989c5763-5a99-4014-9a7e-397dbc6f755e" + }, + "tags" : "Element,Deployment Node,Amazon Web Services - RDS" + } ], + "environment" : "Live", + "id" : "6", + "infrastructureNodes" : [ { + "description" : "Routes incoming requests based upon domain name.", + "environment" : "Live", + "id" : "7", + "name" : "DNS router", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.dns" + }, + "relationships" : [ { + "description" : "Forwards requests to", + "destinationId" : "8", + "id" : "9", + "sourceId" : "7", + "tags" : "Relationship", + "technology" : "HTTPS" + } ], + "tags" : "Element,Infrastructure Node,Amazon Web Services - Route 53", + "technology" : "Route 53" + }, { + "description" : "Automatically distributes incoming application traffic.", + "environment" : "Live", + "id" : "8", + "name" : "Load Balancer", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region.lb" + }, + "relationships" : [ { + "description" : "Forwards requests to", + "destinationId" : "12", + "id" : "13", + "sourceId" : "8", + "tags" : "Relationship", + "technology" : "HTTPS" + } ], + "tags" : "Element,Infrastructure Node,Amazon Web Services - Elastic Load Balancing", + "technology" : "Elastic Load Balancer" + } ], + "instances" : "1", + "name" : "US-East-1", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4.region" + }, + "tags" : "Element,Deployment Node,Amazon Web Services - Region" + } ], + "environment" : "Live", + "id" : "5", + "instances" : "1", + "name" : "Amazon Web Services", + "properties" : { + "structurizr.dsl.identifier" : "live.4955ad68-6fbf-42a0-a268-21d790224ef4" + }, + "tags" : "Element,Deployment Node,Amazon Web Services - Cloud" + } ], + "softwareSystems" : [ { + "containers" : [ { + "documentation" : { }, + "id" : "2", + "name" : "Web Application", + "properties" : { + "structurizr.dsl.identifier" : "x.wa" + }, + "relationships" : [ { + "description" : "Reads from and writes to", + "destinationId" : "3", + "id" : "4", + "sourceId" : "2", + "tags" : "Relationship", + "technology" : "MySQL Protocol/SSL" + } ], + "tags" : "Element,Container,Application", + "technology" : "Java and Spring Boot" + }, { + "documentation" : { }, + "id" : "3", + "name" : "Database Schema", + "properties" : { + "structurizr.dsl.identifier" : "x.db" + }, + "tags" : "Element,Container,Database" + } ], + "documentation" : { }, + "id" : "1", + "location" : "Unspecified", + "name" : "X", + "properties" : { + "structurizr.dsl.identifier" : "x" + }, + "tags" : "Element,Software System" + } ] + }, + "name" : "Amazon Web Services Example", + "properties" : { + "structurizr.inspection.error" : "23", + "structurizr.dsl" : "d29ya3NwYWNlICJBbWF6b24gV2ViIFNlcnZpY2VzIEV4YW1wbGUiICJBbiBleGFtcGxlIEFXUyBkZXBsb3ltZW50IGFyY2hpdGVjdHVyZS4iIHsKCiAgICAhaWRlbnRpZmllcnMgaGllcmFyY2hpY2FsCgogICAgbW9kZWwgewogICAgICAgIHggPSBzb2Z0d2FyZXN5c3RlbSAiWCIgewogICAgICAgICAgICB3YSA9IGNvbnRhaW5lciAiV2ViIEFwcGxpY2F0aW9uIiB7CiAgICAgICAgICAgICAgICB0ZWNobm9sb2d5ICJKYXZhIGFuZCBTcHJpbmcgQm9vdCIKICAgICAgICAgICAgICAgIHRhZ3MgIkFwcGxpY2F0aW9uIgogICAgICAgICAgICB9CiAgICAgICAgICAgIGRiID0gY29udGFpbmVyICJEYXRhYmFzZSBTY2hlbWEiIHsKICAgICAgICAgICAgICAgIHRhZ3MgIkRhdGFiYXNlIgogICAgICAgICAgICB9CgogICAgICAgICAgICB3YSAtPiBkYiAiUmVhZHMgZnJvbSBhbmQgd3JpdGVzIHRvIiAiTXlTUUwgUHJvdG9jb2wvU1NMIgogICAgICAgIH0KCiAgICAgICAgbGl2ZSA9IGRlcGxveW1lbnRFbnZpcm9ubWVudCAiTGl2ZSIgewogICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQW1hem9uIFdlYiBTZXJ2aWNlcyIgewogICAgICAgICAgICAgICAgdGFncyAiQW1hem9uIFdlYiBTZXJ2aWNlcyAtIENsb3VkIgoKICAgICAgICAgICAgICAgIHJlZ2lvbiA9IGRlcGxveW1lbnROb2RlICJVUy1FYXN0LTEiIHsKICAgICAgICAgICAgICAgICAgICB0YWdzICJBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9uIgoKICAgICAgICAgICAgICAgICAgICBkbnMgPSBpbmZyYXN0cnVjdHVyZU5vZGUgIkROUyByb3V0ZXIiIHsKICAgICAgICAgICAgICAgICAgICAgICAgdGVjaG5vbG9neSAiUm91dGUgNTMiCiAgICAgICAgICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uICJSb3V0ZXMgaW5jb21pbmcgcmVxdWVzdHMgYmFzZWQgdXBvbiBkb21haW4gbmFtZS4iCiAgICAgICAgICAgICAgICAgICAgICAgIHRhZ3MgIkFtYXpvbiBXZWIgU2VydmljZXMgLSBSb3V0ZSA1MyIKICAgICAgICAgICAgICAgICAgICB9CgogICAgICAgICAgICAgICAgICAgIGxiID0gaW5mcmFzdHJ1Y3R1cmVOb2RlICJMb2FkIEJhbGFuY2VyIiB7CiAgICAgICAgICAgICAgICAgICAgICAgIHRlY2hub2xvZ3kgIkVsYXN0aWMgTG9hZCBCYWxhbmNlciIKICAgICAgICAgICAgICAgICAgICAgICAgZGVzY3JpcHRpb24gIkF1dG9tYXRpY2FsbHkgZGlzdHJpYnV0ZXMgaW5jb21pbmcgYXBwbGljYXRpb24gdHJhZmZpYy4iCiAgICAgICAgICAgICAgICAgICAgICAgIHRhZ3MgIkFtYXpvbiBXZWIgU2VydmljZXMgLSBFbGFzdGljIExvYWQgQmFsYW5jaW5nIgogICAgICAgICAgICAgICAgICAgICAgICBkbnMgLT4gdGhpcyAiRm9yd2FyZHMgcmVxdWVzdHMgdG8iICJIVFRQUyIKICAgICAgICAgICAgICAgICAgICB9CgogICAgICAgICAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJBdXRvc2NhbGluZyBncm91cCIgewogICAgICAgICAgICAgICAgICAgICAgICB0YWdzICJBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5nIgoKICAgICAgICAgICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkFtYXpvbiBFQzIgLSBVYnVudHUgc2VydmVyIiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0YWdzICJBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMyIgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdlYkFwcGxpY2F0aW9uSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSB4LndhIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsYiAtPiB0aGlzICJGb3J3YXJkcyByZXF1ZXN0cyB0byIgIkhUVFBTIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQW1hem9uIFJEUyIgewogICAgICAgICAgICAgICAgICAgICAgICB0YWdzICJBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIgoKICAgICAgICAgICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIk15U1FMIiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0YWdzICJBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNlIgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGFiYXNlSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSB4LmRiCiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICB9CgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfQoKICAgIHZpZXdzIHsKICAgICAgICBkZXBsb3ltZW50IHggbGl2ZSAiQW1hem9uV2ViU2VydmljZXNEZXBsb3ltZW50IiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhdXRvbGF5b3V0IGxyCiAgICAgICAgfQoKICAgICAgICBzdHlsZXMgewogICAgICAgICAgICBlbGVtZW50ICJBcHBsaWNhdGlvbiIgewogICAgICAgICAgICAgICAgc2hhcGUgcm91bmRlZGJveAogICAgICAgICAgICB9CiAgICAgICAgICAgIGVsZW1lbnQgIkRhdGFiYXNlIiB7CiAgICAgICAgICAgICAgICBzaGFwZSBjeWxpbmRlcgogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICB0aGVtZXMgaHR0cHM6Ly9zdGF0aWMuc3RydWN0dXJpenIuY29tL3RoZW1lcy9hbWF6b24td2ViLXNlcnZpY2VzLTIwMjAuMDQuMzAvdGhlbWUuanNvbgogICAgfQoKfQ==", + "structurizr.inspection.info" : "0", + "structurizr.inspection.ignore" : "0", + "structurizr.inspection.warning" : "0" + }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { + "elements" : [ { + "shape" : "RoundedBox", + "tag" : "Application" + }, { + "shape" : "Cylinder", + "tag" : "Database" + } ] + }, + "terminology" : { }, + "themes" : [ "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json" ] + }, + "deploymentViews" : [ { + "automaticLayout" : { + "applied" : false, + "edgeSeparation" : 0, + "implementation" : "Graphviz", + "nodeSeparation" : 300, + "rankDirection" : "LeftRight", + "rankSeparation" : 300, + "vertices" : false + }, + "elements" : [ { + "id" : "5", + "x" : 0, + "y" : 0 + }, { + "id" : "6", + "x" : 0, + "y" : 0 + }, { + "id" : "7", + "x" : 0, + "y" : 0 + }, { + "id" : "8", + "x" : 0, + "y" : 0 + }, { + "id" : "10", + "x" : 0, + "y" : 0 + }, { + "id" : "11", + "x" : 0, + "y" : 0 + }, { + "id" : "12", + "x" : 0, + "y" : 0 + }, { + "id" : "14", + "x" : 0, + "y" : 0 + }, { + "id" : "15", + "x" : 0, + "y" : 0 + }, { + "id" : "16", + "x" : 0, + "y" : 0 + } ], + "environment" : "Live", + "key" : "AmazonWebServicesDeployment", + "order" : 1, + "relationships" : [ { + "id" : "13" + }, { + "id" : "17" + }, { + "id" : "9" + } ], + "softwareSystemId" : "1" + } ] + } +} \ No newline at end of file diff --git a/structurizr-export/src/test/resources/structurizr-36141-workspace.json b/structurizr-export/src/test/resources/big-bank-plc.json similarity index 100% rename from structurizr-export/src/test/resources/structurizr-36141-workspace.json rename to structurizr-export/src/test/resources/big-bank-plc.json diff --git a/structurizr-export/src/test/resources/structurizr-54915-workspace.json b/structurizr-export/src/test/resources/structurizr-54915-workspace.json deleted file mode 100644 index 28e62ac94..000000000 --- a/structurizr-export/src/test/resources/structurizr-54915-workspace.json +++ /dev/null @@ -1,353 +0,0 @@ -{ - "id": 54915, - "name": "Amazon Web Services Example", - "description": "An example AWS deployment architecture.", - "model": { - "softwareSystems": [ - { - "id": "1", - "tags": "Element,Software System", - "name": "Spring PetClinic", - "description": "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", - "location": "Unspecified", - "containers": [ - { - "id": "3", - "tags": "Element,Container,Database", - "name": "Database", - "description": "Stores information regarding the veterinarians, the clients, and their pets.", - "technology": "Relational database schema" - }, - { - "id": "2", - "tags": "Element,Container,Application", - "name": "Web Application", - "description": "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", - "relationships": [ - { - "id": "4", - "tags": "Relationship", - "sourceId": "2", - "destinationId": "3", - "description": "Reads from and writes to", - "technology": "MySQL Protocol/SSL" - } - ], - "technology": "Java and Spring Boot" - } - ], - "documentation": {} - } - ], - "deploymentNodes": [ - { - "id": "5", - "tags": "Element,Deployment Node,Amazon Web Services - Cloud", - "name": "Amazon Web Services", - "environment": "Live", - "instances": 1, - "children": [ - { - "id": "6", - "tags": "Element,Deployment Node,Amazon Web Services - Region", - "name": "US-East-1", - "environment": "Live", - "instances": 1, - "children": [ - { - "id": "12", - "tags": "Element,Deployment Node,Amazon Web Services - RDS", - "name": "Amazon RDS", - "environment": "Live", - "instances": 1, - "children": [ - { - "id": "13", - "tags": "Element,Deployment Node,Amazon Web Services - RDS MySQL instance", - "name": "MySQL", - "environment": "Live", - "instances": 1, - "containerInstances": [ - { - "id": "14", - "tags": "Container Instance", - "environment": "Live", - "deploymentGroups": [ - "Default" - ], - "instanceId": 1, - "containerId": "3" - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - }, - { - "id": "9", - "tags": "Element,Deployment Node,Amazon Web Services - Auto Scaling", - "name": "Autoscaling group", - "environment": "Live", - "instances": 1, - "children": [ - { - "id": "10", - "tags": "Element,Deployment Node,Amazon Web Services - EC2", - "name": "Amazon EC2", - "environment": "Live", - "instances": 1, - "containerInstances": [ - { - "id": "11", - "tags": "Container Instance", - "relationships": [ - { - "id": "15", - "sourceId": "11", - "destinationId": "14", - "description": "Reads from and writes to", - "technology": "MySQL Protocol/SSL", - "linkedRelationshipId": "4" - } - ], - "environment": "Live", - "deploymentGroups": [ - "Default" - ], - "instanceId": 1, - "containerId": "2" - } - ], - "children": [], - "softwareSystemInstances": [], - "infrastructureNodes": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - } - ], - "infrastructureNodes": [ - { - "id": "8", - "tags": "Element,Infrastructure Node,Amazon Web Services - Elastic Load Balancing", - "name": "Elastic Load Balancer", - "description": "Automatically distributes incoming application traffic.", - "relationships": [ - { - "id": "17", - "tags": "Relationship", - "sourceId": "8", - "destinationId": "11", - "description": "Forwards requests to", - "technology": "HTTPS" - } - ], - "environment": "Live" - }, - { - "id": "7", - "tags": "Element,Infrastructure Node,Amazon Web Services - Route 53", - "name": "Route 53", - "description": "Highly available and scalable cloud DNS service.", - "relationships": [ - { - "id": "16", - "tags": "Relationship", - "sourceId": "7", - "destinationId": "8", - "description": "Forwards requests to", - "technology": "HTTPS" - } - ], - "environment": "Live" - } - ], - "softwareSystemInstances": [], - "containerInstances": [] - } - ], - "softwareSystemInstances": [], - "containerInstances": [], - "infrastructureNodes": [] - } - ], - "customElements": [], - "people": [] - }, - "documentation": { - "sections": [], - "decisions": [], - "images": [] - }, - "views": { - "deploymentViews": [ - { - "softwareSystemId": "1", - "key": "AmazonWebServicesDeployment", - "order": 1, - "paperSize": "A3_Landscape", - "dimensions": { - "width": 3925, - "height": 1816 - }, - "automaticLayout": { - "implementation": "Graphviz", - "rankDirection": "LeftRight", - "rankSeparation": 300, - "nodeSeparation": 300, - "edgeSeparation": 0, - "vertices": false - }, - "environment": "Live", - "animations": [ - { - "order": 1, - "elements": [ - "5", - "6", - "7" - ] - }, - { - "order": 2, - "elements": [ - "8" - ], - "relationships": [ - "16" - ] - }, - { - "order": 3, - "elements": [ - "11", - "9", - "10" - ], - "relationships": [ - "17" - ] - }, - { - "order": 4, - "elements": [ - "12", - "13", - "14" - ], - "relationships": [ - "15" - ] - } - ], - "elements": [ - { - "id": "11", - "x": 1987, - "y": 672 - }, - { - "id": "12", - "x": 175, - "y": 175 - }, - { - "id": "13", - "x": 175, - "y": 175 - }, - { - "id": "14", - "x": 2887, - "y": 672 - }, - { - "id": "5", - "x": 175, - "y": 175 - }, - { - "id": "6", - "x": 175, - "y": 175 - }, - { - "id": "7", - "x": 487, - "y": 672 - }, - { - "id": "8", - "x": 1237, - "y": 672 - }, - { - "id": "9", - "x": 175, - "y": 175 - }, - { - "id": "10", - "x": 175, - "y": 175 - } - ], - "relationships": [ - { - "id": "17" - }, - { - "id": "16" - }, - { - "id": "15" - } - ] - } - ], - "configuration": { - "branding": {}, - "styles": { - "elements": [ - { - "tag": "Element", - "background": "#ffffff", - "shape": "RoundedBox" - }, - { - "tag": "Container", - "background": "#ffffff" - }, - { - "tag": "Application", - "background": "#ffffff" - }, - { - "tag": "Database", - "shape": "Cylinder" - } - ], - "relationships": [] - }, - "themes": [ - "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json" - ], - "terminology": {}, - "lastSavedView": "AmazonWebServicesDeployment" - }, - "customViews": [], - "systemLandscapeViews": [], - "systemContextViews": [], - "containerViews": [], - "componentViews": [], - "dynamicViews": [], - "filteredViews": [] - } -} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java index 24176d02b..b7ae51afe 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -59,10 +59,27 @@ public void importDiagram(ImageView view, String content) throws Exception { String[] lines = content.split(NEWLINE); for (String line : lines) { if (line.startsWith(TITLE_STRING)) { - String title = line.substring(TITLE_STRING.length()); - view.setTitle(title); + view.setTitle(extractTitle(line)); } } } + private String extractTitle(String line) { + String title = line.substring(TITLE_STRING.length()); + + if (title.contains(NEWLINE)) { + title = title.split(NEWLINE)[0]; + } + + if (title.startsWith("") + 1); + + if (title.endsWith("")) { + title = title.substring(0, title.indexOf("")); + } + } + + return title; + } + } \ No newline at end of file diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java index 0de289d83..7ca514921 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java @@ -63,7 +63,8 @@ public void importDiagram_AsInlinePNG() throws Exception { ImageView view = workspace.getViews().createImageView("key"); new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); - assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("Sequence diagram example", view.getTitle()); + assertEquals("data:image/png;base64,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", view.getContent()); assertEquals("image/png", view.getContentType()); } From 1e7a9c29bba430b51a79fa986c1ea4df6316d1d1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 17 Sep 2025 10:49:15 +0100 Subject: [PATCH 377/418] Adds support for filtered deployment views (https://github.com/structurizr/java/issues/409). --- changelog.md | 1 + .../com/structurizr/view/FilteredView.java | 6 ++--- .../java/com/structurizr/view/ViewSet.java | 24 ++++++++++++++++--- .../com/structurizr/view/ViewSetTests.java | 18 ++++++++++++-- .../structurizr/dsl/FilteredViewParser.java | 22 ++++++++--------- .../dsl/FilteredViewParserTests.java | 6 ++--- 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/changelog.md b/changelog.md index 56f85766d..91d9b037a 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## v5.0.0 (unreleased) - structurizr-core: Removes support for deprecated enterprise and location concepts. +- structurizr-core: Adds support for filtered deployment views (https://github.com/structurizr/java/issues/409). - structurizr-component: Fixes https://github.com/structurizr/java/issues/437 (Make ComponentFinder.run() not fail on empty Set). - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). - structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. diff --git a/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java b/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java index 084090afe..256bc3763 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java @@ -11,7 +11,7 @@ */ public final class FilteredView extends View { - private StaticView view; + private ModelView view; private String baseViewKey; private FilterMode mode = FilterMode.Exclude; @@ -20,7 +20,7 @@ public final class FilteredView extends View { FilteredView() { } - FilteredView(StaticView view, String key, String description, FilterMode mode, String... tags) { + FilteredView(ModelView view, String key, String description, FilterMode mode, String... tags) { this.view = view; setKey(key); setDescription(description); @@ -33,7 +33,7 @@ public View getView() { return view; } - void setView(StaticView view) { + void setView(ModelView view) { this.view = view; } diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java index f597f4027..ddb5241a2 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -498,6 +498,24 @@ public FilteredView createFilteredView(StaticView view, String key, FilterMode m * @return a FilteredView object */ public FilteredView createFilteredView(StaticView view, String key, String description, FilterMode mode, String... tags) { + return newFilteredView(view, key, description, mode, tags); + } + + /** + * Creates a FilteredView on top of an existing deployment view. + * + * @param view the deployment view to base the FilteredView upon + * @param key the key for the filtered view (must be unique) + * @param description a description + * @param mode whether to Include or Exclude elements/relationships based upon their tag + * @param tags the tags to include or exclude + * @return a FilteredView object + */ + public FilteredView createFilteredView(DeploymentView view, String key, String description, FilterMode mode, String... tags) { + return newFilteredView(view, key, description, mode, tags); + } + + private FilteredView newFilteredView(ModelView view, String key, String description, FilterMode mode, String... tags) { boolean keyIsAutomaticallyGenerated = false; if (StringUtils.isNullOrEmpty(key)) { @@ -900,11 +918,11 @@ void hydrate(Model model) { ); } - if (view instanceof StaticView) { - filteredView.setView((StaticView)view); + if (view instanceof StaticView || view instanceof DeploymentView) { + filteredView.setView((ModelView)view); } else { throw new WorkspaceValidationException( - String.format("The filtered view with key \"%s\" is based upon a view (key=%s), but that view is not a static view.", + String.format("The filtered view with key \"%s\" is based upon a view (key=%s), but that view is not a static or deployment view.", filteredView.getKey(), filteredView.getBaseViewKey()) ); } diff --git a/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java index 94591bb7e..d862adf33 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java @@ -491,7 +491,7 @@ void createDeploymentViewForSoftwareSystem() { void createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() { try { Workspace workspace = new Workspace("Name", "Description"); - workspace.getViews().createFilteredView(null, "key", "Description", FilterMode.Include, "tag1", "tag2"); + workspace.getViews().createFilteredView((SystemLandscapeView)null, "key", "Description", FilterMode.Include, "tag1", "tag2"); fail(); } catch (IllegalArgumentException iae) { assertEquals("A view must be specified.", iae.getMessage()); @@ -530,7 +530,7 @@ void createFilteredView_ThrowsAnException_WhenADuplicateKeyIsUsed() { } @Test - void createFilteredView() { + void createFilteredView_OnStaticView() { Workspace workspace = new Workspace("Name", "Description"); SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); FilteredView filteredView = workspace.getViews().createFilteredView(view, "key", "Description", FilterMode.Include, "tag1", "tag2"); @@ -543,6 +543,20 @@ void createFilteredView() { assertTrue(filteredView.getTags().contains("tag2")); } + @Test + void createFilteredView_OnDeploymentView() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentView view = workspace.getViews().createDeploymentView("deployment"); + FilteredView filteredView = workspace.getViews().createFilteredView(view, "key", "Description", FilterMode.Include, "tag1", "tag2"); + + assertEquals("key", filteredView.getKey()); + assertEquals("Description", filteredView.getDescription()); + assertEquals(FilterMode.Include, filteredView.getMode()); + assertEquals(2, filteredView.getTags().size()); + assertTrue(filteredView.getTags().contains("tag1")); + assertTrue(filteredView.getTags().contains("tag2")); + } + @Test void copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesMatch() { Workspace workspace1 = createWorkspace(); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java index 1c45cea8a..8f03f9b4c 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java @@ -2,9 +2,7 @@ import com.structurizr.Workspace; import com.structurizr.util.StringUtils; -import com.structurizr.view.FilterMode; -import com.structurizr.view.FilteredView; -import com.structurizr.view.StaticView; +import com.structurizr.view.*; import java.text.DecimalFormat; import java.util.HashSet; @@ -37,7 +35,7 @@ FilteredView parse(DslContext context, Tokens tokens) { Workspace workspace = context.getWorkspace(); String key = ""; - StaticView baseView; + View baseView; String baseKey = tokens.get(BASE_KEY_INDEX); String mode = tokens.get(MODE_INDEX); String tagsAsString = tokens.get(TAGS_INDEX); @@ -64,13 +62,9 @@ FilteredView parse(DslContext context, Tokens tokens) { throw new RuntimeException("Filter mode should be include or exclude"); } - if (workspace.getViews().getViews().stream().noneMatch(v -> v.getKey().equals(baseKey))) { - throw new RuntimeException("The view \"" + baseKey + "\" does not exist"); - } - - baseView = (StaticView)workspace.getViews().getViews().stream().filter(v -> v instanceof StaticView && v.getKey().equals(baseKey)).findFirst().orElse(null); + baseView = workspace.getViews().getViewWithKey(baseKey); if (baseView == null) { - throw new RuntimeException("The view \"" + baseKey + "\" must be a System Landscape, System Context, Container, or Component view"); + throw new RuntimeException("The view \"" + baseKey + "\" does not exist"); } if (tokens.includes(KEY_INDEX)) { @@ -78,7 +72,13 @@ FilteredView parse(DslContext context, Tokens tokens) { validateViewKey(key); } - return workspace.getViews().createFilteredView(baseView, key, description, filterMode, tags.toArray(new String[0])); + if (baseView instanceof StaticView) { + return workspace.getViews().createFilteredView((StaticView)baseView, key, description, filterMode, tags.toArray(new String[0])); + } else if (baseView instanceof DeploymentView) { + return workspace.getViews().createFilteredView((DeploymentView)baseView, key, description, filterMode, tags.toArray(new String[0])); + } else { + throw new RuntimeException("The view \"" + baseKey + "\" must be a System Landscape, System Context, Container, Component, or Deployment view"); + } } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java index 5e5c5699c..c34afec00 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FilteredViewParserTests.java @@ -80,14 +80,14 @@ void test_parse_ThrowsAnException_WhenTheBaseViewDoesNotExist() { } @Test - void test_parse_ThrowsAnException_WhenTheBaseViewIsNotAStaticView() { + void test_parse_ThrowsAnException_WhenTheBaseViewIsNotAStaticOrDeploymentView() { DslContext context = context(); - views.createDeploymentView("baseKey", "Description"); + views.createDynamicView("baseKey", "Description"); try { parser.parse(context, tokens("filtered", "baseKey", "include", "Tag 1, Tag 2", "key")); fail(); } catch (RuntimeException iae) { - assertEquals("The view \"baseKey\" must be a System Landscape, System Context, Container, or Component view", iae.getMessage()); + assertEquals("The view \"baseKey\" must be a System Landscape, System Context, Container, Component, or Deployment view", iae.getMessage()); } } From 102010a9ef793f5a47e674c3160f0e837881ba20 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 17 Sep 2025 13:56:21 +0100 Subject: [PATCH 378/418] Adds some caching when inlining PlantUML, Mermaid, and Kroki images. --- .../java/com/structurizr/util/ImageUtils.java | 76 +++++++++++++------ .../diagrams/image/ImageImporter.java | 4 +- .../diagrams/kroki/KrokiImporter.java | 4 +- .../diagrams/mermaid/MermaidImporter.java | 4 +- .../diagrams/plantuml/PlantUMLImporter.java | 4 +- 5 files changed, 60 insertions(+), 32 deletions(-) diff --git a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java index bed31efc7..e0baf42e6 100644 --- a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java +++ b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java @@ -13,6 +13,8 @@ import java.net.http.HttpResponse; import java.nio.file.Files; import java.util.Base64; +import java.util.HashMap; +import java.util.Map; /** * Some utility methods for dealing with images. @@ -28,6 +30,8 @@ public class ImageUtils { public static final String CONTENT_TYPE_IMAGE_JPG = "image/jpeg"; public static final String CONTENT_TYPE_IMAGE_SVG = "image/svg+xml"; + private static final Map imageCache = new HashMap<>(); + /** * Gets the content type of the specified file representing an image. * @@ -128,33 +132,57 @@ public static String getImageAsDataUri(File file) throws IOException { } public static String getSvgAsDataUri(@Nonnull URL url) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(url.toURI()) - .header("accept", CONTENT_TYPE_IMAGE_SVG) - .build(); - HttpClient client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - String svg = response.body(); - - return DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_SVG + ";base64," + Base64.getEncoder().encodeToString(svg.getBytes()); + return getSvgAsDataUri(url, false); + } + + public static String getSvgAsDataUri(@Nonnull URL url, boolean cache) throws Exception { + String urlAsString = url.toString(); + String dataUri = cache ? imageCache.get(urlAsString) : null; + + if (StringUtils.isNullOrEmpty(dataUri)) { + HttpRequest request = HttpRequest.newBuilder() + .uri(url.toURI()) + .header("accept", CONTENT_TYPE_IMAGE_SVG) + .build(); + HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + String svg = response.body(); + + dataUri = DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_SVG + ";base64," + Base64.getEncoder().encodeToString(svg.getBytes()); + imageCache.put(urlAsString, dataUri); + } + + return dataUri; } public static String getPngAsDataUri(@Nonnull URL url) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(url.toURI()) - .header("accept", CONTENT_TYPE_IMAGE_PNG) - .build(); - HttpClient client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); - byte[] png = response.body(); - - return DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_PNG + ";base64," + Base64.getEncoder().encodeToString(png); + return getPngAsDataUri(url, false); + } + + public static String getPngAsDataUri(@Nonnull URL url, boolean cache) throws Exception { + String urlAsString = url.toString(); + String dataUri = cache ? imageCache.get(urlAsString) : null; + + if (StringUtils.isNullOrEmpty(dataUri)) { + HttpRequest request = HttpRequest.newBuilder() + .uri(url.toURI()) + .header("accept", CONTENT_TYPE_IMAGE_PNG) + .build(); + HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + byte[] png = response.body(); + + dataUri = DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_PNG + ";base64," + Base64.getEncoder().encodeToString(png); + imageCache.put(urlAsString, dataUri); + } + + return dataUri; } public static void validateImage(String imageDescriptor) { diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java index dbdf10b60..6708f0f06 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java @@ -28,9 +28,9 @@ public void importDiagram(ImageView view, String url) throws Exception { } if (imageFormat.equals(CONTENT_TYPE_IMAGE_SVG)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), false)); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), false)); } view.setContentType(imageFormat); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java index 5277bd99b..67b3d4413 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java @@ -44,9 +44,9 @@ public void importDiagram(ImageView view, String format, String content) throws String inline = getViewOrViewSetProperty(view, KROKI_INLINE_PROPERTY); if ("true".equals(inline)) { if (imageFormat.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true)); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true)); } } else { view.setContent(url); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java index 2be38290c..ab1f1a00c 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java @@ -55,9 +55,9 @@ public void importDiagram(ImageView view, String content) throws Exception { String inline = getViewOrViewSetProperty(view, MERMAID_INLINE_PROPERTY); if ("true".equals(inline)) { if (format.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true)); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true)); } } else { view.setContent(url); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java index b7ae51afe..28ecf1c3e 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -47,9 +47,9 @@ public void importDiagram(ImageView view, String content) throws Exception { String inline = getViewOrViewSetProperty(view, PLANTUML_INLINE_PROPERTY); if ("true".equals(inline)) { if (format.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url))); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true)); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url))); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true)); } } else { view.setContent(url); From f52092dfaabb684f36a3671db70b489f794291d0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 18 Sep 2025 12:06:08 +0100 Subject: [PATCH 379/418] Modifies how view names are generated to make them all consistent. --- .../com/structurizr/view/ComponentView.java | 2 +- .../com/structurizr/view/ContainerView.java | 2 +- .../java/com/structurizr/view/CustomView.java | 2 +- .../com/structurizr/view/DeploymentView.java | 8 +- .../com/structurizr/view/DynamicView.java | 9 +- .../structurizr/view/SystemContextView.java | 2 +- .../structurizr/view/SystemLandscapeView.java | 2 +- .../main/java/com/structurizr/view/View.java | 1 - .../structurizr/view/ComponentViewTests.java | 2 +- .../structurizr/view/ContainerViewTests.java | 2 +- .../structurizr/view/DeploymentViewTests.java | 8 +- .../structurizr/view/DynamicViewTests.java | 7 + .../view/SystemContextViewTests.java | 2 +- .../view/SystemLandscapeViewTests.java | 3 +- .../java/com/structurizr/view/ViewTests.java | 7 - .../export/ilograph/IlographExporter.java | 7 +- .../plantuml/StructurizrPlantUMLExporter.java | 2 +- .../WebSequenceDiagramsExporter.java | 6 +- .../export/dot/DOTDiagramExporterTests.java | 125 +++++++++--------- .../ilograph/IlographExporterTests.java | 2 +- .../export/mermaid/36141-Components.mmd | 48 ------- .../export/mermaid/36141-Containers.mmd | 39 ------ .../mermaid/36141-DevelopmentDeployment.mmd | 61 --------- .../export/mermaid/36141-LiveDeployment.mmd | 92 ------------- .../export/mermaid/36141-SignIn-sequence.mmd | 13 -- .../export/mermaid/36141-SignIn.mmd | 27 ---- .../export/mermaid/36141-SystemContext.mmd | 25 ---- .../export/mermaid/36141-SystemLandscape.mmd | 36 ----- .../mermaid/MermaidDiagramExporterTests.java | 22 +-- .../C4PlantUMLDiagramExporterTests.java | 72 +++++----- ...ructurizrPlantUMLDiagramExporterTests.java | 72 +++++----- .../WebSequenceDiagramsExporterTests.java | 4 +- 32 files changed, 192 insertions(+), 520 deletions(-) delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd delete mode 100644 structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd diff --git a/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java index b543dc876..c9168cee6 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java @@ -148,7 +148,7 @@ public void remove(Component component) { */ @Override public String getName() { - return getSoftwareSystem().getName() + " - " + getContainer().getName() + " - Components"; + return "Component View: " + getSoftwareSystem().getName() + " - " + getContainer().getName(); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java index 664164974..a5b3e4bf0 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java @@ -77,7 +77,7 @@ public void remove(Container container) { */ @Override public String getName() { - return getSoftwareSystem().getName() + " - Containers"; + return "Container View: " + getSoftwareSystem().getName(); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/CustomView.java b/structurizr-core/src/main/java/com/structurizr/view/CustomView.java index 81558f604..35469259a 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/CustomView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/CustomView.java @@ -40,7 +40,7 @@ public final class CustomView extends ModelView implements AnimatedView { */ @Override public String getName() { - return "Custom - " + getTitle(); + return "Custom View: " + getTitle(); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java b/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java index e38113e9a..444bb2250 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java @@ -248,13 +248,9 @@ public RelationshipView add(@Nonnull Relationship relationship) { public String getName() { String name; if (getSoftwareSystem() != null) { - name = getSoftwareSystem().getName() + " - Deployment"; + name = "Deployment View: " + getSoftwareSystem().getName() + " - " + getEnvironment(); } else { - name = "Deployment"; - } - - if (!StringUtils.isNullOrEmpty(getEnvironment())) { - name = name + " - " + getEnvironment(); + name = "Deployment View: " + getEnvironment(); } return name; diff --git a/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java b/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java index 04d0fbdfb..5f206caca 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java @@ -254,9 +254,14 @@ private RelationshipView addRelationship(Relationship relationship, String descr @Override public String getName() { if (element != null) { - return element.getName() + " - Dynamic"; + if (element instanceof Container) { + Container container = (Container)element; + return "Dynamic View: " + container.getParent().getName() + " - " + container.getName(); + } else { + return "Dynamic View: " + element.getName(); + } } else { - return "Dynamic"; + return "Dynamic View"; } } diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java index 513fe02c1..3c88717da 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java @@ -31,7 +31,7 @@ public final class SystemContextView extends StaticView { */ @Override public String getName() { - return getSoftwareSystem().getName() + " - System Context"; + return "System Context View: " + getSoftwareSystem().getName(); } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java index 359245180..2284e575c 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java @@ -31,7 +31,7 @@ public final class SystemLandscapeView extends StaticView { */ @Override public String getName() { - return "System Landscape"; + return "System Landscape View"; } /** diff --git a/structurizr-core/src/main/java/com/structurizr/view/View.java b/structurizr-core/src/main/java/com/structurizr/view/View.java index f6ee5539d..54588d8cf 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/View.java +++ b/structurizr-core/src/main/java/com/structurizr/view/View.java @@ -111,7 +111,6 @@ public void setTitle(String title) { * * @return the name, as a String */ - @JsonIgnore public abstract String getName(); void setViewSet(@Nonnull ViewSet viewSet) { diff --git a/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java index db21ee52e..a1408b08b 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java @@ -25,7 +25,7 @@ public void setUp() { @Test void construction() { - assertEquals("The System - Web Application - Components", view.getName()); + assertEquals("Component View: The System - Web Application", view.getName()); assertEquals("Some description", view.getDescription()); assertEquals(0, view.getElements().size()); assertSame(softwareSystem, view.getSoftwareSystem()); diff --git a/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java index a9caf817d..7d877de13 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java @@ -20,7 +20,7 @@ public void setUp() { @Test void construction() { - assertEquals("The System - Containers", view.getName()); + assertEquals("Container View: The System", view.getName()); assertEquals("Description", view.getDescription()); assertEquals(0, view.getElements().size()); assertSame(softwareSystem, view.getSoftwareSystem()); diff --git a/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java index 0fcafacca..56b0a0038 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java @@ -18,21 +18,21 @@ public void setup() { @Test void getName_WithNoSoftwareSystemAndNoEnvironment() { deploymentView = views.createDeploymentView("deployment", "Description"); - assertEquals("Deployment - Default", deploymentView.getName()); + assertEquals("Deployment View: Default", deploymentView.getName()); } @Test void getName_WithNoSoftwareSystemAndAnEnvironment() { deploymentView = views.createDeploymentView("deployment", "Description"); deploymentView.setEnvironment("Live"); - assertEquals("Deployment - Live", deploymentView.getName()); + assertEquals("Deployment View: Live", deploymentView.getName()); } @Test void getName_WithASoftwareSystemAndNoEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); - assertEquals("Software System - Deployment - Default", deploymentView.getName()); + assertEquals("Deployment View: Software System - Default", deploymentView.getName()); } @Test @@ -40,7 +40,7 @@ void getName_WithASoftwareSystemAndAnEnvironment() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); deploymentView.setEnvironment("Live"); - assertEquals("Software System - Deployment - Live", deploymentView.getName()); + assertEquals("Deployment View: Software System - Live", deploymentView.getName()); } @Test diff --git a/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java index 029224428..bcbea49d6 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java @@ -43,6 +43,13 @@ public void setup() { containerB1 = softwareSystemB.addContainer("Container B1", "", ""); } + @Test + void name() { + assertEquals("Dynamic View", views.createDynamicView("key1").getName()); + assertEquals("Dynamic View: Software System A", views.createDynamicView(softwareSystemA, "key2").getName()); + assertEquals("Dynamic View: Software System A - Container A1", views.createDynamicView(containerA1, "key3").getName()); + } + @Test void add_ThrowsAnException_WhenPassedANullSourceElement() { try { diff --git a/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java index bc7694728..b35075f43 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java @@ -20,7 +20,7 @@ public void setUp() { @Test void construction() { - assertEquals("The System - System Context", view.getName()); + assertEquals("System Context View: The System", view.getName()); assertEquals(1, view.getElements().size()); assertSame(view.getElements().iterator().next().getElement(), softwareSystem); assertSame(softwareSystem, view.getSoftwareSystem()); diff --git a/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java index 274afed64..481c1c77c 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java @@ -18,14 +18,13 @@ public void setUp() { @Test void construction() { - assertEquals("System Landscape", view.getName()); assertEquals(0, view.getElements().size()); assertSame(model, view.getModel()); } @Test void getName() { - assertEquals("System Landscape", view.getName()); + assertEquals("System Landscape View", view.getName()); } @Test diff --git a/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java index b4cb96bcd..068c17991 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java @@ -255,13 +255,6 @@ void copyLayoutInformationFrom() { assertEquals(Routing.Direct, dynamicView2.getRelationshipView(personUsesSoftwareSystem2).getRouting()); } - @Test - void getName() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); - SystemContextView systemContextView = new SystemContextView(softwareSystem, "context", "Description"); - assertEquals("The System - System Context", systemContextView.getName()); - } - @Test void removeElementsThatAreUnreachableFrom_DoesNothing_WhenANullElementIsSpecified() { SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); diff --git a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java index 06aefe67d..9a80784e1 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java @@ -298,8 +298,13 @@ private void writeRelationshipsForStaticStructurePerspective(Configuration confi } private void writeDynamicView(DynamicView dynamicView, IndentingWriter writer) { + String scope = dynamicView.getName(); + scope = scope.substring("Dynamic View".length()); + if (scope.startsWith(": ")) { + scope = scope.substring(2); + } writer.indent(); - writer.writeLine("- name: Dynamic - " + dynamicView.getName()); + writer.writeLine("- name: Dynamic: " + scope); writer.indent(); writer.writeLine("sequence:"); diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 6dccc103c..18238eb08 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -629,7 +629,7 @@ private ElementStyle findGroupStyle(ModelView view, String group) { if (elementStyleForGroup != null && elementStyleForGroup.getFontSize() != null) { fontSize = elementStyleForGroup.getFontSize(); - } else if (elementStyleForAllGroups != null && elementStyleForGroup.getFontSize() != null) { + } else if (elementStyleForAllGroups != null && elementStyleForAllGroups.getFontSize() != null) { fontSize = elementStyleForAllGroups.getFontSize(); } style.setFontSize(fontSize); diff --git a/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java index e05832792..c7932abcc 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java @@ -75,7 +75,11 @@ public Diagram export(DeploymentView view) { @Override protected void writeHeader(ModelView view, IndentingWriter writer) { - writer.writeLine("title " + view.getName() + " - " + view.getKey()); + if (!StringUtils.isNullOrEmpty(view.getDescription())) { + writer.writeLine("title " + view.getName() + "\n" + view.getDescription()); + } else { + writer.writeLine("title " + view.getName()); + } writer.writeLine(); } diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java index 02aa78296..937af8766 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -31,7 +31,7 @@ public void test_BigBankPlcExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
System Landscape> + label=<
System Landscape View> subgraph "cluster_group_Big Bank plc" { margin=25 @@ -71,7 +71,7 @@ public void test_BigBankPlcExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
Internet Banking System - System Context
The system context diagram for the Internet Banking System.> + label=<
System Context View: Internet Banking System
The system context diagram for the Internet Banking System.> subgraph "cluster_group_Big Bank plc" { margin=25 @@ -103,7 +103,7 @@ public void test_BigBankPlcExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
Internet Banking System - Containers
The container diagram for the Internet Banking System.> + label=<
Container View: Internet Banking System
The container diagram for the Internet Banking System.> 1 [id=1,shape=rect, label=<Personal Banking
Customer

[Person]

A customer of the bank, with
personal bank accounts.
>, style=filled, color="#052e56", fillcolor="#08427b", fontcolor="#ffffff"] 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] @@ -144,7 +144,7 @@ public void test_BigBankPlcExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
Internet Banking System - API Application - Components
The component diagram for the API Application.> + label=<
Component View: Internet Banking System - API Application
The component diagram for the API Application.> 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] @@ -191,7 +191,7 @@ public void test_BigBankPlcExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
API Application - Dynamic
Summarises how the sign in feature works in the single-page application.> + label=<
Dynamic View: Internet Banking System - API Application
Summarises how the sign in feature works in the single-page application.> subgraph cluster_11 { margin=25 @@ -224,7 +224,7 @@ public void test_BigBankPlcExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
Internet Banking System - Deployment - Development
An example development deployment scenario for the Internet Banking System.> + label=<
Deployment View: Internet Banking System - Development
An example development deployment scenario for the Internet Banking System.> subgraph cluster_50 { margin=25 @@ -325,7 +325,7 @@ public void test_BigBankPlcExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
Internet Banking System - Deployment - Live
An example live deployment scenario for the Internet Banking System.> + label=<
Deployment View: Internet Banking System - Live
An example live deployment scenario for the Internet Banking System.> subgraph cluster_67 { margin=25 @@ -493,7 +493,7 @@ public void test_AmazonWebServicesExample() throws Exception { graph [fontname="Arial", rankdir=LR, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
X - Deployment - Live> + label=<
Deployment View: X - Live> subgraph cluster_5 { margin=25 @@ -582,7 +582,7 @@ public void test_GroupsExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
System Landscape> + label=<
System Landscape View> subgraph "cluster_group_Group 1" { margin=25 @@ -635,7 +635,7 @@ public void test_GroupsExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
D - Containers> + label=<
Container View: D> 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] @@ -674,7 +674,7 @@ public void test_GroupsExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
D - F - Components> + label=<
Component View: D - F> 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] @@ -740,7 +740,7 @@ public void test_NestedGroupsExample() throws Exception { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
System Landscape
Description> + label=<
System Landscape View
Description> subgraph "cluster_group_Organisation 1" { margin=25 @@ -827,7 +827,7 @@ public void test_renderContainerDiagramWithExternalContainers() { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
Software System 1 - Containers> + label=<
Container View: Software System 1> subgraph cluster_1 { margin=25 @@ -879,7 +879,7 @@ public void test_renderComponentDiagramWithExternalComponents() { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
Software System 1 - Container 1 - Components> + label=<
Component View: Software System 1 - Container 1> subgraph cluster_2 { margin=25 @@ -929,7 +929,7 @@ public void test_renderGroupStyles() { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
System Landscape> + label=<
System Landscape View> subgraph "cluster_group_Group 1" { margin=25 @@ -979,7 +979,7 @@ public void test_renderGroupStyles() { graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] node [fontname="Arial", shape=box, margin="0.4,0.3"] edge [fontname="Arial"] - label=<
System Landscape> + label=<
System Landscape View> subgraph "cluster_group_Group 1" { margin=25 @@ -1062,58 +1062,51 @@ public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSepar ContainerView view = workspace.getViews().createContainerView(softwareSystem, "Containers", ""); view.addAllElements(); - String expectedResult = "digraph {\n" + - " compound=true\n" + - " graph [fontname=\"Arial\", rankdir=TB, ranksep=1.0, nodesep=1.0]\n" + - " node [fontname=\"Arial\", shape=box, margin=\"0.4,0.3\"]\n" + - " edge [fontname=\"Arial\"]\n" + - " label=<
Software System - Containers>\n" + - "\n" + - " subgraph cluster_1 {\n" + - " margin=25\n" + - " label=<
Software System

[Software System]>\n" + - " labelloc=b\n" + - " color=\"#444444\"\n" + - " fontcolor=\"#444444\"\n" + - " fillcolor=\"#444444\"\n" + - "\n" + - " subgraph \"cluster_group_Group 1\" {\n" + - " margin=25\n" + - " label=<
Group 1
>\n" + - " labelloc=b\n" + - " color=\"#cccccc\"\n" + - " fontcolor=\"#cccccc\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 2 [id=2,shape=rect, label=<Container 1
[Container]>, style=filled, color=\"#444444\", fillcolor=\"#ffffff\", fontcolor=\"#444444\"]\n" + - " }\n" + - "\n" + - " subgraph \"cluster_group_Group 2\" {\n" + - " margin=25\n" + - " label=<
Group 2
>\n" + - " labelloc=b\n" + - " color=\"#cccccc\"\n" + - " fontcolor=\"#cccccc\"\n" + - " fillcolor=\"#ffffff\"\n" + - " style=\"dashed\"\n" + - "\n" + - " 3 [id=3,shape=rect, label=<Container 2
[Container]>, style=filled, color=\"#444444\", fillcolor=\"#ffffff\", fontcolor=\"#444444\"]\n" + - " }\n" + - "\n" + - " }\n" + - "\n" + - "}"; - DOTExporter exporter = new DOTExporter(); Diagram diagram = exporter.export(view); - assertEquals(expectedResult, diagram.getDefinition()); - - // this should be the same - workspace.getModel().addProperty("structurizr.groupSeparator", "/"); - exporter = new DOTExporter(); - diagram = exporter.export(view); - assertEquals(expectedResult, diagram.getDefinition()); + assertEquals(""" + digraph { + compound=true + graph [fontname="Arial", rankdir=TB, ranksep=1.0, nodesep=1.0] + node [fontname="Arial", shape=box, margin="0.4,0.3"] + edge [fontname="Arial"] + label=<
Container View: Software System> + + subgraph cluster_1 { + margin=25 + label=<
Software System

[Software System]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + subgraph "cluster_group_Group 1" { + margin=25 + label=<
Group 1
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<Container 1
[Container]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 2" { + margin=25 + label=<
Group 2
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 3 [id=3,shape=rect, label=<Container 2
[Container]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + }""", diagram.getDefinition()); } } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java index 596b7592c..a9057c03d 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -555,7 +555,7 @@ public void test_BigBankPlcExample() throws Exception { description: "JSON/HTTPS" color: "#444444" - - name: Dynamic - API Application - Dynamic + - name: Dynamic: Internet Banking System - API Application sequence: start: "8" steps: diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd deleted file mode 100644 index 505e7fa62..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Components.mmd +++ /dev/null @@ -1,48 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["Internet Banking System - API Application - Components"] - style diagram fill:#ffffff,stroke:#ffffff - - 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] - style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff - 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff - 9["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] - style 9 fill:#438dd5,stroke:#2e6295,color:#ffffff - 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff - - subgraph 11 ["API Application"] - style 11 fill:#ffffff,stroke:#2e6295,color:#2e6295 - - 12["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] - style 12 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 13["
Accounts Summary Controller
[Component: Spring MVC Rest Controller]
Provides customers with a
summary of their bank
accounts.
"] - style 13 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 14["
Reset Password Controller
[Component: Spring MVC Rest Controller]
Allows users to reset their
passwords with a single use
URL.
"] - style 14 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 15["
Security Component
[Component: Spring Bean]
Provides functionality
related to signing in,
changing passwords, etc.
"] - style 15 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 16["
Mainframe Banking System Facade
[Component: Spring Bean]
A facade onto the mainframe
banking system.
"] - style 16 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 17["
E-mail Component
[Component: Spring Bean]
Sends e-mails to users.
"] - style 17 fill:#85bbf0,stroke:#5d82a8,color:#000000 - end - - 8-. "
Makes API calls to
[JSON/HTTPS]
" .->12 - 8-. "
Makes API calls to
[JSON/HTTPS]
" .->13 - 8-. "
Makes API calls to
[JSON/HTTPS]
" .->14 - 9-. "
Makes API calls to
[JSON/HTTPS]
" .->12 - 9-. "
Makes API calls to
[JSON/HTTPS]
" .->13 - 9-. "
Makes API calls to
[JSON/HTTPS]
" .->14 - 12-. "
Uses
" .->15 - 13-. "
Uses
" .->16 - 14-. "
Uses
" .->15 - 14-. "
Uses
" .->17 - 15-. "
Reads from and writes to
[SQL/TCP]
" .->18 - 16-. "
Makes API calls to
[XML/HTTPS]
" .->4 - 17-. "
Sends e-mail using
" .->5 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd deleted file mode 100644 index e962db529..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-Containers.mmd +++ /dev/null @@ -1,39 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["Internet Banking System - Containers"] - style diagram fill:#ffffff,stroke:#ffffff - - 1["
Personal Banking Customer
[Person]
A customer of the bank, with
personal bank accounts.
"] - style 1 fill:#08427b,stroke:#052e56,color:#ffffff - 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] - style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff - - subgraph 7 ["Internet Banking System"] - style 7 fill:#ffffff,stroke:#0b4884,color:#0b4884 - - 10["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] - style 10 fill:#438dd5,stroke:#2e6295,color:#ffffff - 11["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] - style 11 fill:#438dd5,stroke:#2e6295,color:#ffffff - 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff - 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff - 9["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] - style 9 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - 5-. "
Sends e-mails to
" .->1 - 1-. "
Visits bigbank.com/ib using
[HTTPS]
" .->10 - 1-. "
Views account balances, and
makes payments using
" .->8 - 1-. "
Views account balances, and
makes payments using
" .->9 - 10-. "
Delivers to the customer's
web browser
" .->8 - 8-. "
Makes API calls to
[JSON/HTTPS]
" .->11 - 9-. "
Makes API calls to
[JSON/HTTPS]
" .->11 - 11-. "
Reads from and writes to
[SQL/TCP]
" .->18 - 11-. "
Makes API calls to
[XML/HTTPS]
" .->4 - 11-. "
Sends e-mail using
" .->5 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd deleted file mode 100644 index 0347c49a8..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-DevelopmentDeployment.mmd +++ /dev/null @@ -1,61 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["Internet Banking System - Deployment - Development"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph 50 ["Developer Laptop"] - style 50 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 51 ["Web Browser"] - style 51 fill:#ffffff,stroke:#888888,color:#000000 - - 52["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 52 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - subgraph 53 ["Docker Container - Web Server"] - style 53 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 54 ["Apache Tomcat"] - style 54 fill:#ffffff,stroke:#888888,color:#000000 - - 55["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] - style 55 fill:#438dd5,stroke:#2e6295,color:#ffffff - 57["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] - style 57 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - end - - subgraph 59 ["Docker Container - Database Server"] - style 59 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 60 ["Database Server"] - style 60 fill:#ffffff,stroke:#888888,color:#000000 - - 61[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 61 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - end - - end - - subgraph 63 ["Big Bank plc"] - style 63 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 64 ["bigbank-dev001"] - style 64 fill:#ffffff,stroke:#888888,color:#000000 - - 65["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 65 fill:#999999,stroke:#6b6b6b,color:#ffffff - end - - end - - 55-. "
Delivers to the customer's
web browser
" .->52 - 52-. "
Makes API calls to
[JSON/HTTPS]
" .->57 - 57-. "
Reads from and writes to
[SQL/TCP]
" .->61 - 57-. "
Makes API calls to
[XML/HTTPS]
" .->65 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd deleted file mode 100644 index e4cedb00e..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-LiveDeployment.mmd +++ /dev/null @@ -1,92 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["Internet Banking System - Deployment - Live"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph 67 ["Customer's mobile device"] - style 67 fill:#ffffff,stroke:#888888,color:#000000 - - 68["
Mobile App
[Container: Xamarin]
Provides a limited subset of
the Internet banking
functionality to customers
via their mobile device.
"] - style 68 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - subgraph 69 ["Customer's computer"] - style 69 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 70 ["Web Browser"] - style 70 fill:#ffffff,stroke:#888888,color:#000000 - - 71["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 71 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - end - - subgraph 72 ["Big Bank plc"] - style 72 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 73 ["bigbank-web***"] - style 73 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 74 ["Apache Tomcat"] - style 74 fill:#ffffff,stroke:#888888,color:#000000 - - 75["
Web Application
[Container: Java and Spring MVC]
Delivers the static content
and the Internet banking
single page application.
"] - style 75 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - end - - subgraph 77 ["bigbank-api***"] - style 77 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 78 ["Apache Tomcat"] - style 78 fill:#ffffff,stroke:#888888,color:#000000 - - 79["
API Application
[Container: Java and Spring MVC]
Provides Internet banking
functionality via a
JSON/HTTPS API.
"] - style 79 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - end - - subgraph 82 ["bigbank-db01"] - style 82 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 83 ["Oracle - Primary"] - style 83 fill:#ffffff,stroke:#888888,color:#000000 - - 84[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 84 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - end - - subgraph 86 ["bigbank-db02"] - style 86 fill:#ffffff,stroke:#888888,color:#000000 - - subgraph 87 ["Oracle - Secondary"] - style 87 fill:#ffffff,stroke:#888888,color:#000000 - - 88[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 88 fill:#438dd5,stroke:#2e6295,color:#ffffff - end - - end - - subgraph 90 ["bigbank-prod001"] - style 90 fill:#ffffff,stroke:#888888,color:#000000 - - 91["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 91 fill:#999999,stroke:#6b6b6b,color:#ffffff - end - - end - - 75-. "
Delivers to the customer's
web browser
" .->71 - 68-. "
Makes API calls to
[JSON/HTTPS]
" .->79 - 71-. "
Makes API calls to
[JSON/HTTPS]
" .->79 - 79-. "
Reads from and writes to
[SQL/TCP]
" .->84 - 79-. "
Reads from and writes to
[SQL/TCP]
" .->88 - 79-. "
Makes API calls to
[XML/HTTPS]
" .->91 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd deleted file mode 100644 index f25511e2f..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn-sequence.mmd +++ /dev/null @@ -1,13 +0,0 @@ -sequenceDiagram - - participant 8 as Single-Page Application
[Container: JavaScript and Angular] - participant 12 as Sign In Controller
[Component: Spring MVC Rest Controller] - participant 15 as Security Component
[Component: Spring Bean] - participant 18 as Database
[Container: Oracle Database Schema] - - 8->>12: Submits credentials to
[JSON/HTTPS] - 12->>15: Validates credentials using - 15->>18: select * from users where username = ?
[SQL/TCP] - 18-->>15: Returns user data to
[SQL/TCP] - 15-->>12: Returns true if the hashed password matches - 12-->>8: Sends back an authentication token to
[JSON/HTTPS] \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd deleted file mode 100644 index 378a93c7a..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SignIn.mmd +++ /dev/null @@ -1,27 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["API Application - Dynamic"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph 11 ["API Application"] - style 11 fill:#ffffff,stroke:#2e6295,color:#2e6295 - - 12["
Sign In Controller
[Component: Spring MVC Rest Controller]
Allows users to sign in to
the Internet Banking System.
"] - style 12 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 15["
Security Component
[Component: Spring Bean]
Provides functionality
related to signing in,
changing passwords, etc.
"] - style 15 fill:#85bbf0,stroke:#5d82a8,color:#000000 - end - - 8["
Single-Page Application
[Container: JavaScript and Angular]
Provides all of the Internet
banking functionality to
customers via their web
browser.
"] - style 8 fill:#438dd5,stroke:#2e6295,color:#ffffff - 18[("
Database
[Container: Oracle Database Schema]
Stores user registration
information, hashed
authentication credentials,
access logs, etc.
")] - style 18 fill:#438dd5,stroke:#2e6295,color:#ffffff - - 8-. "
1. Submits credentials to
[JSON/HTTPS]
" .->12 - 12-. "
2. Validates credentials
using
" .->15 - 15-. "
3. select * from users where
username = ?
[SQL/TCP]
" .->18 - 18-. "
4. Returns user data to
[SQL/TCP]
" .->15 - 15-. "
5. Returns true if the hashed
password matches
" .->12 - 12-. "
6. Sends back an
authentication token to
[JSON/HTTPS]
" .->8 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd deleted file mode 100644 index 1dd22f429..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemContext.mmd +++ /dev/null @@ -1,25 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["Internet Banking System - System Context"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph group1 ["Big Bank plc"] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] - style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff - 7["
Internet Banking System
[Software System]
Allows customers to view
information about their bank
accounts, and make payments.
"] - style 7 fill:#1168bd,stroke:#0b4884,color:#ffffff - end - - 1["
Personal Banking Customer
[Person]
A customer of the bank, with
personal bank accounts.
"] - style 1 fill:#08427b,stroke:#052e56,color:#ffffff - - 1-. "
Views account balances, and
makes payments using
" .->7 - 7-. "
Gets account information
from, and makes payments
using
" .->4 - 7-. "
Sends e-mail using
" .->5 - 5-. "
Sends e-mails to
" .->1 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd b/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd deleted file mode 100644 index 83023a9e9..000000000 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/36141-SystemLandscape.mmd +++ /dev/null @@ -1,36 +0,0 @@ -graph TB - linkStyle default fill:#ffffff - - subgraph diagram ["System Landscape"] - style diagram fill:#ffffff,stroke:#ffffff - - subgraph group1 ["Big Bank plc"] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - - 2["
Customer Service Staff
[Person]
Customer service staff within
the bank.
"] - style 2 fill:#999999,stroke:#6b6b6b,color:#ffffff - 3["
Back Office Staff
[Person]
Administration and support
staff within the bank.
"] - style 3 fill:#999999,stroke:#6b6b6b,color:#ffffff - 4["
Mainframe Banking System
[Software System]
Stores all of the core
banking information about
customers, accounts,
transactions, etc.
"] - style 4 fill:#999999,stroke:#6b6b6b,color:#ffffff - 5["
E-mail System
[Software System]
The internal Microsoft
Exchange e-mail system.
"] - style 5 fill:#999999,stroke:#6b6b6b,color:#ffffff - 6["
ATM
[Software System]
Allows customers to withdraw
cash.
"] - style 6 fill:#999999,stroke:#6b6b6b,color:#ffffff - 7["
Internet Banking System
[Software System]
Allows customers to view
information about their bank
accounts, and make payments.
"] - style 7 fill:#1168bd,stroke:#0b4884,color:#ffffff - end - - 1["
Personal Banking Customer
[Person]
A customer of the bank, with
personal bank accounts.
"] - style 1 fill:#08427b,stroke:#052e56,color:#ffffff - - 1-. "
Views account balances, and
makes payments using
" .->7 - 7-. "
Gets account information
from, and makes payments
using
" .->4 - 7-. "
Sends e-mail using
" .->5 - 5-. "
Sends e-mails to
" .->1 - 1-. "
Asks questions to
[Telephone]
" .->2 - 2-. "
Uses
" .->4 - 1-. "
Withdraws cash using
" .->6 - 6-. "
Uses
" .->4 - 3-. "
Uses
" .->4 - end \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java index 065b56220..fe176f043 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -33,7 +33,7 @@ public void test_AmazonWebServicesExample() throws Exception { graph LR linkStyle default fill:#ffffff - subgraph diagram ["X - Deployment - Live"] + subgraph diagram ["Deployment View: X - Live"] style diagram fill:#ffffff,stroke:#ffffff subgraph 5 ["Amazon Web Services"] @@ -95,7 +95,7 @@ public void test_GroupsExample() throws Exception { graph TB linkStyle default fill:#ffffff - subgraph diagram ["System Landscape"] + subgraph diagram ["System Landscape View"] style diagram fill:#ffffff,stroke:#ffffff subgraph group1 ["Group 1"] @@ -133,7 +133,7 @@ public void test_GroupsExample() throws Exception { graph TB linkStyle default fill:#ffffff - subgraph diagram ["D - Containers"] + subgraph diagram ["Container View: D"] style diagram fill:#ffffff,stroke:#ffffff 3["
C
[Software System]
"] @@ -163,7 +163,7 @@ public void test_GroupsExample() throws Exception { graph TB linkStyle default fill:#ffffff - subgraph diagram ["D - F - Components"] + subgraph diagram ["Component View: D - F"] style diagram fill:#ffffff,stroke:#ffffff 3["
C
[Software System]
"] @@ -220,7 +220,7 @@ public void test_NestedGroupsExample() throws Exception { graph TB linkStyle default fill:#ffffff - subgraph diagram ["System Landscape"] + subgraph diagram ["System Landscape View"] style diagram fill:#ffffff,stroke:#ffffff subgraph group1 ["Organisation 1"] @@ -281,7 +281,7 @@ public void test_renderContainerDiagramWithExternalContainers() { graph TB linkStyle default fill:#ffffff - subgraph diagram ["Software System 1 - Containers"] + subgraph diagram ["Container View: Software System 1"] style diagram fill:#ffffff,stroke:#ffffff subgraph 1 ["Software System 1"] @@ -299,7 +299,7 @@ public void test_renderContainerDiagramWithExternalContainers() { end 2-. "
Uses
" .->4 - + end""", diagram.getDefinition()); } @@ -324,7 +324,7 @@ public void test_renderComponentDiagramWithExternalComponents() { graph TB linkStyle default fill:#ffffff - subgraph diagram ["Software System 1 - Container 1 - Components"] + subgraph diagram ["Component View: Software System 1 - Container 1"] style diagram fill:#ffffff,stroke:#ffffff subgraph 2 ["Container 1"] @@ -342,7 +342,7 @@ public void test_renderComponentDiagramWithExternalComponents() { end 3-. "
Uses
" .->6 - + end""", diagram.getDefinition()); } @@ -365,7 +365,7 @@ public void test_renderGroupStyles() { graph TB linkStyle default fill:#ffffff - subgraph diagram ["System Landscape"] + subgraph diagram ["System Landscape View"] style diagram fill:#ffffff,stroke:#ffffff subgraph group1 ["Group 1"] @@ -399,7 +399,7 @@ public void test_renderGroupStyles() { graph TB linkStyle default fill:#ffffff - subgraph diagram ["System Landscape"] + subgraph diagram ["System Landscape View"] style diagram fill:#ffffff,stroke:#ffffff subgraph group1 ["Group 1"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index 02cf8273a..784c6fe7a 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -29,7 +29,7 @@ public void test_BigBankPlcExample() throws Exception { Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -80,7 +80,7 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); assertEquals(""" @startuml - title Internet Banking System - System Context\\nThe system context diagram for the Internet Banking System. + title System Context View: Internet Banking System\\nThe system context diagram for the Internet Banking System. set separator none top to bottom direction @@ -122,7 +122,7 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); assertEquals(""" @startuml - title Internet Banking System - Containers\\nThe container diagram for the Internet Banking System. + title Container View: Internet Banking System\\nThe container diagram for the Internet Banking System. set separator none top to bottom direction @@ -179,7 +179,7 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); assertEquals(""" @startuml - title Internet Banking System - API Application - Components\\nThe component diagram for the API Application. + title Component View: Internet Banking System - API Application\\nThe component diagram for the API Application. set separator none top to bottom direction @@ -242,7 +242,7 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); assertEquals(""" @startuml - title API Application - Dynamic\\nSummarises how the sign in feature works in the single-page application. + title Dynamic View: Internet Banking System - API Application\\nSummarises how the sign in feature works in the single-page application. set separator none top to bottom direction @@ -289,7 +289,7 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); assertEquals(""" @startuml - title Internet Banking System - Deployment - Development\\nAn example development deployment scenario for the Internet Banking System. + title Deployment View: Internet Banking System - Development\\nAn example development deployment scenario for the Internet Banking System. set separator none top to bottom direction @@ -355,7 +355,7 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); assertEquals(""" @startuml - title Internet Banking System - Deployment - Live\\nAn example live deployment scenario for the Internet Banking System. + title Deployment View: Internet Banking System - Live\\nAn example live deployment scenario for the Internet Banking System. set separator none top to bottom direction @@ -446,7 +446,7 @@ public void test_BigBankPlcExample() throws Exception { diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); assertEquals(""" @startuml - title API Application - Dynamic\\nSummarises how the sign in feature works in the single-page application. + title Dynamic View: Internet Banking System - API Application\\nSummarises how the sign in feature works in the single-page application. set separator none @@ -497,7 +497,7 @@ public void test_AmazonWebServicesExampleWithoutTags() throws Exception { Diagram diagram = diagrams.stream().findFirst().get(); assertEquals(""" @startuml - title X - Deployment - Live + title Deployment View: X - Live set separator none left to right direction @@ -560,7 +560,7 @@ public void test_AmazonWebServicesExampleWithTags() throws Exception { Diagram diagram = diagrams.stream().findFirst().get(); assertEquals(""" @startuml - title X - Deployment - Live + title Deployment View: X - Live set separator none left to right direction @@ -632,7 +632,7 @@ public void test_GroupsExample() throws Exception { Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -675,7 +675,7 @@ public void test_GroupsExample() throws Exception { diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); assertEquals(""" @startuml - title D - Containers + title Container View: D set separator none top to bottom direction @@ -712,7 +712,7 @@ public void test_GroupsExample() throws Exception { diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); assertEquals(""" @startuml - title D - F - Components + title Component View: D - F set separator none top to bottom direction @@ -776,7 +776,7 @@ public void test_NestedGroupsExample() throws Exception { Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -839,7 +839,7 @@ public void test_NestedGroupsExample_WithDotAsGroupSeparator() throws Exception Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -896,7 +896,7 @@ protected double calculateIconScale(String iconUrl, int maxIconSize) { Diagram diagram = exporter.export(view); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -936,7 +936,7 @@ protected double calculateIconScale(String iconUrl, int maxIconSize) { diagram = exporter.export(view); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -990,7 +990,7 @@ public void test_renderContainerDiagramWithExternalContainers() { Diagram diagram = new C4PlantUMLExporter().export(containerView); assertEquals(""" @startuml - title Software System 1 - Containers + title Container View: Software System 1 set separator none top to bottom direction @@ -1041,7 +1041,7 @@ public void test_renderComponentDiagramWithExternalComponents() { Diagram diagram = new C4PlantUMLExporter().export(componentView); assertEquals(""" @startuml - title Software System 1 - Container 1 - Components + title Component View: Software System 1 - Container 1 set separator none top to bottom direction @@ -1085,7 +1085,7 @@ public void test_renderDiagramWithElementUrls() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1120,7 +1120,7 @@ public void test_renderDiagramWithIncludes() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1156,7 +1156,7 @@ public void test_renderDiagramWithNewLineCharacterInElementName() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1190,7 +1190,7 @@ public void test_renderInfrastructureNodeWithTechnology() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title Deployment - Default\\nview description + title Deployment View: Default\\nview description set separator none top to bottom direction @@ -1239,7 +1239,7 @@ public void test_printProperties() throws Exception { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title SoftwareSystem - Containers + title Container View: SoftwareSystem set separator none top to bottom direction @@ -1293,7 +1293,7 @@ public void test_deploymentViewPrintProperties() throws Exception { Diagram diagram = new C4PlantUMLExporter().export(deploymentView); assertEquals(""" @startuml - title Deployment - Default + title Deployment View: Default set separator none top to bottom direction @@ -1336,7 +1336,7 @@ public void test_legendAndStereotypes() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1363,7 +1363,7 @@ public void test_legendAndStereotypes() { diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1390,7 +1390,7 @@ public void test_legendAndStereotypes() { diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1417,7 +1417,7 @@ public void test_legendAndStereotypes() { diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1465,7 +1465,7 @@ public void test_renderContainerShapes() throws Exception { Diagram diagram = new C4PlantUMLExporter().export(containerView); assertEquals(""" @startuml - title Software System - Containers + title Container View: Software System set separator none top to bottom direction @@ -1522,7 +1522,7 @@ public void test_renderComponentShapes() throws Exception { Diagram diagram = new C4PlantUMLExporter().export(componentView); assertEquals(""" @startuml - title Software System - Container - Components + title Component View: Software System - Container set separator none top to bottom direction @@ -1561,7 +1561,7 @@ public void testFont() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1596,7 +1596,7 @@ public void stdlib_false() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1631,7 +1631,7 @@ public void componentWithoutTechnology() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title Name - Name - Components\\nDescription + title Component View: Name - Name\\nDescription set separator none top to bottom direction @@ -1668,7 +1668,7 @@ public void borderStyling() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1703,7 +1703,7 @@ public void elementWithUrl() { Diagram diagram = new C4PlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 8a6e1ff66..6e16d8b9b 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -33,7 +33,7 @@ public void systemLandscapeView_NoStyling_Light() { assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -135,7 +135,7 @@ public void systemLandscapeView_NoStyling_Dark() { assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -237,7 +237,7 @@ public void systemContextView_NoStyling_Light() { assertEquals(""" @startuml - title A - System Context\\nDescription + title System Context View: A\\nDescription set separator none top to bottom direction @@ -294,7 +294,7 @@ public void containerView_NoStyling_Light() { assertEquals(""" @startuml - title Software System A - Containers\\nDescription + title Container View: Software System A\\nDescription set separator none top to bottom direction @@ -365,7 +365,7 @@ public void componentView_NoStyling_Light() { assertEquals(""" @startuml - title Software System - Container - Components\\nDescription + title Component View: Software System - Container\\nDescription set separator none top to bottom direction @@ -435,7 +435,7 @@ public void deploymentView_NoStyling_Light() { assertEquals(""" @startuml - title Deployment - Default\\nDefault + title Deployment View: Default\\nDefault set separator none top to bottom direction @@ -490,7 +490,7 @@ public void dynamicView_CollaborationStyle_NoStyling_Light() { assertEquals(""" @startuml - title Dynamic\\nDescription + title Dynamic View\\nDescription set separator none top to bottom direction @@ -552,7 +552,7 @@ public void dynamicView_CollaborationStyle_Frames() { assertEquals(""" @startuml - title Dynamic\\nDescription + title Dynamic View\\nDescription set separator none top to bottom direction @@ -597,7 +597,7 @@ public void dynamicView_CollaborationStyle_Frames() { assertEquals(""" @startuml - title Dynamic\\nDescription + title Dynamic View\\nDescription set separator none top to bottom direction @@ -658,7 +658,7 @@ public void dynamicView_SequenceStyle_NoStyling_Light() { assertEquals(""" @startuml - title Dynamic\\nDescription + title Dynamic View\\nDescription set separator none hide stereotype @@ -755,7 +755,7 @@ public void groups() throws Exception { Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -846,7 +846,7 @@ public void groups() throws Exception { diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); assertEquals(""" @startuml - title D - Containers + title Container View: D set separator none top to bottom direction @@ -921,7 +921,7 @@ public void groups() throws Exception { diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); assertEquals(""" @startuml - title D - F - Components + title Component View: D - F set separator none top to bottom direction @@ -1026,7 +1026,7 @@ public void nestedGroups() { Diagram diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemLandscape")).findFirst().get(); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -1147,7 +1147,7 @@ public void containerDiagramWithExternalContainers() { Diagram diagram = new StructurizrPlantUMLExporter().export(containerView); assertEquals(""" @startuml - title Software System 1 - Containers + title Container View: Software System 1 set separator none top to bottom direction @@ -1239,7 +1239,7 @@ public void componentDiagramWithExternalComponents() { Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); assertEquals(""" @startuml - title Software System 1 - Container 1 - Components + title Component View: Software System 1 - Container 1 set separator none top to bottom direction @@ -1334,7 +1334,7 @@ public void componentDiagramWithExternalComponentsAndSoftwareSystemBoundariesInc Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); assertEquals(""" @startuml - title Software System 1 - Container 1 - Components + title Component View: Software System 1 - Container 1 set separator none top to bottom direction @@ -1448,7 +1448,7 @@ public void dynamicView_ExternalContainers() { Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); assertEquals(""" @startuml - title Software System 1 - Dynamic + title Dynamic View: Software System 1 set separator none top to bottom direction @@ -1539,7 +1539,7 @@ public void dynamicView_ExternalComponents() { Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); assertEquals(""" @startuml - title Container 1 - Dynamic + title Dynamic View: Software System 1 - Container 1 set separator none top to bottom direction @@ -1633,7 +1633,7 @@ public void dynamicView_ExternalComponentsAndSoftwareSystemBoundariesIncluded() Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); assertEquals(""" @startuml - title Container 1 - Dynamic + title Dynamic View: Software System 1 - Container 1 set separator none top to bottom direction @@ -1771,7 +1771,7 @@ public void newLineCharacterInElementName() { Diagram diagram = new StructurizrPlantUMLExporter().export(view); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -1867,7 +1867,7 @@ void renderWorkspaceWithUnicodeElementName() { assertEquals(""" @startuml - title System Landscape\\nDescription + title System Landscape View\\nDescription set separator none top to bottom direction @@ -1954,7 +1954,7 @@ public void dynamicView_UnscopedWithGroups() { Diagram diagram = exporter.export(view); assertEquals(""" @startuml - title Dynamic + title Dynamic View set separator none top to bottom direction @@ -2041,7 +2041,7 @@ public void dynamicView_SoftwareSystemScopedWithGroups() { Diagram diagram = exporter.export(view); assertEquals(""" @startuml - title A - Dynamic + title Dynamic View: A set separator none top to bottom direction @@ -2157,7 +2157,7 @@ public void dynamicView_ContainerScopedWithGroups() { Diagram diagram = exporter.export(view); assertEquals(""" @startuml - title A - Dynamic + title Dynamic View: A - A set separator none top to bottom direction @@ -2277,7 +2277,7 @@ public void deploymentView_WithGroups() { Diagram diagram = exporter.export(view); assertEquals(""" @startuml - title Deployment - Default + title Deployment View: Default set separator none top to bottom direction @@ -2352,7 +2352,7 @@ void light_group() { Diagram diagram = exporter.export(view); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -2409,7 +2409,7 @@ void dark_group() { Diagram diagram = exporter.export(view); assertEquals(""" @startuml - title System Landscape + title System Landscape View set separator none top to bottom direction @@ -2466,7 +2466,7 @@ public void amazonWebServicesExample_Light() throws Exception { Diagram diagram = diagrams.stream().findFirst().get(); assertEquals(""" @startuml - title X - Deployment - Live + title Deployment View: X - Live set separator none left to right direction @@ -2825,7 +2825,7 @@ public void amazonWebServicesExample_Dark() throws Exception { Diagram diagram = diagrams.stream().findFirst().get(); assertEquals(""" @startuml - title X - Deployment - Live + title Deployment View: X - Live set separator none left to right direction @@ -3171,4 +3171,16 @@ public void amazonWebServicesExample_Dark() throws Exception { @enduml""", diagram.getLegend().getDefinition()); } + @Test + void c4model() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("/Users/simon/Desktop/c4model/c4model/01-original/workspace.json")); + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + + View view = workspace.getViews().getViewWithKey("Dynamic-SignIn-Collaboration"); + + Diagram diagram = exporter.export((DynamicView)view); + System.out.println(diagram.getDefinition()); + + } + } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java index 25319e27f..9e9d6b8a3 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java @@ -25,7 +25,7 @@ public void test_BigBankPlcExample() throws Exception { Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); assertEquals(""" - title API Application - Dynamic - SignIn + title Dynamic View: Internet Banking System - API Application\\nSummarises how the sign in feature works in the single-page application. participant <>\\nSingle-Page Application as Single-Page Application participant <>\\nSign In Controller as Sign In Controller @@ -56,7 +56,7 @@ public void test_dynamicViewThatDoeNotOverrideRelationshipDescriptions() throws Collection diagrams = exporter.export(workspace); Diagram diagram = diagrams.iterator().next(); assertEquals(""" - title Dynamic - key + title Dynamic View\\nDescription participant <>\\nA as A participant <>\\nB as B From 146a514388e8921e60204f4382776059a6ec6efb Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 18 Sep 2025 12:06:59 +0100 Subject: [PATCH 380/418] Propagates view title/name and description to image views based upon a model view. --- .../structurizr/dsl/ImageViewContentParser.java | 15 +++++++++++++++ .../dsl/ImageViewContentParserTests.java | 9 ++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index 2264a16b8..5e8021fcb 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -7,6 +7,7 @@ import com.structurizr.importer.diagrams.mermaid.MermaidImporter; import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; import com.structurizr.util.Url; import com.structurizr.view.ImageView; import com.structurizr.view.ModelView; @@ -56,6 +57,13 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); String plantuml = exporter.export((ModelView) viewWithKey).getDefinition(); new PlantUMLImporter().importDiagram(context.getView(), plantuml); + + if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { + context.getView().setTitle(viewWithKey.getTitle()); + } else { + context.getView().setTitle(viewWithKey.getName()); + } + context.getView().setDescription(viewWithKey.getDescription()); } else { if (Url.isUrl(source)) { RemoteContent content = readFromUrl(source); @@ -107,6 +115,13 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { MermaidDiagramExporter exporter = new MermaidDiagramExporter(); String mermaid = exporter.export((ModelView) viewWithKey).getDefinition(); new MermaidImporter().importDiagram(context.getView(), mermaid); + + if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { + context.getView().setTitle(viewWithKey.getTitle()); + } else { + context.getView().setTitle(viewWithKey.getName()); + } + context.getView().setDescription(viewWithKey.getDescription()); } else { if (Url.isUrl(source)) { RemoteContent content = readFromUrl(source); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index 4a2266c73..6de21a3b0 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -55,8 +55,9 @@ void test_parsePlantUML_WithViewKey() { parser = new ImageViewContentParser(true); parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape")); - assertEquals("System Landscape", imageView.getTitle()); - assertEquals("https://plantuml.com/plantuml/svg/ZLBBJiCm4BpxArRb32rLuH2IgE4b3kL243q01pVU9bOJRsHlr0VYtt6Qj1n0Y9LihMTjpth6KyVISbELWZMN2A7JEmp6apZTEiOAPj8ebyaQms5RYT_CSSSjkipgcZMPlYY4GmQ7jRIIoO8XWuAf1YPO43DLeBJ5h3qY2gqGF8T5ucsDGeIEjwM_1C0ICNpu1E1QPglSKcFK3PLa0pXPxkDgNxqdmmTyieyM__HZE8Ix4Yiqx1TdVNhwDD-KY_bBev8e-XV1J1lyIT3XQTjk0ADlvBdGsSgWSm6C_sgmmzDMHnZto0DPlVEeB9DIvwPjDu3CpsYx3MaX5QsroO-KZtAZgwQQQyL509EBKVTuRqOdf6YbbYRtjWwYA3bOTtuPlwQqvBMq29tDxxs10mZ3txIAOv0E4Y6cQ9J_B5y0", imageView.getContent()); + assertEquals("System Landscape View", imageView.getTitle()); + assertEquals("Description", imageView.getDescription()); + assertEquals("https://plantuml.com/plantuml/svg/ZLBBJiCm4BpxArPmXfQgS0X9rF0IXt8Xg3q01pVP9bOTRsHlr0VYtt6Qj1n0Y9LihMTjpth64yVISbDfmOerGkZK3eFHE4wtZh62gJIvosIDC5Eu3WTjENupnsrtw3AhQbPa-g8G3XaSrj9A9Wk630gc6fXWGSnKGQuiPkqHKQeSmHDP9DxMA4JeUAlz9G2MYE739m0tCbiLbXgJtv8c6y3fSX_N--e36JxWutsq-ASVWm7SQwpGi5-Sz-dPytoZ5_DPaoTHz2-2gJBuaw33qxRT08RVo4kfifL1vm8O_TLWXwUjZZ3gaKUoQkTHgHEj2jEs6q3cPxJTXhIKEQsLAOwKJtAZggQQgvpB0CQNm-xntenEID5ABKtXlJs9ekHWtSLL_9hIajVI8dHUl_S6da0O_gPL78Dqa0WnGPFx7_C5", imageView.getContent()); } @Test @@ -95,7 +96,9 @@ void test_parseMermaid_WithViewKey() { parser = new ImageViewContentParser(true); parser.parseMermaid(context, null, tokens("mermaid", "SystemLandscape")); - assertEquals("https://mermaid.ink/svg/pako:eJxtkMtqwzAQRX9lmFK8cWgChYKaGNp1d-4uzkKxRraIHkaaNE1D_r12lEJfs5qBM4fLPQG2QREK7KIcenh9bjyANX5X89ESKNJybxm0sVbc6Ms0fmLSfptflJHj4mDdYH1MTA5epFeplQM1uJnQEc6yK_ldViaOYUc_3QCL0bZU5i1_rgodPM8OZLqeBWyDVUX1tLwbgeoPlcwHCXiY3z6Ck7EzfsZhEDAf3otqXQfNBxkJctRNdvzKufg_4f1lyjbYEL-unJe8whLQUXTSKBQn5J7c1Oq1PzyfPwHoMXnQ", imageView.getContent()); + assertEquals("System Landscape View", imageView.getTitle()); + assertEquals("Description", imageView.getDescription()); + assertEquals("https://mermaid.ink/svg/pako:eJxtkM1rwkAQxf-VYYrkEqmCUNjaQD33luLFeFizs8nifoTd1WjF_72Ja6Ffc5qB33vzeBfA2glCho3nXQvvq8oCaGX3ZTxrAkGSH3QEqbRmD_I2lR2ZcNgliVB8WAxsKizPIZKBN25FqHlHsFbUV7gd-UGRHO_4d8c8RO_29PMBwHywXAp1TMqXTDobpz2ppo0Mdk6LrHhdPg5A8YcK6oMYPM0mz2C4b5SdRtcxmHWnrNiUTsaee4KUd5s8fuWc_59wcZu8dtr5ryvlJSswBzTkDVcC2QVjS2as9l4iXq-fYuV7iw==", imageView.getContent()); } @Test From 2adf519a76687bab3ecd5a9117608ba6c6f0e96f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 19 Sep 2025 13:17:28 +0100 Subject: [PATCH 381/418] Adds support for separate images for light and dark color schemes. --- changelog.md | 3 +- .../java/com/structurizr/view/ImageView.java | 54 +++- .../com/structurizr/view/ImageViewTests.java | 27 ++ .../dsl/ImageViewContentParser.java | 271 +++++++++--------- .../structurizr/dsl/ImageViewDslContext.java | 30 +- .../com/structurizr/dsl/NameValuePair.java | 3 +- .../structurizr/dsl/StructurizrDslParser.java | 49 +++- .../java/com/structurizr/dsl/DslTests.java | 39 ++- .../dsl/ImageViewContentParserTests.java | 181 +++++++++++- .../dsl/ImageViewDslContextTests.java | 31 ++ .../src/test/resources/dsl/test.dsl | 10 + .../src/test/resources/dsl/text-block.dsl | 31 +- .../diagrams/image/ImageImporter.java | 16 +- .../diagrams/kroki/KrokiImporter.java | 17 +- .../diagrams/mermaid/MermaidImporter.java | 17 +- .../diagrams/plantuml/PlantUMLImporter.java | 17 +- .../diagrams/image/ImageImporterTests.java | 3 +- 17 files changed, 605 insertions(+), 194 deletions(-) create mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewDslContextTests.java diff --git a/changelog.md b/changelog.md index 91d9b037a..be3db6f30 100644 --- a/changelog.md +++ b/changelog.md @@ -3,7 +3,8 @@ ## v5.0.0 (unreleased) - structurizr-core: Removes support for deprecated enterprise and location concepts. -- structurizr-core: Adds support for filtered deployment views (https://github.com/structurizr/java/issues/409). +- structurizr-core: Adds support for filtered deployment views (https://github.com/structurizr/java/issues/409). +- structurizr-core: Adds support for separate images for light and dark color schemes. - structurizr-component: Fixes https://github.com/structurizr/java/issues/437 (Make ComponentFinder.run() not fail on empty Set). - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). - structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. diff --git a/structurizr-core/src/main/java/com/structurizr/view/ImageView.java b/structurizr-core/src/main/java/com/structurizr/view/ImageView.java index b67845a32..bd66943d2 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ImageView.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ImageView.java @@ -13,6 +13,8 @@ public final class ImageView extends View { private Element element; private String elementId; private String content; + private String contentLight; + private String contentDark; private String contentType; ImageView() { @@ -62,17 +64,58 @@ public String getContent() { return content; } + /** + * Gets the content of this view (a URL or a data URI), for the light color scheme. + * + * @return the content, as a String + */ + public String getContentLight() { + return contentLight; + } + + /** + * Gets the content of this view (a URL or a data URI), for the dark color scheme. + * + * @return the content, as a String + */ + public String getContentDark() { + return contentDark; + } + /** * Sets the content of this image view, which needs to be a URL or a data URI. * * @param content the content of this view */ public void setContent(String content) { + setContent(content, null); + } + + /** + * Sets the content of this image view, which needs to be a URL or a data URI. + * + * @param content the content of this view + */ + public void setContent(String content, ColorScheme colorScheme) { if (StringUtils.isNullOrEmpty(content)) { - this.content = null; + if (colorScheme == ColorScheme.Dark) { + this.contentDark = null; + } else if (colorScheme == ColorScheme.Light) { + this.contentLight = null; + } else { + this.content = null; + } } else { ImageUtils.validateImage(content); - this.content = content.trim(); + content = content.trim(); + + if (colorScheme == ColorScheme.Dark) { + this.contentDark = content; + } else if (colorScheme == ColorScheme.Light) { + this.contentLight = content; + } else { + this.content = content; + } } } @@ -99,4 +142,11 @@ public String getName() { return getTitle(); } + public boolean hasContent() { + return + !StringUtils.isNullOrEmpty(content) || + !StringUtils.isNullOrEmpty(contentLight) || + !StringUtils.isNullOrEmpty(contentDark); + } + } \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java index b7b921099..97056284e 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java @@ -27,4 +27,31 @@ void construction_WhenAnElementIsSpecified() { assertEquals(softwareSystem.getId(), view.getElementId()); } + @Test + void hasContent_WhenNoContent() { + ImageView view = views.createImageView("key"); + assertFalse(view.hasContent()); + } + + @Test + void hasContent_WhenContent() { + ImageView view = views.createImageView("key"); + view.setContent("https://example.com/image.png"); + assertTrue(view.hasContent()); + } + + @Test + void hasContent_WhenContentLight() { + ImageView view = views.createImageView("key"); + view.setContent("https://example.com/image.png", ColorScheme.Light); + assertTrue(view.hasContent()); + } + + @Test + void hasContent_WhenContentDark() { + ImageView view = views.createImageView("key"); + view.setContent("https://example.com/image.png", ColorScheme.Dark); + assertTrue(view.hasContent()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index 5e8021fcb..b37b55dc8 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -6,15 +6,13 @@ import com.structurizr.importer.diagrams.kroki.KrokiImporter; import com.structurizr.importer.diagrams.mermaid.MermaidImporter; import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; -import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; import com.structurizr.util.Url; -import com.structurizr.view.ImageView; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ModelView; import com.structurizr.view.View; import java.io.File; -import java.net.URL; final class ImageViewContentParser extends AbstractParser { @@ -24,9 +22,12 @@ final class ImageViewContentParser extends AbstractParser { private static final String IMAGE_GRAMMAR = "image "; private static final int PLANTUML_SOURCE_INDEX = 1; + private static final int MERMAID_SOURCE_INDEX = 1; + private static final int KROKI_FORMAT_INDEX = 1; private static final int KROKI_SOURCE_INDEX = 2; + private static final int IMAGE_SOURCE_INDEX = 1; private boolean restricted = false; @@ -38,199 +39,207 @@ final class ImageViewContentParser extends AbstractParser { void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { // plantuml + if (!tokens.includes(PLANTUML_SOURCE_INDEX)) { + throw new RuntimeException("Expected: " + PLANTUML_GRAMMAR); + } + if (tokens.hasMoreThan(PLANTUML_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + PLANTUML_GRAMMAR); } - ImageView view = context.getView(); - if (view != null) { - if (tokens.size() == 2) { - String source = tokens.get(PLANTUML_SOURCE_INDEX); + String source = tokens.get(PLANTUML_SOURCE_INDEX); + ColorScheme colorScheme = context.getColorScheme(); + + try { + if (source.contains("\n")) { + // inline source + new PlantUMLImporter().importDiagram(context.getView(), source, colorScheme); + } else { + View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); + if (viewWithKey instanceof ModelView) { + String plantumlLight = new StructurizrPlantUMLExporter(ColorScheme.Light).export((ModelView) viewWithKey).getDefinition(); + new PlantUMLImporter().importDiagram(context.getView(), plantumlLight, ColorScheme.Light); + + String plantumlDark = new StructurizrPlantUMLExporter(ColorScheme.Dark).export((ModelView) viewWithKey).getDefinition(); + new PlantUMLImporter().importDiagram(context.getView(), plantumlDark, ColorScheme.Dark); - try { - if (source.contains("\n")) { - // inline source - new PlantUMLImporter().importDiagram(context.getView(), source); + if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { + context.getView().setTitle(viewWithKey.getTitle()); } else { - View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); - if (viewWithKey instanceof ModelView) { - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); - String plantuml = exporter.export((ModelView) viewWithKey).getDefinition(); - new PlantUMLImporter().importDiagram(context.getView(), plantuml); - - if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { - context.getView().setTitle(viewWithKey.getTitle()); + context.getView().setTitle(viewWithKey.getName()); + } + context.getView().setDescription(viewWithKey.getDescription()); + } else { + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new PlantUMLImporter().importDiagram(context.getView(), content.getContent(), colorScheme); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + new PlantUMLImporter().importDiagram(context.getView(), file, colorScheme); } else { - context.getView().setTitle(viewWithKey.getName()); + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } - context.getView().setDescription(viewWithKey.getDescription()); } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new PlantUMLImporter().importDiagram(context.getView(), content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); - } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - if (file.exists()) { - context.setDslPortable(false); - new PlantUMLImporter().importDiagram(context.getView(), file); - } else { - throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); - } - } else { - throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); - } - } + throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); } } - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); } - } else { - throw new RuntimeException("Expected: " + PLANTUML_GRAMMAR); } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); } } void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { // mermaid + if (!tokens.includes(MERMAID_SOURCE_INDEX)) { + throw new RuntimeException("Expected: " + MERMAID_GRAMMAR); + } + if (tokens.hasMoreThan(MERMAID_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + MERMAID_GRAMMAR); } - ImageView view = context.getView(); - if (view != null) { - if (tokens.size() == 2) { - String source = tokens.get(MERMAID_SOURCE_INDEX); + String source = tokens.get(MERMAID_SOURCE_INDEX); + ColorScheme colorScheme = context.getColorScheme(); - try { - if (source.contains("\n")) { - // inline source - new MermaidImporter().importDiagram(context.getView(), source); + try { + if (source.contains("\n")) { + // inline source + new MermaidImporter().importDiagram(context.getView(), source, colorScheme); + } else { + View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); + if (viewWithKey instanceof ModelView) { + MermaidDiagramExporter exporter = new MermaidDiagramExporter(); + String mermaid = exporter.export((ModelView) viewWithKey).getDefinition(); + new MermaidImporter().importDiagram(context.getView(), mermaid, colorScheme); + + if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { + context.getView().setTitle(viewWithKey.getTitle()); } else { - View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); - if (viewWithKey instanceof ModelView) { - MermaidDiagramExporter exporter = new MermaidDiagramExporter(); - String mermaid = exporter.export((ModelView) viewWithKey).getDefinition(); - new MermaidImporter().importDiagram(context.getView(), mermaid); - - if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { - context.getView().setTitle(viewWithKey.getTitle()); + context.getView().setTitle(viewWithKey.getName()); + } + context.getView().setDescription(viewWithKey.getDescription()); + } else { + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new MermaidImporter().importDiagram(context.getView(), content.getContent(), colorScheme); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + new MermaidImporter().importDiagram(context.getView(), file, colorScheme); } else { - context.getView().setTitle(viewWithKey.getName()); + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } - context.getView().setDescription(viewWithKey.getDescription()); } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new MermaidImporter().importDiagram(context.getView(), content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); - } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - if (file.exists()) { - context.setDslPortable(false); - new MermaidImporter().importDiagram(context.getView(), file); - } else { - throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); - } - } else { - throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); - } - } + throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); } } - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); } - } else { - throw new RuntimeException("Expected: " + MERMAID_GRAMMAR); } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); } } void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) { // kroki + if (!tokens.includes(KROKI_SOURCE_INDEX)) { + throw new RuntimeException("Expected: " + KROKI_GRAMMAR); + } + if (tokens.hasMoreThan(KROKI_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + KROKI_GRAMMAR); } - ImageView view = context.getView(); - if (view != null) { - if (tokens.size() == 3) { - String format = tokens.get(KROKI_FORMAT_INDEX); - String source = tokens.get(KROKI_SOURCE_INDEX); + String format = tokens.get(KROKI_FORMAT_INDEX); + String source = tokens.get(KROKI_SOURCE_INDEX); + ColorScheme colorScheme = context.getColorScheme(); - try { - if (source.contains("\n")) { - // inline source - new KrokiImporter().importDiagram(context.getView(), format, source); - } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new KrokiImporter().importDiagram(context.getView(), format, content.getContent()); - context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); + try { + if (source.contains("\n")) { + // inline source + new KrokiImporter().importDiagram(context.getView(), format, source, colorScheme); + } else { + if (Url.isUrl(source)) { + RemoteContent content = readFromUrl(source); + new KrokiImporter().importDiagram(context.getView(), format, content.getContent(), colorScheme); + context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + new KrokiImporter().importDiagram(context.getView(), format, file, colorScheme); } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - if (file.exists()) { - context.setDslPortable(false); - new KrokiImporter().importDiagram(context.getView(), format, file); - } else { - throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); - } - } else { - throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode"); - } + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } + } else { + throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode"); } - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); } - } else { - throw new RuntimeException("Expected: " + KROKI_GRAMMAR); } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); } } void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { // image + if (!tokens.includes(IMAGE_SOURCE_INDEX)) { + throw new RuntimeException("Expected: " + IMAGE_GRAMMAR); + } + if (tokens.hasMoreThan(IMAGE_SOURCE_INDEX)) { throw new RuntimeException("Too many tokens, expected: " + IMAGE_GRAMMAR); } - ImageView view = context.getView(); - if (view != null) { - if (tokens.size() == 2) { - String source = tokens.get(IMAGE_SOURCE_INDEX); + String source = tokens.get(IMAGE_SOURCE_INDEX); + ColorScheme colorScheme = context.getColorScheme(); - try { - if (Url.isUrl(source)) { - new ImageImporter().importDiagram(context.getView(), source); + try { + if (Url.isUrl(source)) { + new ImageImporter().importDiagram(context.getView(), source, colorScheme); + } else { + if (!restricted) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + new ImageImporter().importDiagram(context.getView(), file, colorScheme); } else { - if (!restricted) { - File file = new File(dslFile.getParentFile(), source); - if (file.exists()) { - context.setDslPortable(false); - new ImageImporter().importDiagram(context.getView(), file); - } else { - throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); - } - } else { - throw new RuntimeException("Images must be specified as a URL when running in restricted mode"); - } + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + } else { + throw new RuntimeException("Images must be specified as a URL when running in restricted mode"); } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + private ColorScheme calculateColorScheme(Tokens tokens, int index) { + if (tokens.includes(index)) { + if (ColorScheme.Dark.toString().equalsIgnoreCase(tokens.get(index))) { + return ColorScheme.Dark; + } else if (ColorScheme.Light.toString().equalsIgnoreCase(tokens.get(index))) { + return ColorScheme.Light; } else { - throw new RuntimeException("Expected: " + IMAGE_GRAMMAR); + throw new RuntimeException("Invalid color scheme \"" + tokens.get(index) + "\" - expected: light or dark"); } } + + return null; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java index 1a7a996a5..b33c4cbe7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java @@ -2,12 +2,15 @@ import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import java.io.IOException; class ImageViewDslContext extends ViewDslContext { + private ColorScheme colorScheme; + ImageViewDslContext(ImageView view) { super(view); } @@ -16,6 +19,14 @@ ImageView getView() { return (ImageView)super.getView(); } + ColorScheme getColorScheme() { + return colorScheme; + } + + void setColorScheme(ColorScheme colorScheme) { + this.colorScheme = colorScheme; + } + @Override protected String[] getPermittedTokens() { return new String[] { @@ -32,19 +43,12 @@ protected String[] getPermittedTokens() { void end() { super.end(); - // try to set the content type if it hasn't been set ... this helps the diagram render with image sizing/scaling - ImageView imageView = getView(); - if (StringUtils.isNullOrEmpty(imageView.getContentType())) { - if (ImageUtils.isSupportedDataUri(imageView.getContent())) { - imageView.setContentType(ImageUtils.getContentTypeFromDataUri(imageView.getContent())); - } else { - try { - imageView.setContentType(ImageUtils.getContentType(imageView.getContent())); - } catch (IOException e) { - e.printStackTrace(); - // ignore - } - } + if (colorScheme != null) { + colorScheme = null; + } + + if (!getView().hasContent()) { + throw new RuntimeException("The image view \"" + getView().getKey() + "\" has no content"); } } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java index 225e77144..d415d50e5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java @@ -32,6 +32,7 @@ String getValue() { enum NameValueType { Constant, - Variable + Variable, + TextBlock } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 5561e2e17..61e9503df 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1099,6 +1099,16 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (IMAGE_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { new ImageViewContentParser(restricted).parseImage(getContext(ImageViewDslContext.class), dslFile, tokens); + } else if (LIGHT_COLOR_SCHEME_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class) && shouldStartContext(tokens)) { + ImageViewDslContext context = getContext(ImageViewDslContext.class); + context.setColorScheme(ColorScheme.Light); + startContext(context); + + } else if (DARK_COLOR_SCHEME_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class) && shouldStartContext(tokens)) { + ImageViewDslContext context = getContext(ImageViewDslContext.class); + context.setColorScheme(ColorScheme.Dark); + startContext(context); + } else if (inContext(DynamicViewDslContext.class)) { RelationshipView relationshipView = new DynamicViewContentParser().parseRelationship(getContext(DynamicViewDslContext.class), tokens); @@ -1230,11 +1240,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (VAR_TOKEN.equalsIgnoreCase(firstToken)) { NameValuePair nameValuePair = new NameValueParser().parseVariable(tokens); - - if (constantsAndVariables.containsKey(nameValuePair.getName()) && constantsAndVariables.get(nameValuePair.getName()).getType() == NameValueType.Constant) { - throw new StructurizrDslParserException("A constant \"" + nameValuePair.getName() + "\" already exists"); - } - constantsAndVariables.put(nameValuePair.getName(), nameValuePair); + addVariable(nameValuePair); } else if (IDENTIFIERS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(WorkspaceDslContext.class) || inContext(ModelDslContext.class))) { setIdentifierScope(new IdentifierScopeParser().parse(getContext(), tokens)); @@ -1337,10 +1343,10 @@ private List preProcessLines(List lines) { if (source.endsWith(TEXT_BLOCK_MARKER)) { String[] parts = source.split(TEXT_BLOCK_MARKER); - String constantName = UUID.randomUUID().toString(); - String constantValue = parts[1].substring(0, parts[1].length() - 1); // remove final line break - addConstant(constantName, constantValue); - dslLines.add(new DslLine(parts[0] + "\"" + String.format(STRING_SUBSTITUTION_TEMPLATE, constantName) + "\"", lineNumber)); + String textBlockName = UUID.randomUUID().toString(); + String textBlockValue = parts[1].substring(0, parts[1].length() - 1); // remove final line break + addTextBlock(textBlockName, textBlockValue); + dslLines.add(new DslLine(parts[0] + "\"" + String.format(STRING_SUBSTITUTION_TEMPLATE, textBlockName) + "\"", lineNumber)); } else { dslLines.add(new DslLine(source, lineNumber)); } @@ -1365,7 +1371,13 @@ private String substituteStrings(String token) { String after = null; String name = before.substring(2, before.length()-1); if (constantsAndVariables.containsKey(name)) { - after = constantsAndVariables.get(name).getValue(); + NameValuePair nameValuePair = constantsAndVariables.get(name); + + if (nameValuePair.getType() == NameValueType.TextBlock) { + after = substituteStrings(nameValuePair.getValue()); + } else { + after = nameValuePair.getValue(); + } } else { if (!restricted) { String environmentVariable = System.getenv().get(name); @@ -1540,6 +1552,23 @@ private void addConstant(NameValuePair nameValuePair) { constantsAndVariables.put(nameValuePair.getName(), nameValuePair); } + private void addVariable(NameValuePair nameValuePair) { + if (constantsAndVariables.containsKey(nameValuePair.getName()) && constantsAndVariables.get(nameValuePair.getName()).getType() == NameValueType.Constant) { + throw new IllegalArgumentException("A constant \"" + nameValuePair.getName() + "\" already exists"); + } + constantsAndVariables.put(nameValuePair.getName(), nameValuePair); + } + + private void addTextBlock(String name, String value) { + if (StringUtils.isNullOrEmpty(name)) { + throw new IllegalArgumentException("A text block name must be specified"); + } + + NameValuePair nameValuePair = new NameValuePair(name, value); + nameValuePair.setType(NameValueType.TextBlock); + addConstant(nameValuePair); + } + private boolean inContext(Class clazz) { if (contextStack.empty()) { return false; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 022b748d2..f45817bd5 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1698,22 +1698,39 @@ void test_textBlock() throws Exception { parser.parse(new File("src/test/resources/dsl/text-block.dsl")); workspace = parser.getWorkspace(); - SoftwareSystem softwareSystem = workspace.getModel().getSoftwareSystemWithName("Name"); - assertEquals(""" - - Line 1 - - Line 2 - - Line 3""", softwareSystem.getDescription()); + + ImageView view = (ImageView)workspace.getViews().getViewWithKey("image"); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuUAoAIwfp4cruohApozHgEPI00AdnEJizAByqhmKv_oS_28h1UKqCB3cgkMoqOUgvqhEIImkLl2jT0RHN0wfUIb0ym00", view.getContent()); assertEquals(""" workspace { - model { - softwareSystem = softwareSystem "Name" { - description ""\" - - Line 1 - - Line 2 - - Line 3 + views { + properties { + "plantuml.url" "https://plantuml.com/plantuml" + } + + !const SOURCE ""\" + class MyClass ""\" + + !var STYLES ""\" + + ""\" + + image * "image" { + plantuml ""\" + @startuml + + ${STYLES} + + ${SOURCE} + @enduml + ""\" } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index 6de21a3b0..28b826974 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -1,13 +1,14 @@ package com.structurizr.dsl; +import com.structurizr.importer.diagrams.kroki.KrokiImporter; import com.structurizr.importer.diagrams.mermaid.MermaidImporter; import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; class ImageViewContentParserTests extends AbstractTests { @@ -45,6 +46,49 @@ void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { } } + @Test + void test_parsePlantUML_Source() { + String source = """ + @startuml + Bob -> Alice : hello + @enduml"""; + + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + parser = new ImageViewContentParser(true); + parser.parsePlantUML(new ImageViewDslContext(imageView), null, tokens("plantuml", source)); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", imageView.getContent()); + } + + @Test + void test_parsePlantUML_Source_Light() { + String source = """ + @startuml + Bob -> Alice : hello + @enduml"""; + + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Light); + parser.parsePlantUML(context, null, tokens("plantuml", source)); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", imageView.getContentLight()); + } + + @Test + void test_parsePlantUML_Source_Dark() { + String source = """ + @startuml + Bob -> Alice : hello + @enduml"""; + + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Dark); + parser.parsePlantUML(context, null, tokens("plantuml", source)); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", imageView.getContentDark()); + } + @Test void test_parsePlantUML_WithViewKey() { ImageViewDslContext context = new ImageViewDslContext(imageView); @@ -57,7 +101,9 @@ void test_parsePlantUML_WithViewKey() { parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape")); assertEquals("System Landscape View", imageView.getTitle()); assertEquals("Description", imageView.getDescription()); - assertEquals("https://plantuml.com/plantuml/svg/ZLBBJiCm4BpxArPmXfQgS0X9rF0IXt8Xg3q01pVP9bOTRsHlr0VYtt6Qj1n0Y9LihMTjpth64yVISbDfmOerGkZK3eFHE4wtZh62gJIvosIDC5Eu3WTjENupnsrtw3AhQbPa-g8G3XaSrj9A9Wk630gc6fXWGSnKGQuiPkqHKQeSmHDP9DxMA4JeUAlz9G2MYE739m0tCbiLbXgJtv8c6y3fSX_N--e36JxWutsq-ASVWm7SQwpGi5-Sz-dPytoZ5_DPaoTHz2-2gJBuaw33qxRT08RVo4kfifL1vm8O_TLWXwUjZZ3gaKUoQkTHgHEj2jEs6q3cPxJTXhIKEQsLAOwKJtAZggQQgvpB0CQNm-xntenEID5ABKtXlJs9ekHWtSLL_9hIajVI8dHUl_S6da0O_gPL78Dqa0WnGPFx7_C5", imageView.getContent()); + assertNull(imageView.getContent()); + assertEquals("https://plantuml.com/plantuml/svg/ZLBBJiCm4BpxArPmXfQgS0X9rF0IXt8Xg3q01pVP9bOTRsHlr0VYtt6Qj1n0Y9LihMTjpth64yVISbDfmOerGkZK3eFHE4wtZh62gJIvosIDC5Eu3WTjENupnsrtw3AhQbPa-g8G3XaSrj9A9Wk630gc6fXWGSnKGQuiPkqHKQeSmHDP9DxMA4JeUAlz9G2MYE739m0tCbiLbXgJtv8c6y3fSX_N--e36JxWutsq-ASVWm7SQwpGi5-Sz-dPytoZ5_DPaoTHz2-2gJBuaw33qxRT08RVo4kfifL1vm8O_TLWXwUjZZ3gaKUoQkTHgHEj2jEs6q3cPxJTXhIKEQsLAOwKJtAZggQQgvpB0CQNm-xntenEID5ABKtXlJs9ekHWtSLL_9hIajVI8dHUl_S6da0O_gPL78Dqa0WnGPFx7_C5", imageView.getContentLight()); + assertEquals("https://plantuml.com/plantuml/svg/ZLBBJiCm4BpxArRb37seS0X9rF0IXt8Xg3q01pTU4gkEDxAtwWFnxpXDAGSGOYLRwrdRivxnnBDqlAgDOCq68VPwXz5edEPRprZ3L5hb2zaWp3IkutvRJb_iSTiD-iBfXZNPGr48ZmmU6-aaamDB5WLJ0qom86QgGMc7HNj4L5eX12A7nDi6XOWzRqsu1C0HCRo71E1A5ilIqSggQpBa8ZWPxkDoNxqZorzuiOyM_mYZtuTRWpLQ3ekpGthwED-OnNosKbcI_8jWgYt-9EZml6qtWi4tybJfOcdH-mX6VpNOuNch8up67N9FJky2AarcT6dRTYCemeoksv1NKj5Qs_98-I0tkbxLSwsuYc1yFkWU7ypeX1IjrDAMmTjUacHVrWqlqkUStdWj7KBdzUl1m1x4yMzQfIb83vaG4xGg_9XF", imageView.getContentDark()); } @Test @@ -86,6 +132,58 @@ void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { } } + @Test + void test_parseMermaid_Source() { + String source = """ + flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car]"""; + + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + parser = new ImageViewContentParser(true); + parser.parseMermaid(new ImageViewDslContext(imageView), null, tokens("mermaid", source)); + assertEquals("https://mermaid.ink/svg/pako:eJxVj70OgjAUhV_lppMm8gIMJlKUhUQHtspwAxfbSH9SaoihvLsgi571-85JzgSssS2xlHW9HRuJPkCV3w0sOQkuvRqCxqGGJDnGggJoa-gdIdsVFgZpnVPmsd_8bJWAT-WqEQSpzHPeEP_2r4Yi5KJEF6yrf0k12ghnoW5ymf8n0tPSuogO0w6TBj1w9DU7ANPkNaqWpRMLkvR6oqUOX31g8_wBLY9E1w==", imageView.getContent()); + } + + @Test + void test_parseMermaid_Source_Light() { + String source = """ + flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car]"""; + + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Light); + parser.parseMermaid(context, null, tokens("mermaid", source)); + assertEquals("https://mermaid.ink/svg/pako:eJxVj70OgjAUhV_lppMm8gIMJlKUhUQHtspwAxfbSH9SaoihvLsgi571-85JzgSssS2xlHW9HRuJPkCV3w0sOQkuvRqCxqGGJDnGggJoa-gdIdsVFgZpnVPmsd_8bJWAT-WqEQSpzHPeEP_2r4Yi5KJEF6yrf0k12ghnoW5ymf8n0tPSuogO0w6TBj1w9DU7ANPkNaqWpRMLkvR6oqUOX31g8_wBLY9E1w==", imageView.getContentLight()); + } + + @Test + void test_parseMermaid_Source_Dark() { + String source = """ + flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car]"""; + + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Dark); + parser.parseMermaid(context, null, tokens("mermaid", source)); + assertEquals("https://mermaid.ink/svg/pako:eJxVj70OgjAUhV_lppMm8gIMJlKUhUQHtspwAxfbSH9SaoihvLsgi571-85JzgSssS2xlHW9HRuJPkCV3w0sOQkuvRqCxqGGJDnGggJoa-gdIdsVFgZpnVPmsd_8bJWAT-WqEQSpzHPeEP_2r4Yi5KJEF6yrf0k12ghnoW5ymf8n0tPSuogO0w6TBj1w9DU7ANPkNaqWpRMLkvR6oqUOX31g8_wBLY9E1w==", imageView.getContentDark()); + } + @Test void test_parseMermaid_WithViewKey() { ImageViewDslContext context = new ImageViewDslContext(imageView); @@ -125,6 +223,58 @@ void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { } } + @Test + void test_parseKroki_Source() { + String source = """ + flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car]"""; + + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + parser = new ImageViewContentParser(true); + parser.parseKroki(new ImageViewDslContext(imageView), null, tokens("kroki", "mermaid", source)); + assertEquals("https://kroki.io/mermaid/png/eNpVjLEOwiAURXe_4o068AMOJpZqlyZ16EYYXhrwES2PAEljxH-XdtK7nnOuffIyEcYMY7uDurOSFF3KMyYNQpxKZzLM7M2rQLPvGBJxCM7fD5verA7Id79aBjI5__hsRG714E2BVvUYMgf9A8aFC1yUu1H9_gMUTW2uyuLRopgwgsSovzbHM0c=", imageView.getContent()); + } + + @Test + void test_parseKroki_Source_Light() { + String source = """ + flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car]"""; + + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Light); + parser.parseKroki(context, null, tokens("kroki", "mermaid", source)); + assertEquals("https://kroki.io/mermaid/png/eNpVjLEOwiAURXe_4o068AMOJpZqlyZ16EYYXhrwES2PAEljxH-XdtK7nnOuffIyEcYMY7uDurOSFF3KMyYNQpxKZzLM7M2rQLPvGBJxCM7fD5verA7Id79aBjI5__hsRG714E2BVvUYMgf9A8aFC1yUu1H9_gMUTW2uyuLRopgwgsSovzbHM0c=", imageView.getContentLight()); + } + + @Test + void test_parseKroki_Source_Dark() { + String source = """ + flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car]"""; + + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Dark); + parser.parseKroki(context, null, tokens("kroki", "mermaid", source)); + assertEquals("https://kroki.io/mermaid/png/eNpVjLEOwiAURXe_4o068AMOJpZqlyZ16EYYXhrwES2PAEljxH-XdtK7nnOuffIyEcYMY7uDurOSFF3KMyYNQpxKZzLM7M2rQLPvGBJxCM7fD5verA7Id79aBjI5__hsRG714E2BVvUYMgf9A8aFC1yUu1H9_gMUTW2uyuLRopgwgsSovzbHM0c=", imageView.getContentDark()); + } + @Test void test_parseImage_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { @@ -136,4 +286,29 @@ void test_parseImage_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { } } + @Test + void test_parseImage() { + parser = new ImageViewContentParser(true); + parser.parseImage(new ImageViewDslContext(imageView), null, tokens("image", "https://example.com/image.png")); + assertEquals("https://example.com/image.png", imageView.getContent()); + } + + @Test + void test_parseImage_Url_Light() { + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Light); + parser.parseImage(context, null, tokens("image", "https://example.com/image.png")); + assertEquals("https://example.com/image.png", imageView.getContentLight()); + } + + @Test + void test_parseImage_Url_Dark() { + parser = new ImageViewContentParser(true); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Dark); + parser.parseImage(context, null, tokens("image", "https://example.com/image.png")); + assertEquals("https://example.com/image.png", imageView.getContentDark()); + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewDslContextTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewDslContextTests.java new file mode 100644 index 000000000..b0e450edb --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewDslContextTests.java @@ -0,0 +1,31 @@ +package com.structurizr.dsl; + +import com.structurizr.view.ImageView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ImageViewDslContextTests extends AbstractTests { + + @Test + void end_ThrowsAnException_WhenThereIsNoContent() { + try { + ImageView imageView = workspace.getViews().createImageView("key"); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.end(); + fail(); + } catch (Exception e) { + assertEquals("The image view \"key\" has no content", e.getMessage()); + } + } + + @Test + void end_WhenThereIsContent() { + ImageView imageView = workspace.getViews().createImageView("key"); + imageView.setContent("http://example.com/image.png"); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.end(); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index cf0ef4407..2073a6fc1 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -328,6 +328,16 @@ workspace "Name" "Description" { default } + image * { + light { + image logo.png + } + dark { + image logo.png + } + image logo.png + } + styles { element "Element" { shape roundedbox diff --git a/structurizr-dsl/src/test/resources/dsl/text-block.dsl b/structurizr-dsl/src/test/resources/dsl/text-block.dsl index a96042760..1b442a781 100644 --- a/structurizr-dsl/src/test/resources/dsl/text-block.dsl +++ b/structurizr-dsl/src/test/resources/dsl/text-block.dsl @@ -1,12 +1,31 @@ workspace { - model { - softwareSystem = softwareSystem "Name" { - description """ - - Line 1 - - Line 2 - - Line 3 + views { + properties { + "plantuml.url" "https://plantuml.com/plantuml" + } + + !const SOURCE """ + class MyClass """ + + !var STYLES """ + + """ + + image * "image" { + plantuml """ + @startuml + + ${STYLES} + + ${SOURCE} + @enduml + """ } } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java index 6708f0f06..ea1584d6c 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java @@ -2,6 +2,7 @@ import com.structurizr.importer.diagrams.AbstractDiagramImporter; import com.structurizr.util.ImageUtils; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import java.io.File; @@ -14,12 +15,20 @@ public class ImageImporter extends AbstractDiagramImporter { public static final String IMAGE_INLINE_PROPERTY = "image.inline"; public void importDiagram(ImageView view, File file) throws Exception { + importDiagram(view, file, null); + } + + public void importDiagram(ImageView view, File file, ColorScheme colorScheme) throws Exception { view.setContent(ImageUtils.getImageAsDataUri(file)); view.setContentType(ImageUtils.getContentType(file)); view.setTitle(file.getName()); } public void importDiagram(ImageView view, String url) throws Exception { + importDiagram(view, url, null); + } + + public void importDiagram(ImageView view, String url, ColorScheme colorScheme) throws Exception { String inline = getViewOrViewSetProperty(view, IMAGE_INLINE_PROPERTY); if ("true".equals(inline)) { String imageFormat = ImageUtils.getContentType(url); @@ -28,14 +37,15 @@ public void importDiagram(ImageView view, String url) throws Exception { } if (imageFormat.equals(CONTENT_TYPE_IMAGE_SVG)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), false)); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), false), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), false)); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), false), colorScheme); } view.setContentType(imageFormat); } else { - view.setContent(url); + view.setContent(url, colorScheme); + view.setContentType(ImageUtils.getContentType(url)); } view.setTitle(url.substring(url.lastIndexOf("/")+1)); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java index 67b3d4413..d47ff0c30 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java @@ -3,6 +3,7 @@ import com.structurizr.importer.diagrams.AbstractDiagramImporter; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import java.io.File; @@ -17,13 +18,21 @@ public class KrokiImporter extends AbstractDiagramImporter { public static final String KROKI_INLINE_PROPERTY = "kroki.inline"; public void importDiagram(ImageView view, String format, File file) throws Exception { + importDiagram(view, format, file, null); + } + + public void importDiagram(ImageView view, String format, File file, ColorScheme colorScheme) throws Exception { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); view.setTitle(file.getName()); - importDiagram(view, format, content); + importDiagram(view, format, content, colorScheme); } public void importDiagram(ImageView view, String format, String content) throws Exception { + importDiagram(view, format, content, null); + } + + public void importDiagram(ImageView view, String format, String content, ColorScheme colorScheme) throws Exception { String krokiServer = getViewOrViewSetProperty(view, KROKI_URL_PROPERTY); if (StringUtils.isNullOrEmpty(krokiServer)) { throw new IllegalArgumentException("Please define a view/viewset property named " + KROKI_URL_PROPERTY + " to specify your Kroki server"); @@ -44,12 +53,12 @@ public void importDiagram(ImageView view, String format, String content) throws String inline = getViewOrViewSetProperty(view, KROKI_INLINE_PROPERTY); if ("true".equals(inline)) { if (imageFormat.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true)); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true)); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true), colorScheme); } } else { - view.setContent(url); + view.setContent(url, colorScheme); } view.setContentType(CONTENT_TYPES_BY_FORMAT.get(imageFormat)); } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java index ab1f1a00c..49711aef9 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java @@ -3,6 +3,7 @@ import com.structurizr.importer.diagrams.AbstractDiagramImporter; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import java.io.File; @@ -18,13 +19,21 @@ public class MermaidImporter extends AbstractDiagramImporter { public static final String MERMAID_INLINE_PROPERTY = "mermaid.inline"; public void importDiagram(ImageView view, File file) throws Exception { + importDiagram(view, file, null); + } + + public void importDiagram(ImageView view, File file, ColorScheme colorScheme) throws Exception { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); view.setTitle(file.getName()); - importDiagram(view, content); + importDiagram(view, content, colorScheme); } public void importDiagram(ImageView view, String content) throws Exception { + importDiagram(view, content, null); + } + + public void importDiagram(ImageView view, String content, ColorScheme colorScheme) throws Exception { String mermaidServer = getViewOrViewSetProperty(view, MERMAID_URL_PROPERTY); if (StringUtils.isNullOrEmpty(mermaidServer)) { throw new IllegalArgumentException("Please define a view/viewset property named " + MERMAID_URL_PROPERTY + " to specify your Mermaid server"); @@ -55,12 +64,12 @@ public void importDiagram(ImageView view, String content) throws Exception { String inline = getViewOrViewSetProperty(view, MERMAID_INLINE_PROPERTY); if ("true".equals(inline)) { if (format.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true)); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true)); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true), colorScheme); } } else { - view.setContent(url); + view.setContent(url, colorScheme); } view.setContentType(CONTENT_TYPES_BY_FORMAT.get(format)); } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java index 28ecf1c3e..33605447a 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -4,6 +4,7 @@ import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; import com.structurizr.util.Url; +import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import java.io.File; @@ -20,13 +21,21 @@ public class PlantUMLImporter extends AbstractDiagramImporter { private static final String NEWLINE = "\n"; public void importDiagram(ImageView view, File file) throws Exception { + importDiagram(view, file, null); + } + + public void importDiagram(ImageView view, File file, ColorScheme colorScheme) throws Exception { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); view.setTitle(file.getName()); - importDiagram(view, content); + importDiagram(view, content, colorScheme); } public void importDiagram(ImageView view, String content) throws Exception { + importDiagram(view, content, null); + } + + public void importDiagram(ImageView view, String content, ColorScheme colorScheme) throws Exception { String plantUMLServer = getViewOrViewSetProperty(view, PLANTUML_URL_PROPERTY); if (StringUtils.isNullOrEmpty(plantUMLServer)) { throw new IllegalArgumentException("Please define a view/viewset property named " + PLANTUML_URL_PROPERTY + " to specify your PlantUML server"); @@ -47,12 +56,12 @@ public void importDiagram(ImageView view, String content) throws Exception { String inline = getViewOrViewSetProperty(view, PLANTUML_INLINE_PROPERTY); if ("true".equals(inline)) { if (format.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true)); + view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true)); + view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true), colorScheme); } } else { - view.setContent(url); + view.setContent(url, colorScheme); } view.setContentType(CONTENT_TYPES_BY_FORMAT.get(format)); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java index 4a916217f..1ea507825 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java @@ -11,13 +11,14 @@ public class ImageImporterTests { @Test + @Tag("IntegrationTest") public void importDiagram_Url() throws Exception { Workspace workspace = new Workspace("Name", "Description"); ImageView view = workspace.getViews().createImageView("key"); new ImageImporter().importDiagram(view, "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png"); assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png", view.getContent()); - assertNull(view.getContentType()); + assertEquals("image/png", view.getContentType()); assertEquals("alexa-for-business.png", view.getTitle()); } From d9e71d770b6a7233b7ce7ba38eead9cd9da7bc02 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 19 Sep 2025 13:28:38 +0100 Subject: [PATCH 382/418] . --- .../StructurizrPlantUMLDiagramExporterTests.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 6e16d8b9b..5f2218db0 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -3171,16 +3171,4 @@ public void amazonWebServicesExample_Dark() throws Exception { @enduml""", diagram.getLegend().getDefinition()); } - @Test - void c4model() throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("/Users/simon/Desktop/c4model/c4model/01-original/workspace.json")); - StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); - - View view = workspace.getViews().getViewWithKey("Dynamic-SignIn-Collaboration"); - - Diagram diagram = exporter.export((DynamicView)view); - System.out.println(diagram.getDefinition()); - - } - } \ No newline at end of file From 428bd25ae1b2ac51946eaf764b21bc0b8945c9d9 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 23 Sep 2025 14:39:51 +0100 Subject: [PATCH 383/418] Typo. --- .../src/main/java/com/structurizr/view/ViewSet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java index ddb5241a2..334cf0698 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -792,7 +792,7 @@ public Collection getImageViews() { return new TreeSet<>(imageViews); } - void setImageView(Set imageViews) { + void setImageViews(Set imageViews) { if (imageViews != null) { this.imageViews = new TreeSet<>(imageViews); } From 831cbff4fdee4171db1334c62e17ccede711601c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 23 Sep 2025 17:54:18 +0100 Subject: [PATCH 384/418] structurizr-autolayout: Adds support for custom padding view/viewset properties: `structurizr.groupPadding`,`structurizr.boundaryPadding`, and `structurizr.deploymentNodePadding`. --- changelog.md | 1 + .../autolayout/graphviz/DOTExporter.java | 365 +----------------- .../autolayout/graphviz/DOTExporterTests.java | 139 ++++++- 3 files changed, 139 insertions(+), 366 deletions(-) diff --git a/changelog.md b/changelog.md index be3db6f30..ab7ecffea 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## v5.0.0 (unreleased) +- structurizr-autolayout: Adds support for custom padding view/viewset properties: `structurizr.groupPadding`,`structurizr.boundaryPadding`, and `structurizr.deploymentNodePadding`. - structurizr-core: Removes support for deprecated enterprise and location concepts. - structurizr-core: Adds support for filtered deployment views (https://github.com/structurizr/java/issues/409). - structurizr-core: Adds support for separate images for light and dark color schemes. diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java index a28303f2b..489cf1bb6 100644 --- a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java @@ -17,7 +17,10 @@ */ class DOTExporter extends AbstractDiagramExporter { - private static final int CLUSTER_INTERNAL_MARGIN = 25; + private static final String DEFAULT_CLUSTER_INTERNAL_PADDING = "25"; + private static final String GROUP_PADDING_PROPERTY_NAME = "structurizr.groupPadding"; + private static final String BOUNDARY_PADDING_PROPERTY_NAME = "structurizr.boundaryPadding"; + private static final String DEPLOYMENT_NODE_PADDING_PROPERTY_NAME = "structurizr.deploymentNodePadding"; private Locale locale = Locale.US; private final RankDirection rankDirection; @@ -56,9 +59,8 @@ protected void writeFooter(ModelView view, IndentingWriter writer) { @Override protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { writer.writeLine("subgraph \"cluster_group_" + (groupId++) + "\" {"); - writer.indent(); - writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + writer.writeLine("margin=" + Integer.parseInt(getViewOrViewSetProperty(view, GROUP_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); } @Override @@ -72,7 +74,7 @@ protected void endGroupBoundary(ModelView view, IndentingWriter writer) { protected void startSoftwareSystemBoundary(ModelView view, SoftwareSystem softwareSystem, IndentingWriter writer) { writer.writeLine(String.format("subgraph cluster_%s {", softwareSystem.getId())); writer.indent(); - writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + writer.writeLine("margin=" + Integer.parseInt(getViewOrViewSetProperty(view, BOUNDARY_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); } @Override @@ -86,7 +88,7 @@ protected void endSoftwareSystemBoundary(ModelView view, IndentingWriter writer) protected void startContainerBoundary(ModelView view, Container container, IndentingWriter writer) { writer.writeLine(String.format("subgraph cluster_%s {", container.getId())); writer.indent(); - writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + writer.writeLine("margin=" + Integer.parseInt(getViewOrViewSetProperty(view, BOUNDARY_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); } @Override @@ -100,7 +102,7 @@ protected void endContainerBoundary(ModelView view, IndentingWriter writer) { protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { writer.writeLine(String.format("subgraph cluster_%s {", deploymentNode.getId())); writer.indent(); - writer.writeLine("margin=" + CLUSTER_INTERNAL_MARGIN); + writer.writeLine("margin=" + Integer.parseInt(getViewOrViewSetProperty(view, DEPLOYMENT_NODE_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); } @Override @@ -175,350 +177,6 @@ protected Diagram createDiagram(ModelView view, String definition) { return new DOTDiagram(view, definition); } -// private void write(ModelView view, boolean enterpriseBoundaryIsVisible) throws Exception { -// File file = new File(path, view.getKey() + ".dot"); -// FileWriter fileWriter = new FileWriter(file); -// writeHeader(fileWriter, view); -// -// if (enterpriseBoundaryIsVisible) { -// fileWriter.write(" subgraph cluster_enterprise {\n"); -// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// Set elementsInsideEnterpriseBoundary = new LinkedHashSet<>(); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() == Location.Internal) { -// elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); -// } -// if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() == Location.Internal) { -// elementsInsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); -// } -// } -// writeElements(view, " ", elementsInsideEnterpriseBoundary, fileWriter); -// fileWriter.write(" }\n\n"); -// -// Set elementsOutsideEnterpriseBoundary = new LinkedHashSet<>(); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement() instanceof Person && ((Person)elementView.getElement()).getLocation() != Location.Internal) { -// elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); -// } -// if (elementView.getElement() instanceof SoftwareSystem && ((SoftwareSystem)elementView.getElement()).getLocation() != Location.Internal) { -// elementsOutsideEnterpriseBoundary.add((StaticStructureElement)elementView.getElement()); -// } -// if (elementView.getElement() instanceof CustomElement) { -// elementsOutsideEnterpriseBoundary.add((CustomElement)elementView.getElement()); -// } -// } -// -// writeElements(view, " ", elementsOutsideEnterpriseBoundary, fileWriter); -// } else { -// Set elements = new LinkedHashSet<>(); -// for (ElementView elementView : view.getElements()) { -// elements.add((GroupableElement)elementView.getElement()); -// } -// writeElements(view, " ", elements, fileWriter); -// } -// -// writeRelationships(view, fileWriter); -// writeFooter(fileWriter); -// fileWriter.close(); -// } -// -// void write(ContainerView view) throws Exception { -// File file = new File(path, view.getKey() + ".dot"); -// FileWriter fileWriter = new FileWriter(file); -// writeHeader(fileWriter, view); -// -// Set softwareSystems = new HashSet<>(); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() instanceof SoftwareSystem) { -// softwareSystems.add((SoftwareSystem)elementView.getElement().getParent()); -// } -// } -// List sortedSoftwareSystems = new ArrayList<>(softwareSystems); -// sortedSoftwareSystems.sort(Comparator.comparing(Element::getId)); -// -// for (SoftwareSystem softwareSystem : sortedSoftwareSystems) { -// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", softwareSystem.getId())); -// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// -// Set scopedElements = new LinkedHashSet<>(); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() == softwareSystem) { -// scopedElements.add((StaticStructureElement) elementView.getElement()); -// } -// } -// writeElements(view, " ", scopedElements, fileWriter); -// fileWriter.write(" }\n"); -// -// } -// -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() == null) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } -// -// writeRelationships(view, fileWriter); -// -// writeFooter(fileWriter); -// fileWriter.close(); -// } -// -// void write(ComponentView view) throws Exception { -// File file = new File(path, view.getKey() + ".dot"); -// FileWriter fileWriter = new FileWriter(file); -// writeHeader(fileWriter, view); -// -// Set containers = new HashSet<>(); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() instanceof Container) { -// containers.add((Container)elementView.getElement().getParent()); -// } -// } -// List sortedContainers = new ArrayList<>(containers); -// sortedContainers.sort(Comparator.comparing(Element::getId)); -// -// for (Container container : sortedContainers) { -// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", container.getId())); -// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// -// Set scopedElements = new LinkedHashSet<>(); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() == container) { -// scopedElements.add((StaticStructureElement) elementView.getElement()); -// } -// } -// writeElements(view, " ", scopedElements, fileWriter); -// fileWriter.write(" }\n"); -// } -// -// for (ElementView elementView : view.getElements()) { -// if (!(elementView.getElement().getParent() instanceof Container)) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } -// -// writeRelationships(view, fileWriter); -// -// writeFooter(fileWriter); -// fileWriter.close(); -// } -// -// void write(DynamicView view) throws Exception { -// File file = new File(path, view.getKey() + ".dot"); -// FileWriter fileWriter = new FileWriter(file); -// writeHeader(fileWriter, view); -// -// Element element = view.getElement(); -// -// if (element == null) { -// for (ElementView elementView : view.getElements()) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } else if (element instanceof SoftwareSystem) { -// List softwareSystems = new ArrayList<>(view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Container).map(c -> ((Container)c).getSoftwareSystem()).collect(Collectors.toSet())); -// softwareSystems.sort(Comparator.comparing(Element::getId)); -// -// for (SoftwareSystem softwareSystem : softwareSystems) { -// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", softwareSystem.getId())); -// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() == softwareSystem) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } -// fileWriter.write(" }\n"); -// } -// -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() == null) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } -// } else if (element instanceof Container) { -// List containers = new ArrayList<>(view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Component).map(c -> ((Component)c).getContainer()).collect(Collectors.toSet())); -// containers.sort(Comparator.comparing(Element::getId)); -// -// for (Container container : containers) { -// fileWriter.write(String.format(locale, " subgraph cluster_%s {\n", container.getId())); -// fileWriter.write(" margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement().getParent() == container) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } -// fileWriter.write(" }\n"); -// } -// -// for (ElementView elementView : view.getElements()) { -// if (!(elementView.getElement().getParent() instanceof Container)) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } -// } -// -// writeRelationships(view, fileWriter); -// -// writeFooter(fileWriter); -// fileWriter.close(); -// } -// -// void write(DeploymentView view) throws Exception { -// File file = new File(path, view.getKey() + ".dot"); -// FileWriter fileWriter = new FileWriter(file); -// writeHeader(fileWriter, view); -// -// for (ElementView elementView : view.getElements()) { -// if (elementView.getElement() instanceof DeploymentNode && elementView.getElement().getParent() == null) { -// write(view, (DeploymentNode)elementView.getElement(), fileWriter, ""); -// } else if (elementView.getElement() instanceof CustomElement) { -// writeElement(view, " ", elementView.getElement(), fileWriter); -// } -// } -// -// writeRelationships(view, fileWriter); -// -// writeFooter(fileWriter); -// fileWriter.close(); -// } -// -// private void write(DeploymentView view, DeploymentNode deploymentNode, FileWriter fileWriter, String indent) throws Exception { -// fileWriter.write(String.format(locale, indent + "subgraph cluster_%s {\n", deploymentNode.getId())); -// fileWriter.write(indent + " margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// fileWriter.write(String.format(locale, indent + " label=\"%s: %s\"\n", deploymentNode.getId(), deploymentNode.getName())); -// -// for (DeploymentNode child : deploymentNode.getChildren()) { -// if (view.isElementInView(child)) { -// write(view, child, fileWriter, indent + " "); -// -// } -// } -// -// for (InfrastructureNode infrastructureNode : deploymentNode.getInfrastructureNodes()) { -// if (view.isElementInView(infrastructureNode)) { -// writeElement(view, indent + " ", infrastructureNode, fileWriter); -// } -// } -// -// for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { -// if (view.isElementInView(softwareSystemInstance)) { -// writeElement(view, indent + " ", softwareSystemInstance, fileWriter); -// } -// } -// -// for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { -// if (view.isElementInView(containerInstance)) { -// writeElement(view, indent + " ", containerInstance, fileWriter); -// } -// } -// -// fileWriter.write(indent + "}\n"); -// } -// -// private void writeElements(ModelView view, String padding, Set elements, Writer writer) throws Exception { -// String groupSeparator = view.getModel().getProperties().get(GROUP_SEPARATOR_PROPERTY_NAME); -// boolean nested = !StringUtils.isNullOrEmpty(groupSeparator); -// -// Set groups = new HashSet<>(); -// for (GroupableElement element : elements) { -// String group = element.getGroup(); -// -// if (!StringUtils.isNullOrEmpty(group)) { -// groups.add(group); -// -// if (nested) { -// while (group.contains(groupSeparator)) { -// group = group.substring(0, group.lastIndexOf(groupSeparator)); -// groups.add(group); -// } -// } -// } -// } -// -// List sortedGroups = new ArrayList<>(groups); -// sortedGroups.sort(String::compareTo); -// -// // first render grouped elements -// if (nested) { -// if (groups.size() > 0) { -// String context = ""; -// for (String group : sortedGroups) { -// int groupCount = group.split(groupSeparator).length; -// int contextCount = context.split(groupSeparator).length; -// -// if (groupCount > contextCount) { -// // moved from a to a/b -// // - increase padding -// padding = padding + INDENT; -// } else if (groupCount == contextCount) { -// // moved from a/b to a/c -// // - close off previous subgraph -// if (context.length() > 0) { -// writer.write(padding + "}\n"); -// } -// } else { -// // moved from a/b/c to a/b or a -// // - close off previous subgraphs -// // - close off current subgraph -// for (int i = 0; i < (contextCount - groupCount); i++) { -// writer.write(padding + "}\n"); -// padding = padding.substring(0, padding.length() - INDENT.length()); -// } -// writer.write(padding + "}\n"); -// } -// -// writer.write(padding + "subgraph cluster_group_" + groupId + " {\n"); -//// writer.write(padding + " // " + group + "\n"); -// writer.write(padding + " margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// for (GroupableElement element : elements) { -// if (group.equals(element.getGroup())) { -// writeElement(view, padding + INDENT, element, writer); -// } -// } -// groupId++; -// context = group; -// } -// -// int contextCount = context.split(groupSeparator).length; -// for (int i = 0; i < contextCount; i++) { -// writer.write(padding + "}\n"); -// padding = padding.substring(0, padding.length() - INDENT.length()); -// } -// } -// } else { -// for (String group : sortedGroups) { -// writer.write(padding + "subgraph cluster_group_" + groupId + " {\n"); -// writer.write(padding + " margin=" + CLUSTER_INTERNAL_MARGIN + "\n"); -// for (GroupableElement element : elements) { -// if (group.equals(element.getGroup())) { -// writeElement(view, padding + INDENT, element, writer); -// } -// } -// writer.write(padding + "}\n"); -// groupId++; -// } -// } -// -// // then render ungrouped elements -// for (GroupableElement element : elements) { -// if (StringUtils.isNullOrEmpty(element.getGroup())) { -// writeElement(view, padding, element, writer); -// } -// } -// } -// -// private void writeElement(ModelView view, String padding, Element element, Writer writer) throws Exception { -// writer.write(String.format(locale, "%s%s [width=%f,height=%f,fixedsize=true,id=%s,label=\"%s: %s\"]", -// padding, -// element.getId(), -// getElementWidth(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches -// getElementHeight(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches -// element.getId(), -// element.getId(), -// escape(element.getName()) -// )); -// writer.write("\n"); -// } -// private String escape(String s) { if (StringUtils.isNullOrEmpty(s)) { return s; @@ -526,13 +184,6 @@ private String escape(String s) { return s.replaceAll("\"", "\\\\\""); } } -// -// private void writeRelationships(ModelView view, Writer writer) throws Exception { -// writer.write("\n"); -// -// for (RelationshipView relationshipView : view.getRelationships()) { -// } -// } private Element findElementInside(DeploymentNode deploymentNode, ModelView view) { for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { diff --git a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java index 96bdabf8f..4e03672a6 100644 --- a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java @@ -79,7 +79,7 @@ public void test_writeSystemLandscapeView() { } @Test - public void test_writeSystemLandscapeViewWithGroupedElements() throws Exception { + public void test_writeSystemLandscapeViewWithGroupedElements() { Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); Person user = workspace.getModel().addPerson("User", ""); @@ -121,7 +121,50 @@ public void test_writeSystemLandscapeViewWithGroupedElements() throws Exception } @Test - public void test_writeSystemLandscapeViewWithNestedGroupedElements() throws Exception { + public void test_writeSystemLandscapeViewWithGroupedElementsAndGroupPadding() { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + user.setGroup("External"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.setGroup("Internal"); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + view.add(box); + view.addProperty("structurizr.groupPadding", "50"); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph "cluster_group_1" { + margin=50 + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: User"] + } + + subgraph "cluster_group_2" { + margin=50 + 3 [width=1.500000,height=1.000000,fixedsize=true,id=3,label="3: Software System"] + } + + 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] + + 2 -> 3 [id=4] + + }""", content); + } + + @Test + public void test_writeSystemLandscapeViewWithNestedGroupedElements() { Workspace workspace = new Workspace("Name", ""); workspace.getModel().addProperty("structurizr.groupSeparator", "/"); @@ -183,7 +226,7 @@ public void test_writeSystemLandscapeViewWithNestedGroupedElements() throws Exce } @Test - public void test_writeSystemLandscapeViewInGermanLocale() throws Exception { + public void test_writeSystemLandscapeViewInGermanLocale() { // ranksep=1.0 was being output as ranksep=1,0 Locale.setDefault(new Locale("de", "DE")); Workspace workspace = new Workspace("Name", ""); @@ -217,7 +260,7 @@ public void test_writeSystemLandscapeViewInGermanLocale() throws Exception { } @Test - public void test_writeSystemContextView() throws Exception { + public void test_writeSystemContextView() { Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); Person user = workspace.getModel().addPerson("User", ""); @@ -250,7 +293,7 @@ public void test_writeSystemContextView() throws Exception { @Test - public void test_writeSystemContextViewWithGroupedElements() throws Exception { + public void test_writeSystemContextViewWithGroupedElements() { Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); Person user = workspace.getModel().addPerson("User", ""); @@ -292,7 +335,7 @@ public void test_writeSystemContextViewWithGroupedElements() throws Exception { } @Test - public void test_writeContainerViewWithGroupedElementsInASingleSoftwareSystem() throws Exception { + public void test_writeContainerViewWithGroupedElementsInASingleSoftwareSystem() { Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); @@ -344,7 +387,7 @@ public void test_writeContainerViewWithGroupedElementsInASingleSoftwareSystem() } @Test - public void test_writeContainerViewWithGroupedElementsInMultipleSoftwareSystems() throws Exception { + public void test_writeContainerViewWithGroupedElementsInMultipleSoftwareSystems() { Workspace workspace = new Workspace("Name", ""); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); @@ -396,7 +439,36 @@ public void test_writeContainerViewWithGroupedElementsInMultipleSoftwareSystems( } @Test - public void test_writeComponentViewWithGroupedElements() throws Exception { + public void test_writeContainerViewWithBoundaryPadding() { + Workspace workspace = new Workspace("Name", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + softwareSystem.addContainer("Container"); + + ContainerView view = workspace.getViews().createContainerView(softwareSystem, "key"); + view.addAllElements(); + workspace.getViews().getConfiguration().addProperty("structurizr.boundaryPadding", "50"); + + DOTExporter exporter = new DOTExporter(RankDirection.TopBottom, 300, 300); + Diagram diagram = exporter.export(view); + + String content = diagram.getDefinition(); + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=TB,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph cluster_1 { + margin=50 + 2 [width=1.500000,height=1.000000,fixedsize=true,id=2,label="2: Container"] + } + + }""", content); + } + + @Test + public void test_writeComponentViewWithGroupedElements() { Workspace workspace = new Workspace("Name", ""); CustomElement box = workspace.getModel().addCustomElement("Box"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); @@ -449,7 +521,7 @@ public void test_writeComponentViewWithGroupedElements() throws Exception { } @Test - public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSeparator() throws Exception { + public void test_writeContainerViewWithGroupedElements_WithAndWithoutAGroupSeparator() { Workspace workspace = new Workspace("Name", ""); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); Container container1 = softwareSystem.addContainer("Container 1"); @@ -546,4 +618,53 @@ public void test_AmazonWebServicesExample() throws Exception { }""", diagram.getDefinition()); } + @Test + public void test_AmazonWebServicesExampleWithDeploymentNodePadding() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("src/test/resources/structurizr-54915-workspace.json")); + workspace.getViews().getConfiguration().addProperty("structurizr.deploymentNodePadding", "50"); + DOTExporter exporter = new DOTExporter(RankDirection.LeftRight, 300, 300); + Diagram diagram = exporter.export(workspace.getViews().getDeploymentViews().iterator().next()); + + assertEquals(""" + digraph { + compound=true + graph [splines=polyline,rankdir=LR,ranksep=1.0,nodesep=1.0,fontsize=5] + node [shape=box,fontsize=5] + edge [] + + subgraph cluster_5 { + margin=50 + subgraph cluster_6 { + margin=50 + subgraph cluster_12 { + margin=50 + subgraph cluster_13 { + margin=50 + 14 [width=1.500000,height=1.000000,fixedsize=true,id=14,label="14: Database"] + } + + } + + 7 [width=1.500000,height=1.000000,fixedsize=true,id=7,label="7: Route 53"] + 8 [width=1.500000,height=1.000000,fixedsize=true,id=8,label="8: Elastic Load Balancer"] + subgraph cluster_9 { + margin=50 + subgraph cluster_10 { + margin=50 + 11 [width=1.500000,height=1.000000,fixedsize=true,id=11,label="11: Web Application"] + } + + } + + } + + } + + 11 -> 14 [id=15] + 7 -> 8 [id=16] + 8 -> 11 [id=17] + + }""", diagram.getDefinition()); + } + } \ No newline at end of file From f3b721341a1beaf1434272f4e0061fa578934618 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 24 Sep 2025 15:35:29 +0100 Subject: [PATCH 385/418] Allows the order to be changed. --- .../src/main/java/com/structurizr/view/View.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/structurizr-core/src/main/java/com/structurizr/view/View.java b/structurizr-core/src/main/java/com/structurizr/view/View.java index 54588d8cf..b4132bcd7 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/View.java +++ b/structurizr-core/src/main/java/com/structurizr/view/View.java @@ -84,7 +84,12 @@ public int getOrder() { return order; } - void setOrder(int order) { + /** + * Sets the order of this view. + * + * @param order a positive integer + */ + public void setOrder(int order) { this.order = Math.max(1, order); } From aceacb974e112aed21b38bd1b114f89f111deb13 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 24 Sep 2025 15:35:50 +0100 Subject: [PATCH 386/418] Adds a way to remove views from the workspace. --- .../java/com/structurizr/view/ViewSet.java | 43 ++++++ .../com/structurizr/view/ViewSetTests.java | 137 ++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java index 334cf0698..c91c3c4e3 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -630,6 +630,49 @@ public View getViewWithKey(String key) { return getViews().stream().filter(v -> key.equals(v.getKey())).findFirst().orElse(null); } + /** + * Removes the view with the specified key. + * + * @param key the key + * @throws IllegalArgumentException if a view with the specified key could not be found + */ + public void removeViewWithKey(String key) { + if (StringUtils.isNullOrEmpty(key)) { + throw new IllegalArgumentException("A view key must be specified."); + } + + View view = getViewWithKey(key); + if (view == null) { + throw new IllegalArgumentException("A view with key \"" + key + "\" does not exist."); + } + + for (FilteredView filteredView : filteredViews) { + if (filteredView.getBaseViewKey().equals(key)) { + throw new IllegalArgumentException("A filtered view based upon \"" + key + "\" exists - please remove this first."); + } + } + + if (view instanceof CustomView) { + customViews.remove(view); + } else if (view instanceof SystemLandscapeView) { + systemLandscapeViews.remove(view); + } else if (view instanceof SystemContextView) { + systemContextViews.remove(view); + } else if (view instanceof ContainerView) { + containerViews.remove(view); + } else if (view instanceof ComponentView) { + componentViews.remove(view); + } else if (view instanceof DynamicView) { + dynamicViews.remove(view); + } else if (view instanceof DeploymentView) { + deploymentViews.remove(view); + } else if (view instanceof ImageView) { + imageViews.remove(view); + } else if (view instanceof FilteredView) { + filteredViews.remove(view); + } + } + /** * Finds the filtered view with the specified key, or null if the view does not exist. * diff --git a/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java index d862adf33..4bec2222f 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java @@ -1090,4 +1090,141 @@ public void createDefaultViews_ForSoftwareSystemsWithNamesUsingUTF8Characters() assertSame(ss3, workspace.getViews().getSystemContextViews().stream().filter(v -> v.getKey().equals("SystemContext-003")).findFirst().get().getSoftwareSystem()); } + @Test + void removeViewWithKey_ThrowsAndException_WhenNoKeyIsSpecified() { + try { + new Workspace("Name").getViews().removeViewWithKey(null); + fail(); + } catch (Exception e) { + assertEquals("A view key must be specified.", e.getMessage()); + } + + try { + new Workspace("Name").getViews().removeViewWithKey(""); + fail(); + } catch (Exception e) { + assertEquals("A view key must be specified.", e.getMessage()); + } + } + + @Test + void removeViewWithKey_ThrowsAndException_WhenNoViewExists() { + try { + new Workspace("Name").getViews().removeViewWithKey("key"); + fail(); + } catch (Exception e) { + assertEquals("A view with key \"key\" does not exist.", e.getMessage()); + } + } + + @Test + void removeViewWithKey_ThrowsAndException_WhenABaseViewExistsForTheSpecifiedFilteredView() { + Workspace workspace = new Workspace("Name"); + + try { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("landscape"); + workspace.getViews().createFilteredView(view, "filtered", FilterMode.Include, Tags.ELEMENT); + + workspace.getViews().removeViewWithKey("landscape"); + fail(); + } catch (Exception e) { + assertEquals("A filtered view based upon \"landscape\" exists - please remove this first.", e.getMessage()); + } + } + + @Test + void removeViewWithKey_CustomView() { + Workspace workspace = new Workspace("Name"); + workspace.getViews().createCustomView("key", "title"); + + assertFalse(workspace.getViews().getCustomViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getCustomViews().isEmpty()); + } + + @Test + void removeViewWithKey_SystemLandscapeView() { + Workspace workspace = new Workspace("Name"); + workspace.getViews().createSystemLandscapeView("key"); + + assertFalse(workspace.getViews().getSystemLandscapeViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getSystemLandscapeViews().isEmpty()); + } + + @Test + void removeViewWithKey_SystemContextView() { + Workspace workspace = new Workspace("Name"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + workspace.getViews().createSystemContextView(softwareSystem, "key"); + + assertFalse(workspace.getViews().getSystemContextViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getSystemContextViews().isEmpty()); + } + + @Test + void removeViewWithKey_ContainerView() { + Workspace workspace = new Workspace("Name"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + workspace.getViews().createContainerView(softwareSystem, "key"); + + assertFalse(workspace.getViews().getContainerViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getContainerViews().isEmpty()); + } + + @Test + void removeViewWithKey_ComponentView() { + Workspace workspace = new Workspace("Name"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + Container container = softwareSystem.addContainer("Name"); + workspace.getViews().createComponentView(container, "key"); + + assertFalse(workspace.getViews().getComponentViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getComponentViews().isEmpty()); + } + + @Test + void removeViewWithKey_DynamicView() { + Workspace workspace = new Workspace("Name"); + workspace.getViews().createDynamicView("key"); + + assertFalse(workspace.getViews().getDynamicViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getDynamicViews().isEmpty()); + } + + @Test + void removeViewWithKey_DeploymentView() { + Workspace workspace = new Workspace("Name"); + workspace.getViews().createDeploymentView("key"); + + assertFalse(workspace.getViews().getDeploymentViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getDeploymentViews().isEmpty()); + } + + @Test + void removeViewWithKey_FilteredView() { + Workspace workspace = new Workspace("Name"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("landscape"); + workspace.getViews().createFilteredView(view, "key", FilterMode.Include, Tags.ELEMENT); + + assertFalse(workspace.getViews().getFilteredViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getFilteredViews().isEmpty()); + } + + @Test + void removeViewWithKey_ImageView() { + Workspace workspace = new Workspace("Name"); + workspace.getViews().createImageView("key"); + + assertFalse(workspace.getViews().getImageViews().isEmpty()); + workspace.getViews().removeViewWithKey("key"); + assertTrue(workspace.getViews().getImageViews().isEmpty()); + } + } \ No newline at end of file From c8ba7f90be3ce065451164ecd782e1d2565a635a Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 24 Sep 2025 15:36:32 +0100 Subject: [PATCH 387/418] Better rendering for sequence diagrams. --- .../plantuml/StructurizrPlantUMLExporter.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 18238eb08..1cec09214 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -34,7 +34,7 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { plantUMLStyles = new HashSet<>(); super.writeHeader(view, writer); - if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + if (renderAsSequenceDiagram(view)) { // do nothing } else { if (view.getAutomaticLayout() != null) { @@ -324,7 +324,6 @@ public Diagram export(DynamicView view) { @Override protected void writeElement(ModelView view, Element element, IndentingWriter writer) { ElementStyle elementStyle = findElementStyle(view, element); - PlantUMLElementStyle plantUMLElementStyle = new PlantUMLElementStyle( elementStyle.getTag(), elementStyle.getShape(), @@ -332,7 +331,7 @@ protected void writeElement(ModelView view, Element element, IndentingWriter wri elementStyle.getBackground(), elementStyle.getColor(), elementStyle.getStroke(), - elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH, + renderAsSequenceDiagram(view) ? DEFAULT_STROKE_WIDTH : elementStyle.getStrokeWidth() != null ? elementStyle.getStrokeWidth() : DEFAULT_STROKE_WIDTH, elementStyle.getBorder(), elementStyle.getFontSize(), elementStyle.getIcon(), @@ -342,7 +341,7 @@ protected void writeElement(ModelView view, Element element, IndentingWriter wri int metadataFontSize = calculateMetadataFontSize(elementStyle.getFontSize()); - if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + if (renderAsSequenceDiagram(view)) { writer.writeLine(String.format("%s \"%s\\n%s\" as %s <<%s>> %s", plantumlSequenceType(view, element), element.getName(), @@ -424,7 +423,7 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi style.getTag(), style.getColor(), style.getStyle(), - style.getThickness(), + renderAsSequenceDiagram(view) ? DEFAULT_STROKE_WIDTH : style.getThickness(), style.getFontSize() ); plantUMLStyles.add(plantUMLRelationshipStyle); @@ -432,7 +431,7 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi String description = ""; String technology = relationship.getTechnology(); - if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + if (renderAsSequenceDiagram(view)) { // do nothing - sequence diagrams don't need the order } else { if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { @@ -442,7 +441,7 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi description += (hasValue(relationshipView.getDescription()) ? relationshipView.getDescription() : hasValue(relationshipView.getRelationship().getDescription()) ? relationshipView.getRelationship().getDescription() : ""); - if (view instanceof DynamicView && renderAsSequenceDiagram(view)) { + if (renderAsSequenceDiagram(view)) { String arrowStart = "-"; String arrowEnd = ">"; From 837fe69b3af77f6004777c6da6fe78f77df766b5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 26 Sep 2025 11:25:05 +0100 Subject: [PATCH 388/418] structurizr-export: StructurizrPlantUMLExporter - adds technology to sequence diagrams. Closes #425. --- changelog.md | 1 + .../export/plantuml/StructurizrPlantUMLExporter.java | 12 ++++-------- .../StructurizrPlantUMLDiagramExporterTests.java | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/changelog.md b/changelog.md index ab7ecffea..edf6b0519 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,7 @@ - structurizr-export: Removes support for deprecated enterprise and location concepts. - structurizr-export: PlantUML exporters - replaces skinparams with styles. - structurizr-export: PlantUML exporters - adds support for dark mode exports. +- structurizr-export: StructurizrPlantUMLExporter - adds technology to sequence diagrams (https://github.com/structurizr/java/issues/425) - structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, `kroki.inline`, and `image.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 1cec09214..5553d6117 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -428,6 +428,7 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi ); plantUMLStyles.add(plantUMLRelationshipStyle); + int metadataFontSize = calculateMetadataFontSize(style.getFontSize()); String description = ""; String technology = relationship.getTechnology(); @@ -451,13 +452,14 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi } writer.writeLine( - String.format("%s %s%s %s <<%s>> : %s", + String.format("%s %s%s %s <<%s>> : %s%s", idOf(relationship.getSource()), arrowStart, arrowEnd, idOf(relationship.getDestination()), plantUMLRelationshipStyle.getClassSelector(), - description)); + description, + (StringUtils.isNullOrEmpty(technology) ? "" : "\\n[" + technology + "]"))); } else { String arrow; @@ -467,12 +469,6 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi arrow = "-->"; } -// if (!isVisible(view, relationshipView)) { -// relationshipStyle = "hidden"; -// } - - int metadataFontSize = calculateMetadataFontSize(style.getFontSize()); - // 1 --> 2 : "...\n... writer.writeLine(format("%s %s %s <<%s>> : \"%s%s\"", idOf(relationship.getSource()), diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 5f2218db0..846fa0f3d 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -647,7 +647,7 @@ public void dynamicView_SequenceStyle_NoStyling_Light() { SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); - a.uses(b, "Uses"); + a.uses(b, "Uses", "JSON/HTTPS"); DynamicView view = workspace.getViews().createDynamicView("key", "Description"); view.add(a, b); @@ -692,8 +692,8 @@ public void dynamicView_SequenceStyle_NoStyling_Light() { participant "A\\n[Software System]" as A <> #ffffff participant "B\\n[Software System]" as B <> #ffffff - - A -> B <> : Uses + + A -> B <> : 1. Uses\\n[JSON/HTTPS] @enduml""", diagram.getDefinition()); From cb57d23ef1a814b9be8a381143f1aff8bd71c1f4 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 26 Sep 2025 11:29:55 +0100 Subject: [PATCH 389/418] structurizr-export: PlantUML exporters - adds order number to relationships in sequence diagrams. Closes #431. --- changelog.md | 1 + .../export/plantuml/C4PlantUMLExporter.java | 8 ++---- .../plantuml/StructurizrPlantUMLExporter.java | 8 ++---- .../C4PlantUMLDiagramExporterTests.java | 24 ++++++++--------- ...ructurizrPlantUMLDiagramExporterTests.java | 26 +++++++++---------- 5 files changed, 30 insertions(+), 37 deletions(-) diff --git a/changelog.md b/changelog.md index edf6b0519..ec4b6548e 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,7 @@ - structurizr-export: Removes support for deprecated enterprise and location concepts. - structurizr-export: PlantUML exporters - replaces skinparams with styles. - structurizr-export: PlantUML exporters - adds support for dark mode exports. +- structurizr-export: PlantUML exporters - adds order number to relationships in sequence diagrams. - structurizr-export: StructurizrPlantUMLExporter - adds technology to sequence diagrams (https://github.com/structurizr/java/issues/425) - structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, `kroki.inline`, and `image.inline` properties to inline the resulting PNG/SVG file into the workspace. - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index ef3c2a718..58bb31d28 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -611,12 +611,8 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi String description = ""; - if (renderAsSequenceDiagram(view)) { - // do nothing - sequence diagrams don't need the order - } else { - if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { - description = relationshipView.getOrder() + ". "; - } + if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { + description = relationshipView.getOrder() + ": "; } description += (hasValue(relationshipView.getDescription()) ? relationshipView.getDescription() : hasValue(relationshipView.getRelationship().getDescription()) ? relationshipView.getRelationship().getDescription() : ""); diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 5553d6117..c71534276 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -432,12 +432,8 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi String description = ""; String technology = relationship.getTechnology(); - if (renderAsSequenceDiagram(view)) { - // do nothing - sequence diagrams don't need the order - } else { - if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { - description = relationshipView.getOrder() + ". "; - } + if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { + description = relationshipView.getOrder() + ": "; } description += (hasValue(relationshipView.getDescription()) ? relationshipView.getDescription() : hasValue(relationshipView.getRelationship().getDescription()) ? relationshipView.getRelationship().getDescription() : ""); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index 784c6fe7a..a8d073c18 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -275,12 +275,12 @@ public void test_BigBankPlcExample() throws Exception { Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1. Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2. Validates credentials using", $techn="", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3. select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") - Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4. Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "5. Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "6. Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1: Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2: Validates credentials using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3: select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4: Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "5: Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "6: Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") SHOW_LEGEND(true) hide stereotypes @@ -470,12 +470,12 @@ public void test_BigBankPlcExample() throws Exception { Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "Validates credentials using", $techn="", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") - Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") - Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "1: Submits credentials to", $techn="JSON/HTTPS", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "2: Validates credentials using", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "3: select * from users where username = ?", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.Database, InternetBankingSystem.APIApplication.SecurityComponent, "4: Returns user data to", $techn="SQL/TCP", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.APIApplication.SignInController, "5: Returns true if the hashed password matches", $techn="", $tags="Relationship", $link="") + Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.SinglePageApplication, "6: Sends back an authentication token to", $techn="JSON/HTTPS", $tags="Relationship", $link="") SHOW_LEGEND(true) hide stereotypes diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 846fa0f3d..ea8e19b5f 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -526,7 +526,7 @@ public void dynamicView_CollaborationStyle_NoStyling_Light() { rectangle "==A\\n[Software System]" <> as A rectangle "==B\\n[Software System]" <> as B - A --> B <> : "1. Uses" + A --> B <> : "1: Uses" @enduml""", diagram.getDefinition()); } @@ -590,8 +590,8 @@ public void dynamicView_CollaborationStyle_Frames() { rectangle "==C\\n[Software System]" <> as C hide C - A --> B <> : "1. Uses" - B --> C <> : "2. Uses" + A --> B <> : "1: Uses" + B --> C <> : "2: Uses" @enduml""", frames.get(0).getDefinition()); @@ -635,8 +635,8 @@ public void dynamicView_CollaborationStyle_Frames() { rectangle "==B\\n[Software System]" <> as B rectangle "==C\\n[Software System]" <> as C - A --> B <> : "1. Uses" - B --> C <> : "2. Uses" + A --> B <> : "1: Uses" + B --> C <> : "2: Uses" @enduml""", frames.get(1).getDefinition()); } @@ -1511,7 +1511,7 @@ public void dynamicView_ExternalContainers() { rectangle "==Container 2\\n[Container]" <> as SoftwareSystem2.Container2 } - SoftwareSystem1.Container1 --> SoftwareSystem2.Container2 <> : "1. Uses" + SoftwareSystem1.Container1 --> SoftwareSystem2.Container2 <> : "1: Uses" @enduml""", diagram.getDefinition()); } @@ -1603,8 +1603,8 @@ public void dynamicView_ExternalComponents() { rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 } - SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1. Uses" - SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2. Uses" + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1: Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2: Uses" @enduml""", diagram.getDefinition()); } @@ -1725,8 +1725,8 @@ public void dynamicView_ExternalComponentsAndSoftwareSystemBoundariesIncluded() } - SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1. Uses" - SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2. Uses" + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1: Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2: Uses" @enduml""", diagram.getDefinition()); } @@ -2018,7 +2018,7 @@ public void dynamicView_UnscopedWithGroups() { } - A --> B <> : "1. Uses" + A --> B <> : "1: Uses" @enduml""", diagram.getDefinition()); } @@ -2132,7 +2132,7 @@ public void dynamicView_SoftwareSystemScopedWithGroups() { } - A.A --> B.B <> : "1. Uses" + A.A --> B.B <> : "1: Uses" @enduml""", diagram.getDefinition()); } @@ -2248,7 +2248,7 @@ public void dynamicView_ContainerScopedWithGroups() { } - A.A.A --> B.B.B <> : "1. Uses" + A.A.A --> B.B.B <> : "1: Uses" @enduml""", diagram.getDefinition()); } From ee0e4c07996659b5e809764214396a4e7592e121 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 26 Sep 2025 11:32:59 +0100 Subject: [PATCH 390/418] . --- .../plantuml/StructurizrPlantUMLDiagramExporterTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index ea8e19b5f..116cdf144 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -693,7 +693,7 @@ public void dynamicView_SequenceStyle_NoStyling_Light() { participant "A\\n[Software System]" as A <> #ffffff participant "B\\n[Software System]" as B <> #ffffff - A -> B <> : 1. Uses\\n[JSON/HTTPS] + A -> B <> : 1: Uses\\n[JSON/HTTPS] @enduml""", diagram.getDefinition()); From 83f33e951fe7684d45e2eca0d3af7e7957bea269 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 26 Sep 2025 14:01:23 +0100 Subject: [PATCH 391/418] Fixes an issue where containers could be rendered outside of their software system boundary on component views. --- .../autolayout/graphviz/DOTExporterTests.java | 20 ++-- .../export/AbstractDiagramExporter.java | 68 ++++++++---- .../export/plantuml/C4PlantUMLExporter.java | 4 +- .../export/dot/DOTDiagramExporterTests.java | 96 ++++++++++++----- .../mermaid/MermaidDiagramExporterTests.java | 49 ++++++--- .../C4PlantUMLDiagramExporterTests.java | 76 ++++++++----- ...ructurizrPlantUMLDiagramExporterTests.java | 101 +++++++++++------- 7 files changed, 271 insertions(+), 143 deletions(-) diff --git a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java index 4e03672a6..3bc79c14a 100644 --- a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java @@ -499,19 +499,23 @@ public void test_writeComponentViewWithGroupedElements() { 1 [width=1.500000,height=1.000000,fixedsize=true,id=1,label="1: Box"] - subgraph cluster_3 { + subgraph cluster_2 { margin=25 - subgraph "cluster_group_1" { + subgraph cluster_3 { margin=25 - 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label="5: Component 2"] - } + subgraph "cluster_group_1" { + margin=25 + 5 [width=1.500000,height=1.000000,fixedsize=true,id=5,label="5: Component 2"] + } - subgraph "cluster_group_2" { - margin=25 - 6 [width=1.500000,height=1.000000,fixedsize=true,id=6,label="6: Component 3"] + subgraph "cluster_group_2" { + margin=25 + 6 [width=1.500000,height=1.000000,fixedsize=true,id=6,label="6: Component 3"] + } + + 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label="4: Component 1"] } - 4 [width=1.500000,height=1.000000,fixedsize=true,id=4,label="4: Component 1"] } 4 -> 5 [id=7] diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java index 81894c514..58df90d5f 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -283,30 +283,38 @@ public Diagram export(ComponentView view, Integer animationStep) { IndentingWriter writer = new IndentingWriter(); writeHeader(view, writer); - boolean elementsWritten = false; - for (ElementView elementView : view.getElements()) { - if (!(elementView.getElement() instanceof Component)) { - writeElement(view, elementView.getElement(), writer); - elementsWritten = true; - } + List customElements = getCustomElements(view); + for (CustomElement customElement : customElements) { + writeElement(view, customElement, writer); } - - if (elementsWritten) { + if (!customElements.isEmpty()) { writer.writeLine(); } - boolean includeSoftwareSystemBoundaries = "true".equals(view.getProperties().getOrDefault("structurizr.softwareSystemBoundaries", "false")); + List people = getPeople(view); + for (Person person : people) { + writeElement(view, person, writer); + } + if (!people.isEmpty()) { + writer.writeLine(); + } - List containers = getBoundaryContainers(view); - Set softwareSystems = containers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + List softwareSystems = getSoftwareSystems(view); for (SoftwareSystem softwareSystem : softwareSystems) { + writeElement(view, softwareSystem, writer); + } + if (!softwareSystems.isEmpty()) { + writer.writeLine(); + } - if (includeSoftwareSystemBoundaries) { - startSoftwareSystemBoundary(view, softwareSystem, writer); - writer.indent(); - } + List boundaryContainers = getBoundaryContainers(view); + List containers = getContainers(view); + Set boundarySoftwareSystems = boundaryContainers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + for (SoftwareSystem softwareSystem : boundarySoftwareSystems) { - for (Container container : containers) { + startSoftwareSystemBoundary(view, softwareSystem, writer); + + for (Container container : boundaryContainers) { if (container.getSoftwareSystem() == softwareSystem) { startContainerBoundary(view, container, writer); @@ -317,10 +325,13 @@ public Diagram export(ComponentView view, Integer animationStep) { } } - if (includeSoftwareSystemBoundaries) { - endSoftwareSystemBoundary(view, writer); - writer.outdent(); + for (Container container : containers) { + if (container.getSoftwareSystem() == softwareSystem) { + writeElement(view, container, writer); + } } + + endSoftwareSystemBoundary(view, writer); } writeRelationships(view, writer); @@ -330,11 +341,24 @@ public Diagram export(ComponentView view, Integer animationStep) { return createDiagram(view, writer.toString()); } + protected List getCustomElements(ModelView view) { + return view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof CustomElement).map(c -> ((CustomElement) c)).distinct().sorted(Comparator.comparing(Element::getId)).collect(Collectors.toList()); + } + + protected List getPeople(ModelView view) { + return view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Person).map(c -> ((Person) c)).distinct().sorted(Comparator.comparing(Element::getId)).collect(Collectors.toList()); + } + + protected List getSoftwareSystems(ModelView view) { + return view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof SoftwareSystem).map(c -> ((SoftwareSystem) c)).distinct().sorted(Comparator.comparing(Element::getId)).collect(Collectors.toList()); + } + protected List getBoundaryContainers(ModelView view) { - List containers = new ArrayList<>(view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Component).map(c -> ((Component)c).getContainer()).collect(Collectors.toSet())); - containers.sort(Comparator.comparing(Element::getId)); + return view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Component).map(c -> ((Component) c).getContainer()).distinct().sorted(Comparator.comparing(Element::getId)).collect(Collectors.toList()); + } - return containers; + protected List getContainers(ModelView view) { + return view.getElements().stream().map(ElementView::getElement).filter(e -> e instanceof Container).map(c -> ((Container) c)).distinct().sorted(Comparator.comparing(Element::getId)).collect(Collectors.toList()); } public Diagram export(DynamicView view) { diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index 58bb31d28..4f309a149 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -249,8 +249,6 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { } if (!boundaryStyles.isEmpty()) { - writer.writeLine(); - for (String tagList : boundaryStyles.keySet()) { ElementStyle elementStyle = boundaryStyles.get(tagList); tagList = tagList.replaceFirst("Element,", ""); @@ -273,6 +271,8 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { line = line.replace(", $borderThickness=\"1\")", ")"); writer.writeLine(line); } + + writer.writeLine(); } } } diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java index 937af8766..35cebec48 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -148,24 +148,34 @@ public void test_BigBankPlcExample() throws Exception { 4 [id=4,shape=rect, label=<Mainframe Banking
System

[Software System]

Stores all of the core banking
information about customers,
accounts, transactions, etc.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] 5 [id=5,shape=rect, label=<E-mail System
[Software System]

The internal Microsoft
Exchange e-mail system.
>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] - 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] - subgraph cluster_11 { + subgraph cluster_7 { margin=25 - label=<
API Application

[Container: Java and Spring MVC]> + label=<
Internet Banking System

[Software System]> labelloc=b color="#444444" fontcolor="#444444" fillcolor="#444444" - 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 13 [id=13,shape=rect, label=<Accounts Summary
Controller

[Component: Spring MVC Rest Controller]

Provides customers with a
summary of their bank
accounts.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 14 [id=14,shape=rect, label=<Reset Password
Controller

[Component: Spring MVC Rest Controller]

Allows users to reset their
passwords with a single use
URL.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 16 [id=16,shape=rect, label=<Mainframe Banking
System Facade

[Component: Spring Bean]

A facade onto the mainframe
banking system.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 17 [id=17,shape=rect, label=<E-mail Component
[Component: Spring Bean]

Sends e-mails to users.>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + subgraph cluster_11 { + margin=25 + label=<
API Application

[Container: Java and Spring MVC]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 13 [id=13,shape=rect, label=<Accounts Summary
Controller

[Component: Spring MVC Rest Controller]

Provides customers with a
summary of their bank
accounts.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 14 [id=14,shape=rect, label=<Reset Password
Controller

[Component: Spring MVC Rest Controller]

Allows users to reset their
passwords with a single use
URL.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 16 [id=16,shape=rect, label=<Mainframe Banking
System Facade

[Component: Spring Bean]

A facade onto the mainframe
banking system.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 17 [id=17,shape=rect, label=<E-mail Component
[Component: Spring Bean]

Sends e-mails to users.>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + } + + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 9 [id=9,shape=rect, label=<Mobile App
[Container: Xamarin]

Provides a limited subset of
the Internet banking
functionality to customers via
their mobile device.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } 8 -> 12 [id=32, label=<Makes API calls to
[JSON/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] @@ -181,7 +191,7 @@ public void test_BigBankPlcExample() throws Exception { 15 -> 18 [id=44, label=<Reads from and writes to
[SQL/TCP]>, style="dashed", color="#444444", fontcolor="#444444"] 16 -> 4 [id=46, label=<Makes API calls to
[XML/HTTPS]>, style="dashed", color="#444444", fontcolor="#444444"] 17 -> 5 [id=48, label=<Sends e-mail using>, style="dashed", color="#444444", fontcolor="#444444"] - + }""", diagram.getDefinition()); diagram = diagrams.stream().filter(md -> md.getKey().equals("SignIn")).findFirst().get(); @@ -678,27 +688,37 @@ public void test_GroupsExample() throws Exception { 3 [id=3,shape=rect, label=<C
[Software System]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] - subgraph cluster_6 { + subgraph cluster_4 { margin=25 - label=<
F

[Container]> + label=<
D

[Software System]> labelloc=b color="#444444" fontcolor="#444444" fillcolor="#444444" - subgraph "cluster_group_Group 5" { + subgraph cluster_6 { margin=25 - label=<
Group 5
> + label=<
F

[Container]> labelloc=b - color="#cccccc" - fontcolor="#cccccc" - fillcolor="#ffffff" - style="dashed" + color="#444444" + fontcolor="#444444" + fillcolor="#444444" - 8 [id=8,shape=rect, label=<H
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + subgraph "cluster_group_Group 5" { + margin=25 + label=<
Group 5
> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 8 [id=8,shape=rect, label=<H
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 7 [id=7,shape=rect, label=<G
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] } - 7 [id=7,shape=rect, label=<G
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] } 3 -> 7 [id=13, label=<>, style="dashed", color="#444444", fontcolor="#444444"] @@ -881,26 +901,46 @@ public void test_renderComponentDiagramWithExternalComponents() { edge [fontname="Arial"] label=<
Component View: Software System 1 - Container 1> - subgraph cluster_2 { + subgraph cluster_1 { margin=25 - label=<
Container 1

[Container]> + label=<
Software System 1

[Software System]> labelloc=b color="#444444" fontcolor="#444444" fillcolor="#444444" - 3 [id=3,shape=rect, label=<Component 1
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + subgraph cluster_2 { + margin=25 + label=<
Container 1

[Container]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 3 [id=3,shape=rect, label=<Component 1
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + } - subgraph cluster_5 { + subgraph cluster_4 { margin=25 - label=<
Container 2

[Container]> + label=<
Software System 2

[Software System]> labelloc=b color="#cccccc" fontcolor="#cccccc" fillcolor="#cccccc" - 6 [id=6,shape=rect, label=<Component 2
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + subgraph cluster_5 { + margin=25 + label=<
Container 2

[Container]> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + 6 [id=6,shape=rect, label=<Component 2
[Component]>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + } 3 -> 6 [id=7, label=<Uses>, style="dashed", color="#444444", fontcolor="#444444"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java index fe176f043..31097f38b 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -169,23 +169,28 @@ public void test_GroupsExample() throws Exception { 3["
C
[Software System]
"] style 3 fill:#ffffff,stroke:#444444,color:#444444 - subgraph 6 ["F"] - style 6 fill:#ffffff,stroke:#444444,color:#444444 + subgraph 4 ["D"] + style 4 fill:#ffffff,stroke:#444444,color:#444444 - subgraph group1 ["Group 5"] - style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + subgraph 6 ["F"] + style 6 fill:#ffffff,stroke:#444444,color:#444444 + + subgraph group1 ["Group 5"] + style group1 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 - 8["
H
[Component]
"] - style 8 fill:#ffffff,stroke:#444444,color:#444444 + 8["
H
[Component]
"] + style 8 fill:#ffffff,stroke:#444444,color:#444444 + end + + 7["
G
[Component]
"] + style 7 fill:#ffffff,stroke:#444444,color:#444444 end - 7["
G
[Component]
"] - style 7 fill:#ffffff,stroke:#444444,color:#444444 end 3-. "
" .->7 3-. "
" .->8 - + end""", diagram.getDefinition()); } @@ -327,18 +332,28 @@ public void test_renderComponentDiagramWithExternalComponents() { subgraph diagram ["Component View: Software System 1 - Container 1"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 2 ["Container 1"] - style 2 fill:#ffffff,stroke:#444444,color:#444444 + subgraph 1 ["Software System 1"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + + subgraph 2 ["Container 1"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + + 3["
Component 1
[Component]
"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + end - 3["
Component 1
[Component]
"] - style 3 fill:#ffffff,stroke:#444444,color:#444444 end - subgraph 5 ["Container 2"] - style 5 fill:#ffffff,stroke:#444444,color:#444444 + subgraph 4 ["Software System 2"] + style 4 fill:#ffffff,stroke:#444444,color:#444444 + + subgraph 5 ["Container 2"] + style 5 fill:#ffffff,stroke:#444444,color:#444444 + + 6["
Component 2
[Component]
"] + style 6 fill:#ffffff,stroke:#444444,color:#444444 + end - 6["
Component 2
[Component]
"] - style 6 fill:#ffffff,stroke:#444444,color:#444444 end 3-. "
Uses
" .->6 diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index a8d073c18..dcca9468d 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -147,8 +147,8 @@ public void test_BigBankPlcExample() throws Exception { AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) - AddBoundaryTag("Software System", $bgColor="#ffffff", $borderColor="#0b4884", $fontColor="#0b4884", $shadowing="", $borderStyle="solid") + Person(PersonalBankingCustomer, "Personal Banking Customer", $descr="A customer of the bank, with personal bank accounts.", $tags="Person,Customer", $link="") System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") @@ -204,21 +204,24 @@ public void test_BigBankPlcExample() throws Exception { AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) - AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") + System(MainframeBankingSystem, "Mainframe Banking System", $descr="Stores all of the core banking information about customers, accounts, transactions, etc.", $tags="Software System,Existing System", $link="") System(EmailSystem, "E-mail System", $descr="The internal Microsoft Exchange e-mail system.", $tags="Software System,Existing System", $link="") - Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") - Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") - ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") - Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { - Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.AccountsSummaryController, "Accounts Summary Controller", $techn="Spring MVC Rest Controller", $descr="Provides customers with a summary of their bank accounts.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.ResetPasswordController, "Reset Password Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to reset their passwords with a single use URL.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, "Mainframe Banking System Facade", $techn="Spring Bean", $descr="A facade onto the mainframe banking system.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.EmailComponent, "E-mail Component", $techn="Spring Bean", $descr="Sends e-mails to users.", $tags="Component", $link="") + System_Boundary("InternetBankingSystem_boundary", "Internet Banking System", $tags="Software System") { + Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { + Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.AccountsSummaryController, "Accounts Summary Controller", $techn="Spring MVC Rest Controller", $descr="Provides customers with a summary of their bank accounts.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.ResetPasswordController, "Reset Password Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to reset their passwords with a single use URL.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, "Mainframe Banking System Facade", $techn="Spring Bean", $descr="A facade onto the mainframe banking system.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.EmailComponent, "E-mail Component", $techn="Spring Bean", $descr="Sends e-mails to users.", $tags="Component", $link="") + } + + ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") + Container(InternetBankingSystem.MobileApp, "Mobile App", $techn="Xamarin", $descr="Provides a limited subset of the Internet banking functionality to customers via their mobile device.", $tags="Container,Mobile App", $link="") } Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="") @@ -265,8 +268,8 @@ public void test_BigBankPlcExample() throws Exception { AddRelTag("Relationship", $textColor="#444444", $lineColor="#444444", $lineStyle = DashedLine()) - AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") + Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") @@ -730,13 +733,16 @@ public void test_GroupsExample() throws Exception { System(C, "C", $descr="", $tags="", $link="") - Container_Boundary("D.F_boundary", "F", $tags="") { - AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") - Boundary(group_1, "Group 5", $tags="Group 5") { - Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") + System_Boundary("D_boundary", "D", $tags="") { + Container_Boundary("D.F_boundary", "F", $tags="") { + AddBoundaryTag("Group 5", $borderColor="#cccccc", $fontColor="#cccccc", $borderStyle="dashed") + Boundary(group_1, "Group 5", $tags="Group 5") { + Component(D.F.H, "H", $techn="", $descr="", $tags="", $link="") + } + + Component(D.F.G, "G", $techn="", $descr="", $tags="", $link="") } - Component(D.F.G, "G", $techn="", $descr="", $tags="", $link="") } Rel(C, D.F.G, "", $techn="", $tags="", $link="") @@ -1057,12 +1063,18 @@ public void test_renderComponentDiagramWithExternalComponents() { !include !include - Container_Boundary("SoftwareSystem1.Container1_boundary", "Container 1", $tags="") { - Component(SoftwareSystem1.Container1.Component1, "Component 1", $techn="", $descr="", $tags="", $link="") + System_Boundary("SoftwareSystem1_boundary", "Software System 1", $tags="") { + Container_Boundary("SoftwareSystem1.Container1_boundary", "Container 1", $tags="") { + Component(SoftwareSystem1.Container1.Component1, "Component 1", $techn="", $descr="", $tags="", $link="") + } + } - Container_Boundary("SoftwareSystem2.Container2_boundary", "Container 2", $tags="") { - Component(SoftwareSystem2.Container2.Component2, "Component 2", $techn="", $descr="", $tags="", $link="") + System_Boundary("SoftwareSystem2_boundary", "Software System 2", $tags="") { + Container_Boundary("SoftwareSystem2.Container2_boundary", "Container 2", $tags="") { + Component(SoftwareSystem2.Container2.Component2, "Component 2", $techn="", $descr="", $tags="", $link="") + } + } Rel(SoftwareSystem1.Container1.Component1, SoftwareSystem2.Container2.Component2, "Uses", $techn="", $tags="", $link="") @@ -1538,11 +1550,14 @@ public void test_renderComponentShapes() throws Exception { !include !include - Container_Boundary("SoftwareSystem.Container_boundary", "Container", $tags="") { - Component(SoftwareSystem.Container.DefaultComponent, "Default Component", $techn="", $descr="", $tags="", $link="") - ComponentDb(SoftwareSystem.Container.CylinderComponent, "Cylinder Component", $techn="", $descr="", $tags="", $link="") - ComponentQueue(SoftwareSystem.Container.PipeComponent, "Pipe Component", $techn="", $descr="", $tags="", $link="") - Component(SoftwareSystem.Container.RobotComponent, "Robot Component", $techn="", $descr="", $tags="", $link="") + System_Boundary("SoftwareSystem_boundary", "Software System", $tags="") { + Container_Boundary("SoftwareSystem.Container_boundary", "Container", $tags="") { + Component(SoftwareSystem.Container.DefaultComponent, "Default Component", $techn="", $descr="", $tags="", $link="") + ComponentDb(SoftwareSystem.Container.CylinderComponent, "Cylinder Component", $techn="", $descr="", $tags="", $link="") + ComponentQueue(SoftwareSystem.Container.PipeComponent, "Pipe Component", $techn="", $descr="", $tags="", $link="") + Component(SoftwareSystem.Container.RobotComponent, "Robot Component", $techn="", $descr="", $tags="", $link="") + } + } SHOW_LEGEND(true) @@ -1647,8 +1662,11 @@ public void componentWithoutTechnology() { !include !include - Container_Boundary("Name.Name_boundary", "Name", $tags="") { - Component(Name.Name.Name, "Name", $techn="", $descr="Description", $tags="", $link="") + System_Boundary("Name_boundary", "Name", $tags="") { + Container_Boundary("Name.Name_boundary", "Name", $tags="") { + Component(Name.Name.Name, "Name", $techn="", $descr="Description", $tags="", $link="") + } + } SHOW_LEGEND(true) diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 116cdf144..0d55ea296 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -407,11 +407,25 @@ public void componentView_NoStyling_Light() { HorizontalAlignment: center; Shadowing: 0; } + // Software System + .Boundary-U29mdHdhcmUgU3lzdGVt { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } - rectangle "Container\\n[Container: Technology]" <> { - rectangle "==Component 1\\n[Component: Technology]\\n\\nDescription" <> as SoftwareSystem.Container.Component1 - rectangle "==Component 2\\n[Component: Technology]\\n\\nDescription" <> as SoftwareSystem.Container.Component2 + rectangle "Software System\\n[Software System]" <> { + rectangle "Container\\n[Container: Technology]" <> { + rectangle "==Component 1\\n[Component: Technology]\\n\\nDescription" <> as SoftwareSystem.Container.Component1 + rectangle "==Component 2\\n[Component: Technology]\\n\\nDescription" <> as SoftwareSystem.Container.Component2 + } + } SoftwareSystem.Container.Component1 --> SoftwareSystem.Container.Component2 <> : "Description\\n[Technology]" @@ -954,6 +968,17 @@ public void groups() throws Exception { FontColor: #444444; FontSize: 24; } + // D + .Boundary-RA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } // F .Boundary-Rg== { BackgroundColor: #ffffff; @@ -980,12 +1005,15 @@ public void groups() throws Exception { rectangle "==C\\n[Software System]" <> as C - rectangle "F\\n[Container]" <> { - rectangle "Group 5" <> as groupR3JvdXAgNQ== { - rectangle "==H\\n[Component]" <> as D.F.H + rectangle "D\\n[Software System]" <> { + rectangle "F\\n[Container]" <> { + rectangle "Group 5" <> as groupR3JvdXAgNQ== { + rectangle "==H\\n[Component]" <> as D.F.H + } + + rectangle "==G\\n[Component]" <> as D.F.G } - rectangle "==G\\n[Component]" <> as D.F.G } C --> D.F.G <> : "" @@ -1216,25 +1244,20 @@ public void containerDiagramWithExternalContainers() { } @Test - public void componentDiagramWithExternalComponents() { + public void componentDiagram() { Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); Container container1 = softwareSystem1.addContainer("Container 1"); Component component1 = container1.addComponent("Component 1"); Component component2 = container1.addComponent("Component 2"); - - SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); - Container container2 = softwareSystem2.addContainer("Container 2"); - Component component3 = container2.addComponent("Component 3"); + Container container2 = softwareSystem1.addContainer("Container 2"); component1.uses(component2, "Uses"); - component2.uses(component3, "Uses"); + component2.uses(container2, "Uses"); ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); - componentView.add(component1); - componentView.add(component2); - componentView.add(component3); + componentView.addDefaultElements(); Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); assertEquals(""" @@ -1281,8 +1304,8 @@ public void componentDiagramWithExternalComponents() { HorizontalAlignment: center; Shadowing: 0; } - // Container 2 - .Boundary-Q29udGFpbmVyIDI= { + // Software System 1 + .Boundary-U29mdHdhcmUgU3lzdGVtIDE= { BackgroundColor: #ffffff; LineColor: #444444; LineStyle: 0; @@ -1294,23 +1317,23 @@ public void componentDiagramWithExternalComponents() { } - rectangle "Container 1\\n[Container]" <> { - rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 - } + rectangle "Software System 1\\n[Software System]" <> { + rectangle "Container 1\\n[Container]" <> { + rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 + } - rectangle "Container 2\\n[Container]" <> { - rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 + rectangle "==Container 2\\n[Container]" <> as SoftwareSystem1.Container2 } SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "Uses" - SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem1.Container2 <> : "Uses" @enduml""", diagram.getDefinition()); } @Test - public void componentDiagramWithExternalComponentsAndSoftwareSystemBoundariesIncluded() { + public void componentDiagramWithExternalComponents() { Workspace workspace = new Workspace("Name"); SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); @@ -1321,15 +1344,17 @@ public void componentDiagramWithExternalComponentsAndSoftwareSystemBoundariesInc SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); Container container2 = softwareSystem2.addContainer("Container 2"); Component component3 = container2.addComponent("Component 3"); + Container container4 = softwareSystem2.addContainer("Container 4"); component1.uses(component2, "Uses"); component2.uses(component3, "Uses"); + component3.uses(container4, "Uses"); ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); componentView.add(component1); componentView.add(component2); componentView.add(component3); - componentView.addProperty("structurizr.softwareSystemBoundaries", "true"); + componentView.add(container4); Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); assertEquals(""" @@ -1412,22 +1437,24 @@ public void componentDiagramWithExternalComponentsAndSoftwareSystemBoundariesInc rectangle "Software System 1\\n[Software System]" <> { - rectangle "Container 1\\n[Container]" <> { - rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 - } - + rectangle "Container 1\\n[Container]" <> { + rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 } - rectangle "Software System 2\\n[Software System]" <> { - rectangle "Container 2\\n[Container]" <> { - rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 - } + } + rectangle "Software System 2\\n[Software System]" <> { + rectangle "Container 2\\n[Container]" <> { + rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 } - SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "Uses" + rectangle "==Container 4\\n[Container]" <> as SoftwareSystem2.Container4 + } + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "Uses" + SoftwareSystem2.Container2.Component3 --> SoftwareSystem2.Container4 <> : "Uses" + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "Uses" @enduml""", diagram.getDefinition()); } From 9fd9660dc61c9b9116c6b1a9117d3bad8c0681f6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 26 Sep 2025 14:48:33 +0100 Subject: [PATCH 392/418] Fixes an issue where containers could be rendered outside of their software system boundary on container scoped dynamic views (collaboration style). --- .../export/AbstractDiagramExporter.java | 47 ++++-- .../export/dot/DOTDiagramExporterTests.java | 26 +++- .../C4PlantUMLDiagramExporterTests.java | 11 +- ...ructurizrPlantUMLDiagramExporterTests.java | 137 ++++-------------- 4 files changed, 89 insertions(+), 132 deletions(-) diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java index 58df90d5f..4677e7501 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -308,7 +308,6 @@ public Diagram export(ComponentView view, Integer animationStep) { } List boundaryContainers = getBoundaryContainers(view); - List containers = getContainers(view); Set boundarySoftwareSystems = boundaryContainers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); for (SoftwareSystem softwareSystem : boundarySoftwareSystems) { @@ -325,7 +324,7 @@ public Diagram export(ComponentView view, Integer animationStep) { } } - for (Container container : containers) { + for (Container container : getContainers(view)) { if (container.getSoftwareSystem() == softwareSystem) { writeElement(view, container, writer); } @@ -418,18 +417,37 @@ public Diagram export(DynamicView view, String order) { } } else if (element instanceof Container) { // dynamic view with container scope - boolean includeSoftwareSystemBoundaries = "true".equals(view.getProperties().getOrDefault("structurizr.softwareSystemBoundaries", "false")); + List customElements = getCustomElements(view); + for (CustomElement customElement : customElements) { + writeElement(view, customElement, writer); + } + if (!customElements.isEmpty()) { + writer.writeLine(); + } + + List people = getPeople(view); + for (Person person : people) { + writeElement(view, person, writer); + } + if (!people.isEmpty()) { + writer.writeLine(); + } - List containers = getBoundaryContainers(view); - Set softwareSystems = containers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + List softwareSystems = getSoftwareSystems(view); for (SoftwareSystem softwareSystem : softwareSystems) { + writeElement(view, softwareSystem, writer); + } + if (!softwareSystems.isEmpty()) { + writer.writeLine(); + } - if (includeSoftwareSystemBoundaries) { - startSoftwareSystemBoundary(view, softwareSystem, writer); - writer.indent(); - } + List boundaryContainers = getBoundaryContainers(view); + Set boundarySoftwareSystems = boundaryContainers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + for (SoftwareSystem softwareSystem : boundarySoftwareSystems) { + + startSoftwareSystemBoundary(view, softwareSystem, writer); - for (Container container : containers) { + for (Container container : boundaryContainers) { if (container.getSoftwareSystem() == softwareSystem) { startContainerBoundary(view, container, writer); @@ -440,10 +458,13 @@ public Diagram export(DynamicView view, String order) { } } - if (includeSoftwareSystemBoundaries) { - endSoftwareSystemBoundary(view, writer); - writer.outdent(); + for (Container container : getContainers(view)) { + if (container.getSoftwareSystem() == softwareSystem) { + writeElement(view, container, writer); + } } + + endSoftwareSystemBoundary(view, writer); } for (ElementView elementView : view.getElements()) { diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java index 35cebec48..81da0720a 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -203,16 +203,28 @@ public void test_BigBankPlcExample() throws Exception { edge [fontname="Arial"] label=<
Dynamic View: Internet Banking System - API Application
Summarises how the sign in feature works in the single-page application.> - subgraph cluster_11 { + subgraph cluster_7 { margin=25 - label=<
API Application

[Container: Java and Spring MVC]> + label=<
Internet Banking System

[Software System]> labelloc=b - color="#444444" - fontcolor="#444444" - fillcolor="#444444" + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + subgraph cluster_11 { + margin=25 + label=<
API Application

[Container: Java and Spring MVC]> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" - 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] - 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 12 [id=12,shape=rect, label=<Sign In Controller
[Component: Spring MVC Rest Controller]

Allows users to sign in to the
Internet Banking System.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + 15 [id=15,shape=rect, label=<Security Component
[Component: Spring Bean]

Provides functionality related
to signing in, changing
passwords, etc.
>, style=filled, color="#5d82a8", fillcolor="#85bbf0", fontcolor="#000000"] + } + + 18 [id=18,shape=cylinder, label=<Database
[Container: Oracle Database Schema]

Stores user registration
information, hashed
authentication credentials,
access logs, etc.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] + 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] } 8 [id=8,shape=rect, label=<Single-Page
Application

[Container: JavaScript and Angular]

Provides all of the Internet
banking functionality to
customers via their web
browser.
>, style=filled, color="#2e6295", fillcolor="#438dd5", fontcolor="#ffffff"] diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index dcca9468d..f35eb809e 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -270,9 +270,14 @@ public void test_BigBankPlcExample() throws Exception { AddBoundaryTag("Container", $bgColor="#ffffff", $borderColor="#2e6295", $fontColor="#2e6295", $shadowing="", $borderStyle="solid") - Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { - Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") - Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") + System_Boundary("InternetBankingSystem_boundary", "Internet Banking System", $tags="Software System") { + Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Application", $tags="Container") { + Component(InternetBankingSystem.APIApplication.SignInController, "Sign In Controller", $techn="Spring MVC Rest Controller", $descr="Allows users to sign in to the Internet Banking System.", $tags="Component", $link="") + Component(InternetBankingSystem.APIApplication.SecurityComponent, "Security Component", $techn="Spring Bean", $descr="Provides functionality related to signing in, changing passwords, etc.", $tags="Component", $link="") + } + + ContainerDb(InternetBankingSystem.Database, "Database", $techn="Oracle Database Schema", $descr="Stores user registration information, hashed authentication credentials, access logs, etc.", $tags="Container,Database", $link="") + Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") } Container(InternetBankingSystem.SinglePageApplication, "Single-Page Application", $techn="JavaScript and Angular", $descr="Provides all of the Internet banking functionality to customers via their web browser.", $tags="Container,Web Browser", $link="") diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 0d55ea296..880320ba9 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -1555,107 +1555,16 @@ public void dynamicView_ExternalComponents() { SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); Container container2 = softwareSystem2.addContainer("Container 2"); Component component3 = container2.addComponent("Component 3"); + Container container4 = softwareSystem2.addContainer("Container 4"); component1.uses(component2, "Uses"); component2.uses(component3, "Uses"); + component3.uses(container4, "Uses"); DynamicView dynamicView = workspace.getViews().createDynamicView(container1, "Dynamic", ""); dynamicView.add(component1, component2); dynamicView.add(component2, component3); - - Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); - assertEquals(""" - @startuml - title Dynamic View: Software System 1 - Container 1 - - set separator none - top to bottom direction - hide stereotype - - - - rectangle "Container 1\\n[Container]" <> { - rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 - } - - rectangle "Container 2\\n[Container]" <> { - rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 - } - - SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1: Uses" - SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2: Uses" - - @enduml""", diagram.getDefinition()); - } - - @Test - public void dynamicView_ExternalComponentsAndSoftwareSystemBoundariesIncluded() { - Workspace workspace = new Workspace("Name"); - - SoftwareSystem softwareSystem1 = workspace.getModel().addSoftwareSystem("Software System 1"); - Container container1 = softwareSystem1.addContainer("Container 1"); - Component component1 = container1.addComponent("Component 1"); - Component component2 = container1.addComponent("Component 2"); - - SoftwareSystem softwareSystem2 = workspace.getModel().addSoftwareSystem("Software System 2"); - Container container2 = softwareSystem2.addContainer("Container 2"); - Component component3 = container2.addComponent("Component 3"); - - component1.uses(component2, "Uses"); - component2.uses(component3, "Uses"); - - DynamicView dynamicView = workspace.getViews().createDynamicView(container1, "Dynamic", ""); - dynamicView.add(component1, component2); - dynamicView.add(component2, component3); - dynamicView.addProperty("structurizr.softwareSystemBoundaries", "true"); + dynamicView.add(component3, container4); Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); assertEquals(""" @@ -1738,22 +1647,26 @@ public void dynamicView_ExternalComponentsAndSoftwareSystemBoundariesIncluded() rectangle "Software System 1\\n[Software System]" <> { - rectangle "Container 1\\n[Container]" <> { - rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 - rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 - } - + rectangle "Container 1\\n[Container]" <> { + rectangle "==Component 1\\n[Component]" <> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n[Component]" <> as SoftwareSystem1.Container1.Component2 } - rectangle "Software System 2\\n[Software System]" <> { - rectangle "Container 2\\n[Container]" <> { - rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 - } + } + rectangle "Software System 2\\n[Software System]" <> { + rectangle "Container 2\\n[Container]" <> { + rectangle "==Component 3\\n[Component]" <> as SoftwareSystem2.Container2.Component3 } + rectangle "==Container 4\\n[Container]" <> as SoftwareSystem2.Container4 + } + + rectangle "==Container 4\\n[Container]" <> as SoftwareSystem2.Container4 + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <> : "1: Uses" SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <> : "2: Uses" + SoftwareSystem2.Container2.Component3 --> SoftwareSystem2.Container4 <> : "3: Uses" @enduml""", diagram.getDefinition()); } @@ -2261,16 +2174,22 @@ public void dynamicView_ContainerScopedWithGroups() { } - rectangle "A\\n[Container]" <> { - rectangle "Group 1" <> as groupR3JvdXAgMQ== { - rectangle "==A\\n[Component]" <> as A.A.A + rectangle "A\\n[Software System]" <> { + rectangle "A\\n[Container]" <> { + rectangle "Group 1" <> as groupR3JvdXAgMQ== { + rectangle "==A\\n[Component]" <> as A.A.A + } + } } - rectangle "B\\n[Container]" <> { - rectangle "Group 2" <> as groupR3JvdXAgMg== { - rectangle "==B\\n[Component]" <> as B.B.B + rectangle "B\\n[Software System]" <> { + rectangle "B\\n[Container]" <> { + rectangle "Group 2" <> as groupR3JvdXAgMg== { + rectangle "==B\\n[Component]" <> as B.B.B + } + } } From 6e9f8e727d99f8c1a83fa178f166d6b978eb077e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 26 Sep 2025 15:47:31 +0100 Subject: [PATCH 393/418] Adds back the original Big Bank example tests (and some special handling for sequence diagrams). --- .../export/AbstractDiagramExporter.java | 2 +- .../plantuml/StructurizrPlantUMLExporter.java | 97 +- ...ructurizrPlantUMLDiagramExporterTests.java | 1091 ++++++++++++++++- 3 files changed, 1181 insertions(+), 9 deletions(-) diff --git a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java index 4677e7501..f982fd5f1 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -22,7 +22,7 @@ public AbstractDiagramExporter() { } public AbstractDiagramExporter(ColorScheme colorScheme) { - this.colorScheme = colorScheme; + this.colorScheme = colorScheme != null ? colorScheme : ColorScheme.Light; } /** diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index c71534276..531ce9a6a 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -267,7 +267,7 @@ protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode d format( "rectangle \"%s\\n%s%s\" <<%s>> as %s%s {", deploymentNode.getName() + (!"1".equals(deploymentNode.getInstances()) ? " (x" + deploymentNode.getInstances() + ")" : ""), - calculateMetadataFontSize(findBoundaryStyle(view, deploymentNode).getFontSize()), + calculateMetadataFontSize(findElementStyle(view, deploymentNode).getFontSize()), typeOf(view, deploymentNode, true), icon, classSelectorFor(elementStyle), @@ -324,6 +324,15 @@ public Diagram export(DynamicView view) { @Override protected void writeElement(ModelView view, Element element, IndentingWriter writer) { ElementStyle elementStyle = findElementStyle(view, element); + String sequenceDiagramShape = plantumlSequenceType(view, element); + + if (renderAsSequenceDiagram(view)) { + // actor and database require special treatment because the label sits outside the shape + if ("actor".equals(sequenceDiagramShape) || "database".equals(sequenceDiagramShape)) { + elementStyle.color(elementStyle.getStroke()); + } + } + PlantUMLElementStyle plantUMLElementStyle = new PlantUMLElementStyle( elementStyle.getTag(), elementStyle.getShape(), @@ -342,14 +351,14 @@ protected void writeElement(ModelView view, Element element, IndentingWriter wri int metadataFontSize = calculateMetadataFontSize(elementStyle.getFontSize()); if (renderAsSequenceDiagram(view)) { - writer.writeLine(String.format("%s \"%s\\n%s\" as %s <<%s>> %s", - plantumlSequenceType(view, element), + writer.writeLine(String.format("%s \"%s\\n%s\" as %s <<%s>>", + sequenceDiagramShape, element.getName(), metadataFontSize, typeOf(view, element, true), idOf(element), - plantUMLElementStyle.getClassSelector(), - elementStyle.getBackground())); + plantUMLElementStyle.getClassSelector() + )); } else { String shape = plantUMLShapeOf(view, element); String name = element.getName(); @@ -556,7 +565,83 @@ private String classSelectorForBoundary(Element element) { } private ElementStyle findBoundaryStyle(ModelView view, Element element) { - return findElementStyle(view, element); + String background = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_BACKGROUND_DARK : Styles.DEFAULT_BACKGROUND_LIGHT; + String stroke = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_COLOR_DARK : Styles.DEFAULT_COLOR_LIGHT; + int strokeWidth = DEFAULT_STROKE_WIDTH; + Border border = Border.Dotted; + String color = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_COLOR_DARK : Styles.DEFAULT_COLOR_LIGHT; + String icon = ""; + int fontSize = DEFAULT_FONT_SIZE; + + String type = element instanceof SoftwareSystem ? "SoftwareSystem" : "Container"; + + ElementStyle style = new ElementStyle(""); + ElementStyle elementStyleForBoundary = findElementStyle(view, "Boundary:" + type); + ElementStyle elementStyleForAllBoundaries = findElementStyle(view, "Boundary"); + ElementStyle elementStyleForElement = findElementStyle(view, element); + + if (elementStyleForBoundary != null && !StringUtils.isNullOrEmpty(elementStyleForBoundary.getBackground())) { + background = elementStyleForBoundary.getBackground(); + } else if (elementStyleForAllBoundaries != null && !StringUtils.isNullOrEmpty(elementStyleForAllBoundaries.getBackground())) { + background = elementStyleForAllBoundaries.getBackground(); + } + style.setBackground(background); + + if (elementStyleForBoundary != null && !StringUtils.isNullOrEmpty(elementStyleForBoundary.getStroke())) { + stroke = elementStyleForBoundary.getStroke(); + } else if (elementStyleForAllBoundaries != null && !StringUtils.isNullOrEmpty(elementStyleForAllBoundaries.getStroke())) { + stroke = elementStyleForAllBoundaries.getStroke(); + } else if (!StringUtils.isNullOrEmpty(elementStyleForElement.getStroke())) { + stroke = elementStyleForElement.getStroke(); + } + style.setStroke(stroke); + + if (elementStyleForBoundary != null && elementStyleForBoundary.getStrokeWidth() != null) { + strokeWidth = elementStyleForBoundary.getStrokeWidth(); + } else if (elementStyleForAllBoundaries != null && elementStyleForAllBoundaries.getStrokeWidth() != null) { + strokeWidth = elementStyleForAllBoundaries.getStrokeWidth(); + } else if (elementStyleForElement.getStrokeWidth() != null) { + strokeWidth = elementStyleForElement.getStrokeWidth(); + } + style.setStrokeWidth(strokeWidth); + + if (elementStyleForBoundary != null && !StringUtils.isNullOrEmpty(elementStyleForBoundary.getColor())) { + color = elementStyleForBoundary.getColor(); + } else if (elementStyleForAllBoundaries != null && !StringUtils.isNullOrEmpty(elementStyleForAllBoundaries.getColor())) { + color = elementStyleForAllBoundaries.getColor(); + } else if (!StringUtils.isNullOrEmpty(elementStyleForElement.getColor())) { + color = elementStyleForElement.getColor(); + } + style.setColor(color); + + if (elementStyleForBoundary != null && elementStyleForBoundary.getBorder() != null) { + border = elementStyleForBoundary.getBorder(); + } else if (elementStyleForAllBoundaries != null && elementStyleForAllBoundaries.getBorder() != null) { + border = elementStyleForAllBoundaries.getBorder(); + } else if (elementStyleForElement.getBorder() != null) { + border = elementStyleForElement.getBorder(); + } + style.setBorder(border); + + if (elementStyleForBoundary != null && isSupportedIcon(elementStyleForBoundary.getIcon())) { + icon = elementStyleForBoundary.getIcon(); + } else if (elementStyleForAllBoundaries != null && isSupportedIcon(elementStyleForAllBoundaries.getIcon())) { + icon = elementStyleForAllBoundaries.getIcon(); + } else if (isSupportedIcon(elementStyleForElement.getIcon())) { + icon = elementStyleForElement.getIcon(); + } + style.setIcon(icon); + + if (elementStyleForBoundary != null && elementStyleForBoundary.getFontSize() != null) { + fontSize = elementStyleForBoundary.getFontSize(); + } else if (elementStyleForAllBoundaries != null && elementStyleForAllBoundaries.getFontSize() != null) { + fontSize = elementStyleForAllBoundaries.getFontSize(); + } else if (elementStyleForElement.getFontSize() != null) { + fontSize = elementStyleForElement.getFontSize(); + } + style.setFontSize(fontSize); + + return style; } private String classSelectorForGroup(String group) { diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 880320ba9..62a37a4ed 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -18,6 +18,959 @@ public class StructurizrPlantUMLDiagramExporterTests extends AbstractExporterTests { + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/big-bank-plc.json")); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Boundary:SoftwareSystem").color("#0b4884"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Boundary:Container").color("#438dd5"); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Collection diagrams = exporter.export(workspace); + assertEquals(7, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); + assertEquals(""" + @startuml + title System Landscape View + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "Big Bank plc" <> as groupQmlnIEJhbmsgcGxj { + person "==Customer Service Staff\\n[Person]\\n\\nCustomer service staff within the bank." <> as CustomerServiceStaff + person "==Back Office Staff\\n[Person]\\n\\nAdministration and support staff within the bank." <> as BackOfficeStaff + rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem + rectangle "==E-mail System\\n[Software System]\\n\\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem + rectangle "==ATM\\n[Software System]\\n\\nAllows customers to withdraw cash." <> as ATM + rectangle "==Internet Banking System\\n[Software System]\\n\\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem + } + + person "==Personal Banking Customer\\n[Person]\\n\\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer + + PersonalBankingCustomer --> InternetBankingSystem <> : "Views account balances, and makes payments using" + InternetBankingSystem --> MainframeBankingSystem <> : "Gets account information from, and makes payments using" + InternetBankingSystem --> EmailSystem <> : "Sends e-mail using" + EmailSystem --> PersonalBankingCustomer <> : "Sends e-mails to" + PersonalBankingCustomer --> CustomerServiceStaff <> : "Asks questions to\\n[Telephone]" + CustomerServiceStaff --> MainframeBankingSystem <> : "Uses" + PersonalBankingCustomer --> ATM <> : "Withdraws cash using" + ATM --> MainframeBankingSystem <> : "Uses" + BackOfficeStaff --> MainframeBankingSystem <> : "Uses" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); + assertEquals(""" + @startuml + title System Context View: Internet Banking System\\nThe system context diagram for the Internet Banking System. + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "Big Bank plc" <> as groupQmlnIEJhbmsgcGxj { + rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem + rectangle "==E-mail System\\n[Software System]\\n\\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem + rectangle "==Internet Banking System\\n[Software System]\\n\\nAllows customers to view information about their bank accounts, and make payments." <> as InternetBankingSystem + } + + person "==Personal Banking Customer\\n[Person]\\n\\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer + + PersonalBankingCustomer --> InternetBankingSystem <> : "Views account balances, and makes payments using" + InternetBankingSystem --> MainframeBankingSystem <> : "Gets account information from, and makes payments using" + InternetBankingSystem --> EmailSystem <> : "Sends e-mail using" + EmailSystem --> PersonalBankingCustomer <> : "Sends e-mails to" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); + assertEquals(""" + @startuml + title Container View: Internet Banking System\\nThe container diagram for the Internet Banking System. + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + person "==Personal Banking Customer\\n[Person]\\n\\nA customer of the bank, with personal bank accounts." <> as PersonalBankingCustomer + rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem + rectangle "==E-mail System\\n[Software System]\\n\\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem + + rectangle "Internet Banking System\\n[Software System]" <> { + rectangle "==Web Application\\n[Container: Java and Spring MVC]\\n\\nDelivers the static content and the Internet banking single page application." <> as InternetBankingSystem.WebApplication + rectangle "==API Application\\n[Container: Java and Spring MVC]\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <> as InternetBankingSystem.APIApplication + database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database + rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication + rectangle "==Mobile App\\n[Container: Xamarin]\\n\\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp + } + + EmailSystem --> PersonalBankingCustomer <> : "Sends e-mails to" + PersonalBankingCustomer --> InternetBankingSystem.WebApplication <> : "Visits bigbank.com/ib using\\n[HTTPS]" + PersonalBankingCustomer --> InternetBankingSystem.SinglePageApplication <> : "Views account balances, and makes payments using" + PersonalBankingCustomer --> InternetBankingSystem.MobileApp <> : "Views account balances, and makes payments using" + InternetBankingSystem.WebApplication --> InternetBankingSystem.SinglePageApplication <> : "Delivers to the customer's web browser" + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.APIApplication --> InternetBankingSystem.Database <> : "Reads from and writes to\\n[SQL/TCP]" + InternetBankingSystem.APIApplication --> MainframeBankingSystem <> : "Makes API calls to\\n[XML/HTTPS]" + InternetBankingSystem.APIApplication --> EmailSystem <> : "Sends e-mail using" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); + assertEquals(""" + @startuml + title Component View: Internet Banking System - API Application\\nThe component diagram for the API Application. + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as MainframeBankingSystem + rectangle "==E-mail System\\n[Software System]\\n\\nThe internal Microsoft Exchange e-mail system." <> as EmailSystem + + rectangle "Internet Banking System\\n[Software System]" <> { + rectangle "API Application\\n[Container: Java and Spring MVC]" <> { + rectangle "==Sign In Controller\\n[Component: Spring MVC Rest Controller]\\n\\nAllows users to sign in to the Internet Banking System." <> as InternetBankingSystem.APIApplication.SignInController + rectangle "==Accounts Summary Controller\\n[Component: Spring MVC Rest Controller]\\n\\nProvides customers with a summary of their bank accounts." <> as InternetBankingSystem.APIApplication.AccountsSummaryController + rectangle "==Reset Password Controller\\n[Component: Spring MVC Rest Controller]\\n\\nAllows users to reset their passwords with a single use URL." <> as InternetBankingSystem.APIApplication.ResetPasswordController + rectangle "==Security Component\\n[Component: Spring Bean]\\n\\nProvides functionality related to signing in, changing passwords, etc." <> as InternetBankingSystem.APIApplication.SecurityComponent + rectangle "==Mainframe Banking System Facade\\n[Component: Spring Bean]\\n\\nA facade onto the mainframe banking system." <> as InternetBankingSystem.APIApplication.MainframeBankingSystemFacade + rectangle "==E-mail Component\\n[Component: Spring Bean]\\n\\nSends e-mails to users." <> as InternetBankingSystem.APIApplication.EmailComponent + } + + database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database + rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication + rectangle "==Mobile App\\n[Container: Xamarin]\\n\\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as InternetBankingSystem.MobileApp + } + + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.SignInController <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.AccountsSummaryController <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.ResetPasswordController <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication.SignInController <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication.AccountsSummaryController <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication.ResetPasswordController <> : "Makes API calls to\\n[JSON/HTTPS]" + InternetBankingSystem.APIApplication.SignInController --> InternetBankingSystem.APIApplication.SecurityComponent <> : "Uses" + InternetBankingSystem.APIApplication.AccountsSummaryController --> InternetBankingSystem.APIApplication.MainframeBankingSystemFacade <> : "Uses" + InternetBankingSystem.APIApplication.ResetPasswordController --> InternetBankingSystem.APIApplication.SecurityComponent <> : "Uses" + InternetBankingSystem.APIApplication.ResetPasswordController --> InternetBankingSystem.APIApplication.EmailComponent <> : "Uses" + InternetBankingSystem.APIApplication.SecurityComponent --> InternetBankingSystem.Database <> : "Reads from and writes to\\n[SQL/TCP]" + InternetBankingSystem.APIApplication.MainframeBankingSystemFacade --> MainframeBankingSystem <> : "Makes API calls to\\n[XML/HTTPS]" + InternetBankingSystem.APIApplication.EmailComponent --> EmailSystem <> : "Sends e-mail using" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); + assertEquals(""" + @startuml + title Dynamic View: Internet Banking System - API Application\\nSummarises how the sign in feature works in the single-page application. + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "Internet Banking System\\n[Software System]" <> { + rectangle "API Application\\n[Container: Java and Spring MVC]" <> { + rectangle "==Sign In Controller\\n[Component: Spring MVC Rest Controller]\\n\\nAllows users to sign in to the Internet Banking System." <> as InternetBankingSystem.APIApplication.SignInController + rectangle "==Security Component\\n[Component: Spring Bean]\\n\\nProvides functionality related to signing in, changing passwords, etc." <> as InternetBankingSystem.APIApplication.SecurityComponent + } + + database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database + rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication + } + + rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as InternetBankingSystem.SinglePageApplication + database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as InternetBankingSystem.Database + + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.SignInController <> : "1: Submits credentials to\\n[JSON/HTTPS]" + InternetBankingSystem.APIApplication.SignInController --> InternetBankingSystem.APIApplication.SecurityComponent <> : "2: Validates credentials using" + InternetBankingSystem.APIApplication.SecurityComponent --> InternetBankingSystem.Database <> : "3: select * from users where username = ?\\n[SQL/TCP]" + InternetBankingSystem.APIApplication.SecurityComponent <-- InternetBankingSystem.Database <> : "4: Returns user data to\\n[SQL/TCP]" + InternetBankingSystem.APIApplication.SignInController <-- InternetBankingSystem.APIApplication.SecurityComponent <> : "5: Returns true if the hashed password matches" + InternetBankingSystem.SinglePageApplication <-- InternetBankingSystem.APIApplication.SignInController <> : "6: Sends back an authentication token to\\n[JSON/HTTPS]" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); + assertEquals(""" + @startuml + title Deployment View: Internet Banking System - Development\\nAn example development deployment scenario for the Internet Banking System. + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "Developer Laptop\\n[Deployment Node: Microsoft Windows 10 or Apple macOS]" <> as Development.DeveloperLaptop { + rectangle "Web Browser\\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Development.DeveloperLaptop.WebBrowser { + rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 + } + + rectangle "Docker Container - Web Server\\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerWebServer { + rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat { + rectangle "==Web Application\\n[Container: Java and Spring MVC]\\n\\nDelivers the static content and the Internet banking single page application." <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 + rectangle "==API Application\\n[Container: Java and Spring MVC]\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 + } + + } + + rectangle "Docker Container - Database Server\\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer { + rectangle "Database Server\\n[Deployment Node: Oracle 12c]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer { + database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 + } + + } + + } + + rectangle "Big Bank plc\\n[Deployment Node: Big Bank plc data center]" <> as Development.BigBankplc { + rectangle "bigbank-dev001\\n[Deployment Node]" <> as Development.BigBankplc.bigbankdev001 { + rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 + } + + } + + Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 --> Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 <> : "Delivers to the customer's web browser" + Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 --> Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 <> : "Makes API calls to\\n[JSON/HTTPS]" + Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 --> Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 <> : "Reads from and writes to\\n[SQL/TCP]" + Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 --> Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 <> : "Makes API calls to\\n[XML/HTTPS]" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); + assertEquals(""" + @startuml + title Deployment View: Internet Banking System - Live\\nAn example live deployment scenario for the Internet Banking System. + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + + + rectangle "Customer's mobile device\\n[Deployment Node: Apple iOS or Android]" <> as Live.Customersmobiledevice { + rectangle "==Mobile App\\n[Container: Xamarin]\\n\\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as Live.Customersmobiledevice.MobileApp_1 + } + + rectangle "Customer's computer\\n[Deployment Node: Microsoft Windows or Apple macOS]" <> as Live.Customerscomputer { + rectangle "Web Browser\\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Live.Customerscomputer.WebBrowser { + rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as Live.Customerscomputer.WebBrowser.SinglePageApplication_1 + } + + } + + rectangle "Big Bank plc\\n[Deployment Node: Big Bank plc data center]" <> as Live.BigBankplc { + rectangle "bigbank-web*** (x4)\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankweb { + rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankweb.ApacheTomcat { + rectangle "==Web Application\\n[Container: Java and Spring MVC]\\n\\nDelivers the static content and the Internet banking single page application." <> as Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 + } + + } + + rectangle "bigbank-api*** (x8)\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankapi { + rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankapi.ApacheTomcat { + rectangle "==API Application\\n[Container: Java and Spring MVC]\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <> as Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 + } + + } + + rectangle "bigbank-db01\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb01 { + rectangle "Oracle - Primary\\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb01.OraclePrimary { + database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 + } + + } + + rectangle "bigbank-db02\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb02 { + rectangle "Oracle - Secondary\\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb02.OracleSecondary { + database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 + } + + } + + rectangle "bigbank-prod001\\n[Deployment Node]" <> as Live.BigBankplc.bigbankprod001 { + rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 + } + + } + + Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 --> Live.Customerscomputer.WebBrowser.SinglePageApplication_1 <> : "Delivers to the customer's web browser" + Live.Customersmobiledevice.MobileApp_1 --> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 <> : "Makes API calls to\\n[JSON/HTTPS]" + Live.Customerscomputer.WebBrowser.SinglePageApplication_1 --> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 <> : "Makes API calls to\\n[JSON/HTTPS]" + Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 --> Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 <> : "Reads from and writes to\\n[SQL/TCP]" + Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 --> Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 <> : "Reads from and writes to\\n[SQL/TCP]" + Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 --> Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 <> : "Makes API calls to\\n[XML/HTTPS]" + Live.BigBankplc.bigbankdb01.OraclePrimary --> Live.BigBankplc.bigbankdb02.OracleSecondary <> : "Replicates data to" + + @enduml""", diagram.getDefinition()); + + // and the sequence diagram version + workspace.getViews().getConfiguration().addProperty(exporter.PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "true"); + diagrams = exporter.export(workspace); + diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); + assertEquals(""" + @startuml + title Dynamic View: Internet Banking System - API Application\\nSummarises how the sign in feature works in the single-page application. + + set separator none + hide stereotype + + + + participant "Single-Page Application\\n[Container: JavaScript and Angular]" as InternetBankingSystem.SinglePageApplication <> + participant "Sign In Controller\\n[Component: Spring MVC Rest Controller]" as InternetBankingSystem.APIApplication.SignInController <> + participant "Security Component\\n[Component: Spring Bean]" as InternetBankingSystem.APIApplication.SecurityComponent <> + database "Database\\n[Container: Oracle Database Schema]" as InternetBankingSystem.Database <> + + InternetBankingSystem.SinglePageApplication -> InternetBankingSystem.APIApplication.SignInController <> : 1: Submits credentials to\\n[JSON/HTTPS] + InternetBankingSystem.APIApplication.SignInController -> InternetBankingSystem.APIApplication.SecurityComponent <> : 2: Validates credentials using + InternetBankingSystem.APIApplication.SecurityComponent -> InternetBankingSystem.Database <> : 3: select * from users where username = ?\\n[SQL/TCP] + InternetBankingSystem.APIApplication.SecurityComponent <-- InternetBankingSystem.Database <> : 4: Returns user data to\\n[SQL/TCP] + InternetBankingSystem.APIApplication.SignInController <-- InternetBankingSystem.APIApplication.SecurityComponent <> : 5: Returns true if the hashed password matches + InternetBankingSystem.SinglePageApplication <-- InternetBankingSystem.APIApplication.SignInController <> : 6: Sends back an authentication token to\\n[JSON/HTTPS] + + @enduml""", diagram.getDefinition()); + } + @Test public void systemLandscapeView_NoStyling_Light() { Workspace workspace = new Workspace("Name", "Description"); @@ -704,8 +1657,8 @@ public void dynamicView_SequenceStyle_NoStyling_Light() { } - participant "A\\n[Software System]" as A <> #ffffff - participant "B\\n[Software System]" as B <> #ffffff + participant "A\\n[Software System]" as A <> + participant "B\\n[Software System]" as B <> A -> B <> : 1: Uses\\n[JSON/HTTPS] @@ -758,6 +1711,140 @@ public void dynamicView_SequenceStyle_NoStyling_Light() { @enduml""", diagram.getLegend().getDefinition()); } + @Test + public void dynamicView_SequenceStyle_Styling_Light() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + a.addTags("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + b.addTags("B"); + + a.uses(b, "Uses", "JSON/HTTPS"); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("A").shape(Shape.Person).color("#ff0000"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("B").shape(Shape.Cylinder).color("#00ff00"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + view.add(a, b); + view.addProperty(StructurizrPlantUMLExporter.PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "true"); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title Dynamic View\\nDescription + + set separator none + hide stereotype + + + + actor "A\\n[Software System]" as A <> + database "B\\n[Software System]" as B <> + + A -> B <> : 1: Uses\\n[JSON/HTTPS] + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + + + person "==A" <> + + database "==B" <> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <> : "Relationship" + + @enduml""", diagram.getLegend().getDefinition()); + } + @Test public void groups() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); From 6201c03a3a12888f12cd91e7495a94e28d670b53 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 30 Sep 2025 08:39:07 +0100 Subject: [PATCH 394/418] Tidies up element width/height handling, which needs to differ across diagram exporters. --- .../autolayout/graphviz/DOTExporter.java | 17 +- .../autolayout/graphviz/SVGReader.java | 2 +- .../autolayout/graphviz/StyleUtils.java | 38 ++ .../GraphvizAutomaticLayoutTests.java | 35 +- .../com/structurizr/view/ThemeUtilsTests.java | 4 +- .../com/structurizr/view/ElementStyle.java | 3 - .../java/com/structurizr/view/Styles.java | 2 +- .../com/structurizr/view/StylesTests.java | 26 +- .../structurizr/export/dot/DOTExporter.java | 10 + .../plantuml/PlantUMLDeploymentNodeStyle.java | 99 +++ .../plantuml/StructurizrPlantUMLExporter.java | 50 +- ...ructurizrPlantUMLDiagramExporterTests.java | 597 +++++++++--------- 12 files changed, 518 insertions(+), 365 deletions(-) create mode 100644 structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/StyleUtils.java create mode 100644 structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDeploymentNodeStyle.java diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java index 489cf1bb6..783ce7967 100644 --- a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java @@ -6,6 +6,7 @@ import com.structurizr.model.*; import com.structurizr.util.StringUtils; import com.structurizr.view.DeploymentView; +import com.structurizr.view.ElementStyle; import com.structurizr.view.ModelView; import com.structurizr.view.RelationshipView; @@ -114,10 +115,12 @@ protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) @Override protected void writeElement(ModelView view, Element element, IndentingWriter writer) { + ElementStyle elementStyle = StyleUtils.findElementStyle(view, element); + writer.writeLine(String.format(locale, "%s [width=%f,height=%f,fixedsize=true,id=%s,label=\"%s: %s\"]", element.getId(), - getElementWidth(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches - getElementHeight(view, element.getId()) / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches + elementStyle.getWidth() / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches + elementStyle.getHeight() / Constants.STRUCTURIZR_DPI, // convert Structurizr dimensions to inches element.getId(), element.getId(), escape(element.getName()) @@ -217,14 +220,4 @@ private Element findElementInside(DeploymentNode deploymentNode, ModelView view) return null; } - private int getElementWidth(ModelView view, String elementId) { - Element element = view.getModel().getElement(elementId); - return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getWidth(); - } - - private int getElementHeight(ModelView view, String elementId) { - Element element = view.getModel().getElement(elementId); - return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getHeight(); - } - } \ No newline at end of file diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java index 359c2edb5..adc73c6d0 100644 --- a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java @@ -89,7 +89,7 @@ void parseAndApplyLayout(ModelView view) throws Exception { minimumX = Math.min(elementView.getX(), minimumX); minimumY = Math.min(elementView.getY(), minimumY); - ElementStyle style = view.getViewSet().getConfiguration().getStyles().findElementStyle(view.getModel().getElement(elementView.getId())); + ElementStyle style = StyleUtils.findElementStyle(view, view.getModel().getElement(elementView.getId())); maximumX = Math.max(elementView.getX() + style.getWidth(), maximumX); maximumY = Math.max(elementView.getY() + style.getHeight(), maximumY); diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/StyleUtils.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/StyleUtils.java new file mode 100644 index 000000000..ef89674f9 --- /dev/null +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/StyleUtils.java @@ -0,0 +1,38 @@ +package com.structurizr.autolayout.graphviz; + +import com.structurizr.model.Element; +import com.structurizr.view.ElementStyle; +import com.structurizr.view.Shape; +import com.structurizr.view.View; + +class StyleUtils { + + private static final int DEFAULT_WIDTH = 450; + private static final int DEFAULT_HEIGHT = 300; + + private static final int DEFAULT_WIDTH_PERSON = 400; + private static final int DEFAULT_HEIGHT_PERSON = 400; + + static ElementStyle findElementStyle(View view, Element element) { + ElementStyle style = view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + + if (style.getWidth() == null) { + if (style.getShape() == Shape.Person || style.getShape() == Shape.Robot) { + style.setWidth(DEFAULT_WIDTH_PERSON); + } else { + style.setWidth(DEFAULT_WIDTH); + } + } + + if (style.getHeight() == null) { + if (style.getShape() == Shape.Person || style.getShape() == Shape.Robot) { + style.setHeight(DEFAULT_HEIGHT_PERSON); + } else { + style.setHeight(DEFAULT_HEIGHT); + } + } + + return style; + } + +} diff --git a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java index 710553436..c499674a4 100644 --- a/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java @@ -8,7 +8,6 @@ import com.structurizr.view.AutomaticLayout; import com.structurizr.view.Shape; import com.structurizr.view.SystemContextView; -import com.structurizr.view.SystemLandscapeView; import org.junit.jupiter.api.Test; import java.io.File; @@ -23,36 +22,36 @@ public void apply_Workspace() throws Exception { File tempDir = Files.createTempDirectory("graphviz").toFile(); GraphvizAutomaticLayout graphviz = new GraphvizAutomaticLayout(tempDir); - Workspace workspace = new Workspace("Name"); - SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); - SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); - a.uses(b, "Uses"); + Workspace workspace = new Workspace("Name", ""); + Person user = workspace.getModel().addPerson("User"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + user.uses(softwareSystem, "Uses"); - SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", ""); view.addAllElements(); workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.PERSON).shape(Shape.Person); - assertEquals(0, view.getElementView(a).getX()); - assertEquals(0, view.getElementView(a).getY()); - assertEquals(0, view.getElementView(b).getX()); - assertEquals(0, view.getElementView(b).getY()); + assertEquals(0, view.getElementView(user).getX()); + assertEquals(0, view.getElementView(user).getY()); + assertEquals(0, view.getElementView(softwareSystem).getX()); + assertEquals(0, view.getElementView(softwareSystem).getY()); graphviz.apply(workspace); // no change - the view doesn't have automatic layout configured - assertEquals(0, view.getElementView(a).getX()); - assertEquals(0, view.getElementView(a).getY()); - assertEquals(0, view.getElementView(b).getX()); - assertEquals(0, view.getElementView(b).getY()); + assertEquals(0, view.getElementView(user).getX()); + assertEquals(0, view.getElementView(user).getY()); + assertEquals(0, view.getElementView(softwareSystem).getX()); + assertEquals(0, view.getElementView(softwareSystem).getY()); view.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom); graphviz.apply(workspace); - assertEquals(208, view.getElementView(a).getX()); - assertEquals(208, view.getElementView(a).getY()); - assertEquals(208, view.getElementView(b).getX()); - assertEquals(808, view.getElementView(b).getY()); + assertEquals(233, view.getElementView(user).getX()); + assertEquals(208, view.getElementView(user).getY()); + assertEquals(208, view.getElementView(softwareSystem).getX()); + assertEquals(908, view.getElementView(softwareSystem).getY()); } } \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java index 3ebc16983..1ecae3995 100644 --- a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java @@ -94,8 +94,8 @@ void findElementStyle_WithThemes() { workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(new Theme(elementStyles, relationshipStyles)); ElementStyle style = workspace.getViews().getConfiguration().getStyles().findElementStyle(softwareSystem); - assertEquals(Integer.valueOf(450), style.getWidth()); - assertEquals(Integer.valueOf(300), style.getHeight()); + assertNull(style.getWidth()); + assertNull(style.getHeight()); assertEquals("#ff0000", style.getBackground()); // from theme 2 assertEquals("#ffffff", style.getColor()); // from theme 1 assertEquals(Integer.valueOf(24), style.getFontSize()); diff --git a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java index fe4799b60..2787334b1 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java +++ b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java @@ -9,9 +9,6 @@ */ public final class ElementStyle extends AbstractStyle { - public static final int DEFAULT_WIDTH = 450; - public static final int DEFAULT_HEIGHT = 300; - @JsonInclude(value = JsonInclude.Include.NON_NULL) private Integer width; diff --git a/structurizr-core/src/main/java/com/structurizr/view/Styles.java b/structurizr-core/src/main/java/com/structurizr/view/Styles.java index ba0c7e7ee..2d2db79a7 100644 --- a/structurizr-core/src/main/java/com/structurizr/view/Styles.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Styles.java @@ -270,7 +270,7 @@ public ElementStyle findElementStyle(Element element) { * @return an ElementStyle object */ public ElementStyle findElementStyle(Element element, ColorScheme colorScheme) { - ElementStyle style = new ElementStyle(Tags.ELEMENT).shape(Shape.Box).width(ElementStyle.DEFAULT_WIDTH).height(ElementStyle.DEFAULT_HEIGHT).fontSize(24).border(Border.Solid).opacity(100).metadata(true).description(true); + ElementStyle style = new ElementStyle(Tags.ELEMENT).shape(Shape.Box).fontSize(24).border(Border.Solid).opacity(100).metadata(true).description(true); if (element != null) { Set tagsUsedToComposeStyle = new LinkedHashSet<>(); diff --git a/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java index f0c6baab6..befbcebcd 100644 --- a/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java +++ b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java @@ -53,8 +53,8 @@ void test_sortingOfRelationshipStyles() { @Test void findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { ElementStyle style = styles.findElementStyle((Element) null); - assertEquals(Integer.valueOf(450), style.getWidth()); - assertEquals(Integer.valueOf(300), style.getHeight()); + assertNull(style.getWidth()); + assertNull(style.getHeight()); assertEquals("#ffffff", style.getBackground()); assertEquals("#444444", style.getColor()); assertEquals("#444444", style.getStroke()); @@ -72,8 +72,8 @@ void findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { void findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); ElementStyle style = styles.findElementStyle(element); - assertEquals(Integer.valueOf(450), style.getWidth()); - assertEquals(Integer.valueOf(300), style.getHeight()); + assertNull(style.getWidth()); + assertNull(style.getHeight()); assertEquals("#ffffff", style.getBackground()); assertEquals("#444444", style.getColor()); assertEquals("#444444", style.getStroke()); @@ -91,8 +91,8 @@ void findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { void findElementStyleForDarkMode_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); ElementStyle style = styles.findElementStyle(element, ColorScheme.Dark); - assertEquals(Integer.valueOf(450), style.getWidth()); - assertEquals(Integer.valueOf(300), style.getHeight()); + assertNull(style.getWidth()); + assertNull(style.getHeight()); assertEquals("#111111", style.getBackground()); assertEquals("#cccccc", style.getColor()); assertEquals("#cccccc", style.getStroke()); @@ -158,20 +158,6 @@ void findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDe assertEquals("value", style.getProperties().get("name")); } - @Test - void findElementStyle_ReturnsTheDefaultElementSize_WhenTheShapeIsABox() { - SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); - element.addTags("Some Tag"); - - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#ff0000").color("#ffffff"); - styles.addElementStyle("Some Tag").shape(Shape.Box); - - ElementStyle style = styles.findElementStyle(element); - assertEquals(Shape.Box, style.getShape()); - assertEquals(Integer.valueOf(450), style.getWidth()); - assertEquals(Integer.valueOf(300), style.getHeight()); - } - @Test void findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull_ForLightColorScheme() { RelationshipStyle style = styles.findRelationshipStyle((Relationship) null); diff --git a/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java index 26c7aa549..708ea4b46 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java @@ -12,6 +12,8 @@ */ public class DOTExporter extends AbstractDiagramExporter { + private static final int DEFAULT_WIDTH = 450; + private static final int DEFAULT_HEIGHT = 300; private static final String DEFAULT_FONT = "Arial"; private int clusterInternalMargin = 25; @@ -209,6 +211,14 @@ protected void endDeploymentNodeBoundary(ModelView view, IndentingWriter writer) protected void writeElement(ModelView view, Element element, IndentingWriter writer) { ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(element); + if (elementStyle.getWidth() == null) { + elementStyle.setWidth(DEFAULT_WIDTH); + } + + if (elementStyle.getHeight() == null) { + elementStyle.setHeight(DEFAULT_HEIGHT); + } + int nameFontSize = elementStyle.getFontSize() + 10; int metadataFontSize = elementStyle.getFontSize() - 5; int descriptionFontSize = elementStyle.getFontSize(); diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDeploymentNodeStyle.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDeploymentNodeStyle.java new file mode 100644 index 000000000..18915a0f6 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/PlantUMLDeploymentNodeStyle.java @@ -0,0 +1,99 @@ +package com.structurizr.export.plantuml; + +import com.structurizr.export.IndentingWriter; +import com.structurizr.view.Border; + +import java.util.Base64; + +import static java.lang.String.format; + +class PlantUMLDeploymentNodeStyle extends PlantUMLStyle { + + private final String background; + private final String color; + private final String stroke; + private final int strokeWidth; + private final String lineStyle; + private final int fontSize; + private final String icon; + private final boolean shadow; + private Integer width; // only used for the legend + + PlantUMLDeploymentNodeStyle(String name, String background, String color, String stroke, int strokeWidth, Border border, int fontSize, String icon, boolean shadow) { + super(name); + + this.background = background; + this.color = color; + this.stroke = stroke; + this.strokeWidth = strokeWidth; + + switch (border) { + case Dotted: + this.lineStyle = (strokeWidth) + "-" + (strokeWidth); + break; + case Dashed: + this.lineStyle = (strokeWidth * 5) + "-" + (strokeWidth * 5); + break; + default: + this.lineStyle = "0"; + break; + } + + this.fontSize = fontSize; + this.icon = icon; + this.shadow = shadow; + } + + public int getFontSize() { + return fontSize; + } + + public String getIcon() { + return icon; + } + + public void setWidth(int width) { + this.width = width; + } + + @Override + String getClassSelector() { + return "DeploymentNode-" + Base64.getEncoder().encodeToString(name.getBytes()); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PlantUMLDeploymentNodeStyle that = (PlantUMLDeploymentNodeStyle) o; + return getClassSelector().equals(that.getClassSelector()); + } + + @Override + public String toString() { + IndentingWriter writer = new IndentingWriter(); + writer.indent(); + writer.writeLine(format("// %s", name)); + writer.writeLine(format(".%s {", getClassSelector())); + writer.indent(); + + writer.writeLine(String.format("BackgroundColor: %s;", background)); + writer.writeLine(String.format("LineColor: %s;", stroke)); + writer.writeLine(String.format("LineStyle: %s;", lineStyle)); + writer.writeLine(String.format("LineThickness: %s;", strokeWidth)); + writer.writeLine(String.format("FontColor: %s;", color)); + writer.writeLine(String.format("FontSize: %s;", fontSize)); + writer.writeLine("HorizontalAlignment: center;"); + writer.writeLine(String.format("Shadowing: %s;", shadow ? SHADOW_DISTANCE : 0)); + if (width != null) { + writer.writeLine(String.format("MaximumWidth: %s;", width)); + } + + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine(); + + return writer.toString(); + } + +} \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java index 531ce9a6a..30229f0e4 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -15,6 +15,8 @@ public class StructurizrPlantUMLExporter extends AbstractPlantUMLExporter { public static final String PLANTUML_SHADOW = "plantuml.shadow"; + private static final int DEFAULT_WIDTH = 450; + private static final int DEFAULT_HEIGHT = 300; private static final int DEFAULT_STROKE_WIDTH = 2; private static final double METADATA_FONT_SIZE_RATIO = 0.7; private static final int MAX_ICON_SIZE_RATIO = 3; @@ -99,6 +101,7 @@ private void writeStyles(IndentingWriter writer) { sortedStyles.stream().filter(style -> style instanceof PlantUMLRootStyle).forEach(style -> styles.append(style.toString())); sortedStyles.stream().filter(style -> style instanceof PlantUMLElementStyle).forEach(style -> styles.append(style.toString())); sortedStyles.stream().filter(style -> style instanceof PlantUMLRelationshipStyle).forEach(style -> styles.append(style.toString())); + sortedStyles.stream().filter(style -> style instanceof PlantUMLDeploymentNodeStyle).forEach(style -> styles.append(style.toString())); sortedStyles.stream().filter(style -> style instanceof PlantUMLBoundaryStyle).forEach(style -> styles.append(style.toString())); sortedStyles.stream().filter(style -> style instanceof PlantUMLGroupStyle).forEach(style -> styles.append(style.toString())); sortedStyles.stream().filter(style -> style instanceof PlantUMLLegendStyle).forEach(style -> styles.append(style.toString())); @@ -235,10 +238,8 @@ protected void endContainerBoundary(ModelView view, IndentingWriter writer) { protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode deploymentNode, IndentingWriter writer) { ElementStyle elementStyle = findElementStyle(view, deploymentNode); - PlantUMLElementStyle plantUMLElementStyle = new PlantUMLElementStyle( + PlantUMLDeploymentNodeStyle plantUMLDeploymentNodeStyle = new PlantUMLDeploymentNodeStyle( elementStyle.getTag(), - elementStyle.getShape(), - elementStyle.getWidth(), elementStyle.getBackground(), elementStyle.getColor(), elementStyle.getStroke(), @@ -248,7 +249,7 @@ protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode d elementStyle.getIcon(), "true".equalsIgnoreCase(elementStyle.getProperties().getOrDefault(PLANTUML_SHADOW, "false")) ); - plantUMLStyles.add(plantUMLElementStyle); + plantUMLStyles.add(plantUMLDeploymentNodeStyle); String icon = ""; if (isSupportedIcon(elementStyle.getIcon())) { @@ -270,7 +271,7 @@ protected void startDeploymentNodeBoundary(DeploymentView view, DeploymentNode d calculateMetadataFontSize(findElementStyle(view, deploymentNode).getFontSize()), typeOf(view, deploymentNode, true), icon, - classSelectorFor(elementStyle), + plantUMLDeploymentNodeStyle.getClassSelector(), idOf(deploymentNode), url ) @@ -324,6 +325,15 @@ public Diagram export(DynamicView view) { @Override protected void writeElement(ModelView view, Element element, IndentingWriter writer) { ElementStyle elementStyle = findElementStyle(view, element); + + if (elementStyle.getWidth() == null) { + elementStyle.setWidth(DEFAULT_WIDTH); + } + + if (elementStyle.getHeight() == null) { + elementStyle.setHeight(DEFAULT_HEIGHT); + } + String sequenceDiagramShape = plantumlSequenceType(view, element); if (renderAsSequenceDiagram(view)) { @@ -499,6 +509,28 @@ protected Legend createLegend(ModelView view) { writer.writeLine(""); writer.writeLine(); + plantUMLStyles.stream().sorted(Comparator.comparing(PlantUMLStyle::getName)).filter(style -> style instanceof PlantUMLDeploymentNodeStyle).map(style -> (PlantUMLDeploymentNodeStyle)style).forEach(style -> { + style.setWidth(200); + String description = style.getName(); + if (description.startsWith("Element,")) { + description = description.substring("Element,".length()); + } + description = description.replaceAll(",", ", "); + + String icon = ""; + if (isSupportedIcon(style.getIcon())) { + double scale = calculateIconScale(style.getIcon(), style.getFontSize() * MAX_ICON_SIZE_RATIO); + icon = "\\n\\n"; + } + + writer.writeLine(format("rectangle \"==%s%s\" <<%s>>", + description, + icon, + style.getClassSelector()) + ); + writer.writeLine(); + }); + plantUMLStyles.stream().sorted(Comparator.comparing(PlantUMLStyle::getName)).filter(style -> style instanceof PlantUMLElementStyle).map(style -> (PlantUMLElementStyle)style).forEach(style -> { style.setWidth(200); String description = style.getName(); @@ -556,14 +588,6 @@ protected boolean renderAsSequenceDiagram(ModelView view) { return view instanceof DynamicView && "true".equalsIgnoreCase(getViewOrViewSetProperty(view, PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "false")); } - private String classSelectorFor(ElementStyle elementStyle) { - return "Element-" + Base64.getEncoder().encodeToString(elementStyle.getTag().getBytes()); - } - - private String classSelectorForBoundary(Element element) { - return "Boundary-" + Base64.getEncoder().encodeToString(element.getName().getBytes()); - } - private ElementStyle findBoundaryStyle(ModelView view, Element element) { String background = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_BACKGROUND_DARK : Styles.DEFAULT_BACKGROUND_LIGHT; String stroke = colorScheme == ColorScheme.Dark ? Styles.DEFAULT_COLOR_DARK : Styles.DEFAULT_COLOR_LIGHT; diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 62a37a4ed..9ab8c70f4 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -622,18 +622,6 @@ public void test_BigBankPlcExample() throws Exception { BackgroundColor: #ffffff; FontColor: #444444; } - // Element - .Element-RWxlbWVudA== { - BackgroundColor: #ffffff; - LineColor: #444444; - LineStyle: 0; - LineThickness: 2; - FontColor: #444444; - FontSize: 24; - HorizontalAlignment: center; - Shadowing: 0; - MaximumWidth: 450; - } // Element,Container .Element-RWxlbWVudCxDb250YWluZXI= { BackgroundColor: #438dd5; @@ -690,23 +678,34 @@ public void test_BigBankPlcExample() throws Exception { FontColor: #444444; FontSize: 24; } + // Element + .DeploymentNode-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } - rectangle "Developer Laptop\\n[Deployment Node: Microsoft Windows 10 or Apple macOS]" <> as Development.DeveloperLaptop { - rectangle "Web Browser\\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Development.DeveloperLaptop.WebBrowser { + rectangle "Developer Laptop\\n[Deployment Node: Microsoft Windows 10 or Apple macOS]" <> as Development.DeveloperLaptop { + rectangle "Web Browser\\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Development.DeveloperLaptop.WebBrowser { rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 } - rectangle "Docker Container - Web Server\\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerWebServer { - rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat { + rectangle "Docker Container - Web Server\\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerWebServer { + rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat { rectangle "==Web Application\\n[Container: Java and Spring MVC]\\n\\nDelivers the static content and the Internet banking single page application." <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 rectangle "==API Application\\n[Container: Java and Spring MVC]\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 } } - rectangle "Docker Container - Database Server\\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer { - rectangle "Database Server\\n[Deployment Node: Oracle 12c]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer { + rectangle "Docker Container - Database Server\\n[Deployment Node: Docker]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer { + rectangle "Database Server\\n[Deployment Node: Oracle 12c]" <> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer { database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 } @@ -714,8 +713,8 @@ public void test_BigBankPlcExample() throws Exception { } - rectangle "Big Bank plc\\n[Deployment Node: Big Bank plc data center]" <> as Development.BigBankplc { - rectangle "bigbank-dev001\\n[Deployment Node]" <> as Development.BigBankplc.bigbankdev001 { + rectangle "Big Bank plc\\n[Deployment Node: Big Bank plc data center]" <> as Development.BigBankplc { + rectangle "bigbank-dev001\\n[Deployment Node]" <> as Development.BigBankplc.bigbankdev001 { rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 } @@ -744,18 +743,6 @@ public void test_BigBankPlcExample() throws Exception { BackgroundColor: #ffffff; FontColor: #444444; } - // Element - .Element-RWxlbWVudA== { - BackgroundColor: #ffffff; - LineColor: #444444; - LineStyle: 0; - LineThickness: 2; - FontColor: #444444; - FontSize: 24; - HorizontalAlignment: center; - Shadowing: 0; - MaximumWidth: 450; - } // Element,Container .Element-RWxlbWVudCxDb250YWluZXI= { BackgroundColor: #438dd5; @@ -804,18 +791,6 @@ public void test_BigBankPlcExample() throws Exception { Shadowing: 0; MaximumWidth: 450; } - // Element,Failover - .Element-RWxlbWVudCxGYWlsb3Zlcg== { - BackgroundColor: #ffffff; - LineColor: #444444; - LineStyle: 0; - LineThickness: 2; - FontColor: #444444; - FontSize: 24; - HorizontalAlignment: center; - Shadowing: 0; - MaximumWidth: 450; - } // Element,Software System,Existing System .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt { BackgroundColor: #999999; @@ -836,49 +811,71 @@ public void test_BigBankPlcExample() throws Exception { FontColor: #444444; FontSize: 24; } + // Element + .DeploymentNode-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Failover + .DeploymentNode-RWxlbWVudCxGYWlsb3Zlcg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } - rectangle "Customer's mobile device\\n[Deployment Node: Apple iOS or Android]" <> as Live.Customersmobiledevice { + rectangle "Customer's mobile device\\n[Deployment Node: Apple iOS or Android]" <> as Live.Customersmobiledevice { rectangle "==Mobile App\\n[Container: Xamarin]\\n\\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <> as Live.Customersmobiledevice.MobileApp_1 } - rectangle "Customer's computer\\n[Deployment Node: Microsoft Windows or Apple macOS]" <> as Live.Customerscomputer { - rectangle "Web Browser\\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Live.Customerscomputer.WebBrowser { + rectangle "Customer's computer\\n[Deployment Node: Microsoft Windows or Apple macOS]" <> as Live.Customerscomputer { + rectangle "Web Browser\\n[Deployment Node: Chrome, Firefox, Safari, or Edge]" <> as Live.Customerscomputer.WebBrowser { rectangle "==Single-Page Application\\n[Container: JavaScript and Angular]\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <> as Live.Customerscomputer.WebBrowser.SinglePageApplication_1 } } - rectangle "Big Bank plc\\n[Deployment Node: Big Bank plc data center]" <> as Live.BigBankplc { - rectangle "bigbank-web*** (x4)\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankweb { - rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankweb.ApacheTomcat { + rectangle "Big Bank plc\\n[Deployment Node: Big Bank plc data center]" <> as Live.BigBankplc { + rectangle "bigbank-web*** (x4)\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankweb { + rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankweb.ApacheTomcat { rectangle "==Web Application\\n[Container: Java and Spring MVC]\\n\\nDelivers the static content and the Internet banking single page application." <> as Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 } } - rectangle "bigbank-api*** (x8)\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankapi { - rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankapi.ApacheTomcat { + rectangle "bigbank-api*** (x8)\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankapi { + rectangle "Apache Tomcat\\n[Deployment Node: Apache Tomcat 8.x]" <> as Live.BigBankplc.bigbankapi.ApacheTomcat { rectangle "==API Application\\n[Container: Java and Spring MVC]\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <> as Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 } } - rectangle "bigbank-db01\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb01 { - rectangle "Oracle - Primary\\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb01.OraclePrimary { + rectangle "bigbank-db01\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb01 { + rectangle "Oracle - Primary\\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb01.OraclePrimary { database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 } } - rectangle "bigbank-db02\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb02 { - rectangle "Oracle - Secondary\\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb02.OracleSecondary { + rectangle "bigbank-db02\\n[Deployment Node: Ubuntu 16.04 LTS]" <> as Live.BigBankplc.bigbankdb02 { + rectangle "Oracle - Secondary\\n[Deployment Node: Oracle 12c]" <> as Live.BigBankplc.bigbankdb02.OracleSecondary { database "==Database\\n[Container: Oracle Database Schema]\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <> as Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 } } - rectangle "bigbank-prod001\\n[Deployment Node]" <> as Live.BigBankplc.bigbankprod001 { + rectangle "bigbank-prod001\\n[Deployment Node]" <> as Live.BigBankplc.bigbankprod001 { rectangle "==Mainframe Banking System\\n[Software System]\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <> as Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 } @@ -1425,14 +1422,25 @@ public void deploymentView_NoStyling_Light() { Shadowing: 0; MaximumWidth: 450; } + // Element + .DeploymentNode-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } - rectangle "Node 1\\n[Deployment Node]" <> as Default.Node1 { - rectangle "Node 2\\n[Deployment Node]" <> as Default.Node1.Node2 { + rectangle "Node 1\\n[Deployment Node]" <> as Default.Node1 { + rectangle "Node 2\\n[Deployment Node]" <> as Default.Node1.Node2 { rectangle "==A\\n[Software System]" <> as Default.Node1.Node2.A_1 } - rectangle "Node 3\\n[Deployment Node]" <> as Default.Node1.Node3 { + rectangle "Node 3\\n[Deployment Node]" <> as Default.Node1.Node3 { rectangle "==Infrastructure Node\\n[Infrastructure Node]" <> as Default.Node1.Node3.InfrastructureNode } @@ -3333,6 +3341,17 @@ public void deploymentView_WithGroups() { Shadowing: 0; MaximumWidth: 450; } + // Element + .DeploymentNode-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } // Group 1 .Group-R3JvdXAgMQ== { BackgroundColor: #ffffff; @@ -3358,7 +3377,7 @@ public void deploymentView_WithGroups() { rectangle "Group 1" <> as groupR3JvdXAgMQ== { - rectangle "Server 1\\n[Deployment Node]" <> as Default.Server1 { + rectangle "Server 1\\n[Deployment Node]" <> as Default.Server1 { rectangle "Group 2" <> as groupR3JvdXAgMg== { rectangle "==Infrastructure Node 2\\n[Infrastructure Node]" <> as Default.Server1.InfrastructureNode2 rectangle "==Software System\\n[Software System]" <> as Default.Server1.SoftwareSystem_1 @@ -3512,148 +3531,142 @@ public void amazonWebServicesExample_Light() throws Exception { BackgroundColor: #ffffff; FontColor: #444444; } - // Element,Amazon Web Services - Auto Scaling - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { BackgroundColor: #ffffff; - LineColor: #cc2264; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #cc2264; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - Cloud - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { BackgroundColor: #ffffff; - LineColor: #232f3e; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #232f3e; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - EC2 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { BackgroundColor: #ffffff; - LineColor: #d86613; + LineColor: #444444; LineStyle: 0; LineThickness: 2; - FontColor: #d86613; + RoundCorner: 20; + FontColor: #444444; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - Elastic Load Balancing - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { BackgroundColor: #ffffff; - LineColor: #693cc5; + LineColor: #444444; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #444444; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - RDS - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Element,Amazon Web Services - Auto Scaling + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { BackgroundColor: #ffffff; - LineColor: #3b48cc; + LineColor: #cc2264; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #cc2264; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Amazon Web Services - RDS MySQL instance - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { BackgroundColor: #ffffff; - LineColor: #3b48cc; + LineColor: #232f3e; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #232f3e; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Amazon Web Services - Region - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { BackgroundColor: #ffffff; - LineColor: #147eba; + LineColor: #d86613; LineStyle: 0; LineThickness: 2; - FontColor: #147eba; + FontColor: #d86613; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Amazon Web Services - Route 53 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { BackgroundColor: #ffffff; - LineColor: #693cc5; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Application - .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { BackgroundColor: #ffffff; - LineColor: #444444; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - RoundCorner: 20; - FontColor: #444444; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Database - .Element-RWxlbWVudCxEYXRhYmFzZQ== { + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { BackgroundColor: #ffffff; - LineColor: #444444; + LineColor: #147eba; LineStyle: 0; LineThickness: 2; - FontColor: #444444; + FontColor: #147eba; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; - } - // Relationship - .Relationship-UmVsYXRpb25zaGlw { - LineThickness: 2; - LineStyle: 10-10; - LineColor: #444444; - FontColor: #444444; - FontSize: 24; } - rectangle "Amazon Web Services\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices { - rectangle "US-East-1\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1 { - rectangle "Autoscaling group\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { - rectangle "Amazon EC2 - Ubuntu server\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { + rectangle "Amazon Web Services\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices { + rectangle "US-East-1\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1 { + rectangle "Autoscaling group\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2 - Ubuntu server\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { rectangle "==Web Application\\n[Container: Java and Spring Boot]" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 } } - rectangle "Amazon RDS\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { - rectangle "MySQL\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { + rectangle "Amazon RDS\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { database "==Database Schema\\n[Container]" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 } @@ -3682,135 +3695,135 @@ public void amazonWebServicesExample_Light() throws Exception { BackgroundColor: #ffffff; FontColor: #444444; } - // Element,Amazon Web Services - Auto Scaling - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { BackgroundColor: #ffffff; - LineColor: #cc2264; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #cc2264; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Cloud - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { BackgroundColor: #ffffff; - LineColor: #232f3e; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #232f3e; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - EC2 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { BackgroundColor: #ffffff; - LineColor: #d86613; + LineColor: #444444; LineStyle: 0; LineThickness: 2; - FontColor: #d86613; + RoundCorner: 20; + FontColor: #444444; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Elastic Load Balancing - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { BackgroundColor: #ffffff; - LineColor: #693cc5; + LineColor: #444444; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #444444; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - RDS - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Element,Amazon Web Services - Auto Scaling + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { BackgroundColor: #ffffff; - LineColor: #3b48cc; + LineColor: #cc2264; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #cc2264; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - RDS MySQL instance - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { BackgroundColor: #ffffff; - LineColor: #3b48cc; + LineColor: #232f3e; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #232f3e; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Region - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { BackgroundColor: #ffffff; - LineColor: #147eba; + LineColor: #d86613; LineStyle: 0; LineThickness: 2; - FontColor: #147eba; + FontColor: #d86613; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Route 53 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { BackgroundColor: #ffffff; - LineColor: #693cc5; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Application - .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { BackgroundColor: #ffffff; - LineColor: #444444; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - RoundCorner: 20; - FontColor: #444444; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Database - .Element-RWxlbWVudCxEYXRhYmFzZQ== { + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { BackgroundColor: #ffffff; - LineColor: #444444; + LineColor: #147eba; LineStyle: 0; LineThickness: 2; - FontColor: #444444; + FontColor: #147eba; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Relationship - .Relationship-UmVsYXRpb25zaGlw { - LineThickness: 2; - LineStyle: 10-10; - LineColor: #444444; - FontColor: #444444; - FontSize: 24; - } // transparent element for relationships in legend .Element-Transparent { BackgroundColor: transparent; @@ -3819,19 +3832,19 @@ public void amazonWebServicesExample_Light() throws Exception { } - rectangle "==Amazon Web Services - Auto Scaling\\n\\n" <> + rectangle "==Amazon Web Services - Auto Scaling\\n\\n" <> - rectangle "==Amazon Web Services - Cloud\\n\\n" <> + rectangle "==Amazon Web Services - Cloud\\n\\n" <> - rectangle "==Amazon Web Services - EC2\\n\\n" <> + rectangle "==Amazon Web Services - EC2\\n\\n" <> - rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n" <> + rectangle "==Amazon Web Services - RDS\\n\\n" <> - rectangle "==Amazon Web Services - RDS\\n\\n" <> + rectangle "==Amazon Web Services - RDS MySQL instance\\n\\n" <> - rectangle "==Amazon Web Services - RDS MySQL instance\\n\\n" <> + rectangle "==Amazon Web Services - Region\\n\\n" <> - rectangle "==Amazon Web Services - Region\\n\\n" <> + rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n" <> rectangle "==Amazon Web Services - Route 53\\n\\n" <> @@ -3871,148 +3884,142 @@ public void amazonWebServicesExample_Dark() throws Exception { BackgroundColor: #111111; FontColor: #cccccc; } - // Element,Amazon Web Services - Auto Scaling - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { BackgroundColor: #111111; - LineColor: #cc2264; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #cc2264; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - Cloud - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { BackgroundColor: #111111; - LineColor: #232f3e; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #232f3e; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - EC2 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { BackgroundColor: #111111; - LineColor: #d86613; + LineColor: #cccccc; LineStyle: 0; LineThickness: 2; - FontColor: #d86613; + RoundCorner: 20; + FontColor: #cccccc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - Elastic Load Balancing - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { BackgroundColor: #111111; - LineColor: #693cc5; + LineColor: #cccccc; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #cccccc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 450; } - // Element,Amazon Web Services - RDS - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #cccccc; + FontColor: #cccccc; + FontSize: 24; + } + // Element,Amazon Web Services - Auto Scaling + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { BackgroundColor: #111111; - LineColor: #3b48cc; + LineColor: #cc2264; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #cc2264; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Amazon Web Services - RDS MySQL instance - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { BackgroundColor: #111111; - LineColor: #3b48cc; + LineColor: #232f3e; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #232f3e; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Amazon Web Services - Region - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { BackgroundColor: #111111; - LineColor: #147eba; + LineColor: #d86613; LineStyle: 0; LineThickness: 2; - FontColor: #147eba; + FontColor: #d86613; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Amazon Web Services - Route 53 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { BackgroundColor: #111111; - LineColor: #693cc5; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Application - .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { BackgroundColor: #111111; - LineColor: #cccccc; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - RoundCorner: 20; - FontColor: #cccccc; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; } - // Element,Database - .Element-RWxlbWVudCxEYXRhYmFzZQ== { + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { BackgroundColor: #111111; - LineColor: #cccccc; + LineColor: #147eba; LineStyle: 0; LineThickness: 2; - FontColor: #cccccc; + FontColor: #147eba; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; - MaximumWidth: 450; - } - // Relationship - .Relationship-UmVsYXRpb25zaGlw { - LineThickness: 2; - LineStyle: 10-10; - LineColor: #cccccc; - FontColor: #cccccc; - FontSize: 24; } - rectangle "Amazon Web Services\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices { - rectangle "US-East-1\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1 { - rectangle "Autoscaling group\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { - rectangle "Amazon EC2 - Ubuntu server\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { + rectangle "Amazon Web Services\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices { + rectangle "US-East-1\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1 { + rectangle "Autoscaling group\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2 - Ubuntu server\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { rectangle "==Web Application\\n[Container: Java and Spring Boot]" <> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 } } - rectangle "Amazon RDS\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { - rectangle "MySQL\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { + rectangle "Amazon RDS\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\\n[Deployment Node]\\n\\n" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { database "==Database Schema\\n[Container]" <> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 } @@ -4041,135 +4048,135 @@ public void amazonWebServicesExample_Dark() throws Exception { BackgroundColor: #111111; FontColor: #cccccc; } - // Element,Amazon Web Services - Auto Scaling - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { BackgroundColor: #111111; - LineColor: #cc2264; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #cc2264; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Cloud - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { BackgroundColor: #111111; - LineColor: #232f3e; + LineColor: #693cc5; LineStyle: 0; LineThickness: 2; - FontColor: #232f3e; + FontColor: #693cc5; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - EC2 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { BackgroundColor: #111111; - LineColor: #d86613; + LineColor: #cccccc; LineStyle: 0; LineThickness: 2; - FontColor: #d86613; + RoundCorner: 20; + FontColor: #cccccc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Elastic Load Balancing - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { BackgroundColor: #111111; - LineColor: #693cc5; + LineColor: #cccccc; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #cccccc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - RDS - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #cccccc; + FontColor: #cccccc; + FontSize: 24; + } + // Element,Amazon Web Services - Auto Scaling + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n { BackgroundColor: #111111; - LineColor: #3b48cc; + LineColor: #cc2264; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #cc2264; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - RDS MySQL instance - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { BackgroundColor: #111111; - LineColor: #3b48cc; + LineColor: #232f3e; LineStyle: 0; LineThickness: 2; - FontColor: #3b48cc; + FontColor: #232f3e; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Region - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { BackgroundColor: #111111; - LineColor: #147eba; + LineColor: #d86613; LineStyle: 0; LineThickness: 2; - FontColor: #147eba; + FontColor: #d86613; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Amazon Web Services - Route 53 - .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { BackgroundColor: #111111; - LineColor: #693cc5; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - FontColor: #693cc5; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Application - .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { BackgroundColor: #111111; - LineColor: #cccccc; + LineColor: #3b48cc; LineStyle: 0; LineThickness: 2; - RoundCorner: 20; - FontColor: #cccccc; + FontColor: #3b48cc; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Element,Database - .Element-RWxlbWVudCxEYXRhYmFzZQ== { + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { BackgroundColor: #111111; - LineColor: #cccccc; + LineColor: #147eba; LineStyle: 0; LineThickness: 2; - FontColor: #cccccc; + FontColor: #147eba; FontSize: 24; HorizontalAlignment: center; Shadowing: 0; MaximumWidth: 200; } - // Relationship - .Relationship-UmVsYXRpb25zaGlw { - LineThickness: 2; - LineStyle: 10-10; - LineColor: #cccccc; - FontColor: #cccccc; - FontSize: 24; - } // transparent element for relationships in legend .Element-Transparent { BackgroundColor: transparent; @@ -4178,19 +4185,19 @@ public void amazonWebServicesExample_Dark() throws Exception { } - rectangle "==Amazon Web Services - Auto Scaling\\n\\n" <> + rectangle "==Amazon Web Services - Auto Scaling\\n\\n" <> - rectangle "==Amazon Web Services - Cloud\\n\\n" <> + rectangle "==Amazon Web Services - Cloud\\n\\n" <> - rectangle "==Amazon Web Services - EC2\\n\\n" <> + rectangle "==Amazon Web Services - EC2\\n\\n" <> - rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n" <> + rectangle "==Amazon Web Services - RDS\\n\\n" <> - rectangle "==Amazon Web Services - RDS\\n\\n" <> + rectangle "==Amazon Web Services - RDS MySQL instance\\n\\n" <> - rectangle "==Amazon Web Services - RDS MySQL instance\\n\\n" <> + rectangle "==Amazon Web Services - Region\\n\\n" <> - rectangle "==Amazon Web Services - Region\\n\\n" <> + rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n" <> rectangle "==Amazon Web Services - Route 53\\n\\n" <> From 2b8191a839ef16484cb978d3d452c82f821e35ef Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 30 Sep 2025 13:08:23 +0100 Subject: [PATCH 395/418] structurizr-inspection: Fixes a bug preventing inspection severity to be specified via linked relationships. --- changelog.md | 1 + .../PropertyBasedSeverityStrategy.java | 2 +- .../PropertyBasedSeverityStrategyTests.java | 22 +++++++++++++++---- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index ec4b6548e..17922f372 100644 --- a/changelog.md +++ b/changelog.md @@ -29,6 +29,7 @@ - structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable). - structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties. - structurizr-inspection: Fixes `model.deploymentnode.technology` (it was checking the description property rather than technology). +- structurizr-inspection: Fixes a bug preventing inspection severity to be specified via linked relationships. ## v4.1.0 (28th May 2025) diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/PropertyBasedSeverityStrategy.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/PropertyBasedSeverityStrategy.java index 755a1c703..0483cba69 100644 --- a/structurizr-inspection/src/main/java/com/structurizr/inspection/PropertyBasedSeverityStrategy.java +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/PropertyBasedSeverityStrategy.java @@ -69,7 +69,7 @@ public Severity getSeverity(Inspection inspection, Relationship relationship) { Element source = relationship.getSource(); Relationship linkedRelationship = null; if (!StringUtils.isNullOrEmpty(relationship.getLinkedRelationshipId())) { - inspection.getWorkspace().getModel().getRelationship(relationship.getLinkedRelationshipId()); + linkedRelationship = inspection.getWorkspace().getModel().getRelationship(relationship.getLinkedRelationshipId()); } String allRelationshipsType = inspection.getType(); diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/PropertyBasedSeverityStrategyTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/PropertyBasedSeverityStrategyTests.java index e99e4b57e..e6bf91594 100644 --- a/structurizr-inspection/src/test/java/com/structurizr/inspection/PropertyBasedSeverityStrategyTests.java +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/PropertyBasedSeverityStrategyTests.java @@ -5,10 +5,7 @@ import com.structurizr.inspection.model.RelationshipDescriptionInspection; import com.structurizr.inspection.model.RelationshipTechnologyInspection; import com.structurizr.inspection.workspace.WorkspaceScopeInspection; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Relationship; -import com.structurizr.model.SoftwareSystem; +import com.structurizr.model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -254,4 +251,21 @@ void getSeverityForRelationship_BetweenComponents_WhenSpecifiedByRelationshipTyp assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, relationship)); } + @Test + void getSeverityForRelationship_WhenSpecifiedInLinkedRelationship() { + inspection = new RelationshipTechnologyInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + workspace.getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + Container container1 = softwareSystem.addContainer("Container 1"); + Component component1 = container1.addComponent("Component 1"); + Container container2 = softwareSystem.addContainer("Container 2"); + Component component2 = container2.addComponent("Component 2"); + Relationship relationship = component1.uses(component2, ""); + Relationship impliedRelationship = container1.getEfferentRelationshipWith(container2); + + // specify in original relationship + relationship.addProperty("structurizr.inspection.model.relationship.technology", "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, impliedRelationship)); + } + } \ No newline at end of file From ba4fd46c4f4a8007ef95fc119ea878337f00a794 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 18 Oct 2025 14:52:57 +0100 Subject: [PATCH 396/418] Fixes intermittently failing tests. --- .../encryption/AesEncryptionStrategy.java | 3 +- .../AesEncryptionStrategyTests.java | 57 +++++++++---------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java b/structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java index 2279304e2..4f43b7862 100644 --- a/structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java +++ b/structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java @@ -7,6 +7,7 @@ import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; @@ -79,7 +80,7 @@ public String decrypt(String ciphertext) throws Exception { cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(DatatypeConverter.parseHexBinary(iv))); byte[] unencrypted = cipher.doFinal(Base64.getDecoder().decode(ciphertext)); - return new String(unencrypted, "UTF-8"); + return new String(unencrypted, StandardCharsets.UTF_8); } private SecretKey createSecretKey() throws NoSuchAlgorithmException, InvalidKeySpecException { diff --git a/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java b/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java index 3af514905..33171b4b3 100644 --- a/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java +++ b/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java @@ -2,33 +2,30 @@ import org.junit.jupiter.api.Test; -import javax.crypto.BadPaddingException; -import java.security.InvalidKeyException; - -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; public class AesEncryptionStrategyTests { @Test void encrypt_EncryptsPlaintext() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "06DC30A48ADEEE72D98E33C2CEAEAD3E", "ED124530AF64A5CAD8EF463CF5628434", "password"); - String ciphertext = strategy.encrypt("Hello world"); + assertEquals("A/DzjV17WVS6ZAKsLOaC/Q==", ciphertext); } @Test void decrypt_decryptsTheCiphertext_WhenTheSameStrategyInstanceIsUsed() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); + assertEquals("Hello world", strategy.decrypt(ciphertext)); } @Test void decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() throws Exception { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), strategy.getSalt(), strategy.getIv(), "password"); @@ -39,65 +36,65 @@ void decrypt_decryptsTheCiphertext_WhenTheSameConfigurationIsUsed() throws Excep void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectKeySizeIsUsed() throws Exception { try { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); strategy = new AesEncryptionStrategy(256, strategy.getIterationCount(), strategy.getSalt(), strategy.getIv(), "password"); - strategy.decrypt(ciphertext); - } catch (BadPaddingException | InvalidKeyException bpe) { - // BadPaddingException is thrown on Mac and Linux - // InvalidKeyException is thrown in Windows + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); } catch (Exception e) { - fail(); + // this is okay } } @Test void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUsed() throws Exception { - assertThrows(BadPaddingException.class, () -> { + try { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); strategy = new AesEncryptionStrategy(strategy.getKeySize(), 2000, strategy.getSalt(), strategy.getIv(), "password"); - strategy.decrypt(ciphertext); - }); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } } @Test void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throws Exception { - assertThrows(BadPaddingException.class, () -> { + try { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), "133D30C2A658B3081279A97FD3B1F7CDE10C4FB61D39EEA8", strategy.getIv(), "password"); - strategy.decrypt(ciphertext); - }); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } } @Test void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws Exception { - assertThrows(BadPaddingException.class, () -> { + try { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), strategy.getSalt(), "1DED89E4FB15F61DC6433E3BADA4A891", "password"); - strategy.decrypt(ciphertext); - }); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } } @Test void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectPassphraseIsUsed() throws Exception { - assertThrows(BadPaddingException.class, () -> { + try { AesEncryptionStrategy strategy = new AesEncryptionStrategy(128, 1000, "password"); - String ciphertext = strategy.encrypt("Hello world"); strategy = new AesEncryptionStrategy(strategy.getKeySize(), strategy.getIterationCount(), strategy.getSalt(), strategy.getIv(), "The Wrong Password"); - strategy.decrypt(ciphertext); - }); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } } -} +} \ No newline at end of file From ee765b6250e8bc064ea7fea2a307792f761fc46b Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Oct 2025 10:18:36 +0100 Subject: [PATCH 397/418] structurizr-dsl: Deprecates `setRestricted(boolean)` in favour of finer-grained features. --- changelog.md | 1 + structurizr-client/build.gradle | 4 +- .../java/com/structurizr/http/HttpClient.java | 129 ++++++++++++++ .../com/structurizr/http/RemoteContent.java | 38 +++++ .../java/com/structurizr/view/ThemeUtils.java | 82 ++++----- .../com/structurizr/http/HttpClientTests.java | 50 ++++++ structurizr-core/build.gradle | 2 +- .../util/FeatureNotEnabledException.java | 13 ++ .../java/com/structurizr/util/ImageUtils.java | 93 ++--------- .../main/java/com/structurizr/util/Url.java | 23 +++ .../com/structurizr/util/ImageUtilsTests.java | 22 --- .../com/structurizr/dsl/AbstractParser.java | 29 ---- .../com/structurizr/dsl/BrandingParser.java | 24 ++- .../java/com/structurizr/dsl/DslContext.java | 20 +++ .../com/structurizr/dsl/DslParserContext.java | 8 +- .../structurizr/dsl/ElementStyleParser.java | 24 ++- .../java/com/structurizr/dsl/Features.java | 16 ++ .../dsl/ImageViewContentParser.java | 107 ++++++------ .../dsl/ImpliedRelationshipsParser.java | 7 +- .../com/structurizr/dsl/IncludeParser.java | 54 ++++-- .../com/structurizr/dsl/RemoteContent.java | 24 --- .../structurizr/dsl/StructurizrDslParser.java | 146 +++++++++------- .../java/com/structurizr/dsl/ThemeParser.java | 15 +- .../com/structurizr/dsl/WorkspaceParser.java | 42 +++-- .../structurizr/dsl/BrandingParserTests.java | 49 +++++- .../java/com/structurizr/dsl/DslTests.java | 47 +++--- .../dsl/ElementStyleParserTests.java | 67 +++++++- .../dsl/ImageViewContentParserTests.java | 158 ++++++++++++++---- .../dsl/ImpliedRelationshipsParserTests.java | 38 +++-- .../structurizr/dsl/IncludeParserTests.java | 59 ++++++- .../com/structurizr/dsl/ThemeParserTests.java | 47 ++++-- .../structurizr/dsl/WorkspaceParserTests.java | 28 +++- structurizr-import/build.gradle | 2 +- .../diagrams/AbstractDiagramImporter.java | 13 +- .../diagrams/image/ImageImporter.java | 32 ++-- .../diagrams/kroki/KrokiImporter.java | 16 +- .../diagrams/mermaid/MermaidImporter.java | 16 +- .../diagrams/plantuml/PlantUMLImporter.java | 17 +- .../mermaid/MermaidImporterTests.java | 4 +- .../plantuml/PlantUMLImporterTests.java | 2 +- 40 files changed, 1073 insertions(+), 495 deletions(-) create mode 100644 structurizr-client/src/main/java/com/structurizr/http/HttpClient.java create mode 100644 structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java create mode 100644 structurizr-client/src/test/java/com/structurizr/http/HttpClientTests.java create mode 100644 structurizr-core/src/main/java/com/structurizr/util/FeatureNotEnabledException.java delete mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RemoteContent.java diff --git a/changelog.md b/changelog.md index 17922f372..0f3098447 100644 --- a/changelog.md +++ b/changelog.md @@ -20,6 +20,7 @@ - structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. - structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. - structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). +- structurizr-dsl: Deprecates `setRestricted(boolean)` in favour of finer-grained features. - structurizr-export: Removes support for deprecated enterprise and location concepts. - structurizr-export: PlantUML exporters - replaces skinparams with styles. - structurizr-export: PlantUML exporters - adds support for dark mode exports. diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index e078cd9cc..a868ccf81 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -2,8 +2,8 @@ dependencies { api project(':structurizr-core') - api 'com.fasterxml.jackson.core:jackson-databind:2.18.3' - api 'org.apache.httpcomponents.client5:httpclient5:5.4.3' + api 'com.fasterxml.jackson.core:jackson-databind:2.20.0' + api 'org.apache.httpcomponents.client5:httpclient5:5.5.1' api 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' } diff --git a/structurizr-client/src/main/java/com/structurizr/http/HttpClient.java b/structurizr-client/src/main/java/com/structurizr/http/HttpClient.java new file mode 100644 index 000000000..69e2db865 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/http/HttpClient.java @@ -0,0 +1,129 @@ +package com.structurizr.http; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Wrapper for the HTTPClient in Apache HttpComponents, with optional caching and allowed URLs (via regexes). + */ +public class HttpClient { + + public static final String CONTENT_TYPE_IMAGE_PNG = "image/png"; + + private static final int HTTP_OK_STATUS = 200; + + private int timeout = 10000; // milliseconds + private final Set allowedUrlRegexes = new HashSet<>(); + + private final Map contentCache = new HashMap<>(); + + public HttpClient() { + } + + /** + * Sets the timeout in milliseconds. + * + * @param timeoutInMilliseconds the timeout in milliseconds + */ + public void setTimeout(int timeoutInMilliseconds) { + if (timeoutInMilliseconds < 0) { + throw new IllegalArgumentException("Timeout must be a positive integer"); + } + + this.timeout = timeoutInMilliseconds; + } + + /** + * HTTP GET of a URL, without caching. + * + * @param url the URL, as a String + * @return a RemoteContent object representing the response + */ + public RemoteContent get(String url) { + return get(url, false); + } + + /** + * HTTP GET of a URL. + * + * @param url the URL, as a String + * @param cache true if the result should be cached, false otherwise + * @return a RemoteContent object representing the response + */ + public RemoteContent get(String url, boolean cache) { + if (!isAllowed(url)) { + throw new RuntimeException("Access to " + url + " is not permitted"); + } + + RemoteContent remoteContent = contentCache.get(url); + if (remoteContent == null) { + ConnectionConfig connectionConfig = ConnectionConfig.custom() + .setConnectTimeout(timeout, TimeUnit.MILLISECONDS) + .setSocketTimeout(timeout, TimeUnit.MILLISECONDS) + .build(); + + BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); + cm.setConnectionConfig(connectionConfig); + + try (CloseableHttpClient httpClient = HttpClientBuilder.create() + .useSystemProperties() + .setConnectionManager(cm) + .build()) { + + HttpGet httpGet = new HttpGet(url); + CloseableHttpResponse response = httpClient.execute(httpGet); + + int httpStatus = response.getCode(); + if (httpStatus == HTTP_OK_STATUS) { + String contentType = response.getEntity().getContentType(); + if (CONTENT_TYPE_IMAGE_PNG.equals(contentType)) { + remoteContent = new RemoteContent(EntityUtils.toByteArray(response.getEntity()), contentType); + } else { + remoteContent = new RemoteContent(EntityUtils.toString(response.getEntity()), contentType); + } + + if (cache) { + contentCache.put(url, remoteContent); + } + } else { + throw new RuntimeException("The content from " + url + " could not be loaded: HTTP status=" + httpStatus); + } + } catch (Exception ioe) { + throw new RuntimeException("The content from " + url + " could not be loaded: " + ioe.getMessage()); + } + } + + return remoteContent; + } + + /** + * Adds an allowed URL regex. + * + * @param regex the regex to allow + */ + public void allow(String regex) { + allowedUrlRegexes.add(regex); + } + + private boolean isAllowed(String url) { + for (String regex : allowedUrlRegexes) { + if (url.matches(regex)) { + return true; + } + } + + return false; + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java b/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java new file mode 100644 index 000000000..4a4b9ae0d --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java @@ -0,0 +1,38 @@ +package com.structurizr.http; + +/** + * Wrapper for remote content loaded via HTTP. + */ +public final class RemoteContent { + + public static final String CONTENT_TYPE_JSON = "application/json"; + + private final String content; + private final byte[] bytes; + private final String contentType; + + RemoteContent(String content, String contentType) { + this.content = content; + this.bytes = null; + this.contentType = contentType; + } + + RemoteContent(byte[] content, String contentType) { + this.content = null; + this.bytes = content; + this.contentType = contentType; + } + + public String getContentAsString() { + return content; + } + + public byte[] getContentAsBytes() { + return bytes; + } + + public String getContentType() { + return contentType; + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java index 283ec0f3a..0ecd531c6 100644 --- a/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java +++ b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java @@ -5,32 +5,22 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; +import com.structurizr.http.RemoteContent; import com.structurizr.io.WorkspaceWriterException; -import com.structurizr.model.Relationship; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; import com.structurizr.util.Url; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.config.ConnectionConfig; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; -import org.apache.hc.core5.http.io.entity.EntityUtils; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.concurrent.TimeUnit; /** * Some utility methods for exporting themes to JSON. */ public final class ThemeUtils { - private static final int HTTP_OK_STATUS = 200; - private static final int DEFAULT_TIMEOUT_IN_MILLISECONDS = 10000; /** @@ -90,27 +80,38 @@ public static void loadThemes(Workspace workspace) throws Exception { * @throws Exception if something goes wrong */ public static void loadThemes(Workspace workspace, int timeoutInMilliseconds) throws Exception { + HttpClient httpClient = new HttpClient(); + httpClient.setTimeout(timeoutInMilliseconds); + + loadThemes(workspace, httpClient); + } + + public static void loadThemes(Workspace workspace, HttpClient httpClient) throws Exception { for (String themeLocation : workspace.getViews().getConfiguration().getThemes()) { if (Url.isUrl(themeLocation)) { - String json = loadFrom(themeLocation, timeoutInMilliseconds); - Theme theme = fromJson(json); - String baseUrl = themeLocation.substring(0, themeLocation.lastIndexOf('/') + 1); - - for (ElementStyle elementStyle : theme.getElements()) { - String icon = elementStyle.getIcon(); - if (!StringUtils.isNullOrEmpty(icon)) { - if (icon.startsWith("http")) { - // okay, image served over HTTP - } else if (icon.startsWith("data:image")) { - // also okay, data URI - } else { - // convert the relative icon filename into a full URL - elementStyle.setIcon(baseUrl + icon); + RemoteContent remoteContent = httpClient.get(themeLocation); + if (remoteContent.getContentType().equals(RemoteContent.CONTENT_TYPE_JSON)) { + Theme theme = fromJson(remoteContent.getContentAsString()); + String baseUrl = themeLocation.substring(0, themeLocation.lastIndexOf('/') + 1); + + for (ElementStyle elementStyle : theme.getElements()) { + String icon = elementStyle.getIcon(); + if (!StringUtils.isNullOrEmpty(icon)) { + if (Url.isHttpUrl(icon) || Url.isHttpsUrl(icon)) { + // okay, image served over HTTP or HTTPS + } else if (icon.startsWith("data:image")) { + // also okay, data URI + } else { + // convert the relative icon filename into a full URL + elementStyle.setIcon(baseUrl + icon); + } } } - } - workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(theme); + workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(theme); + } else { + throw new RuntimeException(String.format("%s - expected content type of %s, actual content type is %s", themeLocation, RemoteContent.CONTENT_TYPE_JSON, remoteContent.getContentType())); + } } } } @@ -144,31 +145,6 @@ public static void inlineTheme(Workspace workspace, File file) throws Exception workspace.getViews().getConfiguration().getStyles().inlineTheme(theme); } - private static String loadFrom(String url, int timeoutInMilliseconds) throws Exception { - ConnectionConfig connectionConfig = ConnectionConfig.custom() - .setConnectTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) - .setSocketTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) - .build(); - - BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); - cm.setConnectionConfig(connectionConfig); - - try (CloseableHttpClient httpClient = HttpClientBuilder.create() - .useSystemProperties() - .setConnectionManager(cm) - .build()) { - - HttpGet httpGet = new HttpGet(url); - - CloseableHttpResponse response = httpClient.execute(httpGet); - if (response.getCode() == HTTP_OK_STATUS) { - return EntityUtils.toString(response.getEntity()); - } - } - - return ""; - } - private static Theme fromJson(String json) throws Exception { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); diff --git a/structurizr-client/src/test/java/com/structurizr/http/HttpClientTests.java b/structurizr-client/src/test/java/com/structurizr/http/HttpClientTests.java new file mode 100644 index 000000000..a98d05db7 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/http/HttpClientTests.java @@ -0,0 +1,50 @@ +package com.structurizr.http; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HttpClientTests { + + @Test + @Tag("IntegrationTest") + void get_WhenNoAllowedUrlsAreConfigured() { + HttpClient httpClient = new HttpClient(); + + try { + httpClient.get("https://static.structurizr.com/themes/microsoft-azure-2024.07.15/icons.json"); + } catch (Exception e) { + assertEquals("Access to https://static.structurizr.com/themes/microsoft-azure-2024.07.15/icons.json is not permitted", e.getMessage()); + } + } + + @Test + @Tag("IntegrationTest") + void get_WithAllowedUrl() { + HttpClient httpClient = new HttpClient(); + httpClient.allow("https://static.structurizr.com/themes/amazon-web-services.*"); + + httpClient.get("https://static.structurizr.com/themes/amazon-web-services-2023.01.31/icons.json"); + + try { + httpClient.get("https://static.structurizr.com/themes/microsoft-azure-2024.07.15/icons.json"); + } catch (Exception e) { + assertEquals("Access to https://static.structurizr.com/themes/microsoft-azure-2024.07.15/icons.json is not permitted", e.getMessage()); + } + } + + @Test + @Tag("IntegrationTest") + void get_WithDisallowedUrl() { + HttpClient httpClient = new HttpClient(); + httpClient.allow("https://static.structurizr.com/.*"); + + try { + httpClient.get("https://example.com"); + } catch (Exception e) { + assertEquals("Access to https://example.com is not permitted", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index 362d12137..2ac80b7d1 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,6 +1,6 @@ dependencies { - api 'com.fasterxml.jackson.core:jackson-annotations:2.18.3' + api 'com.fasterxml.jackson.core:jackson-annotations:2.20' api 'com.google.code.findbugs:jsr305:3.0.2' api 'commons-logging:commons-logging:1.3.5' diff --git a/structurizr-core/src/main/java/com/structurizr/util/FeatureNotEnabledException.java b/structurizr-core/src/main/java/com/structurizr/util/FeatureNotEnabledException.java new file mode 100644 index 000000000..9e75481c7 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/util/FeatureNotEnabledException.java @@ -0,0 +1,13 @@ +package com.structurizr.util; + +public final class FeatureNotEnabledException extends RuntimeException { + + public FeatureNotEnabledException(String feature) { + super("Feature " + feature + " is not enabled"); + } + + public FeatureNotEnabledException(String feature, String message) { + super(String.format("%s (feature %s is not enabled)", message, feature)); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java index e0baf42e6..65a9f8260 100644 --- a/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java +++ b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java @@ -6,18 +6,12 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.net.URL; import java.net.URLConnection; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.file.Files; import java.util.Base64; -import java.util.HashMap; -import java.util.Map; /** - * Some utility methods for dealing with images. + * Some utility methods for dealing with images as files and data URIs. */ public class ImageUtils { @@ -30,8 +24,6 @@ public class ImageUtils { public static final String CONTENT_TYPE_IMAGE_JPG = "image/jpeg"; public static final String CONTENT_TYPE_IMAGE_SVG = "image/svg+xml"; - private static final Map imageCache = new HashMap<>(); - /** * Gets the content type of the specified file representing an image. * @@ -56,23 +48,6 @@ public static String getContentType(@Nonnull File file) throws IOException { return contentType; } - /** - * Gets the content type of the specified URL representing an image. - * - * @param url a URL pointing to an image - * @return a content type (e.g. "image/png") - * @throws IOException if there is an error reading the file - */ - public static String getContentType(String url) throws IOException { - if (StringUtils.isNullOrEmpty(url)) { - throw new IllegalArgumentException("A URL must be specified."); - } - - URLConnection connection = new URL(url).openConnection(); - connection.setConnectTimeout(1000 * 30); - return connection.getContentType(); - } - /** * Gets the content type of the specified data URI representing an image. * @@ -131,58 +106,24 @@ public static String getImageAsDataUri(File file) throws IOException { return DATA_URI_PREFIX + contentType + ";base64," + base64Content; } - public static String getSvgAsDataUri(@Nonnull URL url) throws Exception { - return getSvgAsDataUri(url, false); - } - - public static String getSvgAsDataUri(@Nonnull URL url, boolean cache) throws Exception { - String urlAsString = url.toString(); - String dataUri = cache ? imageCache.get(urlAsString) : null; - - if (StringUtils.isNullOrEmpty(dataUri)) { - HttpRequest request = HttpRequest.newBuilder() - .uri(url.toURI()) - .header("accept", CONTENT_TYPE_IMAGE_SVG) - .build(); - HttpClient client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - String svg = response.body(); - - dataUri = DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_SVG + ";base64," + Base64.getEncoder().encodeToString(svg.getBytes()); - imageCache.put(urlAsString, dataUri); - } - - return dataUri; - } - - public static String getPngAsDataUri(@Nonnull URL url) throws Exception { - return getPngAsDataUri(url, false); + /** + * Converts an SVG string to a data URI. + * + * @param svg the SVG string + * @return a data URI + */ + public static String getSvgAsDataUri(String svg) { + return DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_SVG + ";base64," + Base64.getEncoder().encodeToString(svg.getBytes()); } - public static String getPngAsDataUri(@Nonnull URL url, boolean cache) throws Exception { - String urlAsString = url.toString(); - String dataUri = cache ? imageCache.get(urlAsString) : null; - - if (StringUtils.isNullOrEmpty(dataUri)) { - HttpRequest request = HttpRequest.newBuilder() - .uri(url.toURI()) - .header("accept", CONTENT_TYPE_IMAGE_PNG) - .build(); - HttpClient client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); - byte[] png = response.body(); - - dataUri = DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_PNG + ";base64," + Base64.getEncoder().encodeToString(png); - imageCache.put(urlAsString, dataUri); - } - - return dataUri; + /** + * Converts an PNG to a data URI. + * + * @param png the PNG as a byte array + * @return a data URI + */ + public static String getPngAsDataUri(byte[] png) { + return DATA_URI_PREFIX + CONTENT_TYPE_IMAGE_PNG + ";base64," + Base64.getEncoder().encodeToString(png); } public static void validateImage(String imageDescriptor) { diff --git a/structurizr-core/src/main/java/com/structurizr/util/Url.java b/structurizr-core/src/main/java/com/structurizr/util/Url.java index adf0989a4..ee26a38bb 100644 --- a/structurizr-core/src/main/java/com/structurizr/util/Url.java +++ b/structurizr-core/src/main/java/com/structurizr/util/Url.java @@ -8,6 +8,9 @@ */ public class Url { + private static final String HTTPS_PROTOCOL = "https://"; + private static final String HTTP_PROTOCOL = "http://"; + public static final String INTRA_WORKSPACE_URL_PREFIX = "{workspace}"; public static final String INTER_WORKSPACE_URL_REGEX = "\\{workspace:\\d+\\}.*"; @@ -30,4 +33,24 @@ public static boolean isUrl(String urlAsString) { return false; } + /** + * Determines whether the supplied string is a valid HTTPS URL. + * + * @param urlAsString the URL, as a String + * @return true if the URL is valid, false otherwise + */ + public static boolean isHttpsUrl(String urlAsString) { + return isUrl(urlAsString) && urlAsString.toLowerCase().startsWith(HTTPS_PROTOCOL); + } + + /** + * Determines whether the supplied string is a valid HTTP URL. + * + * @param urlAsString the URL, as a String + * @return true if the URL is valid, false otherwise + */ + public static boolean isHttpUrl(String urlAsString) { + return isUrl(urlAsString) && urlAsString.toLowerCase().startsWith(HTTP_PROTOCOL); + } + } \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java index fe5b5883f..5f2a700bc 100644 --- a/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java +++ b/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java @@ -62,28 +62,6 @@ void getContentType_ReturnsTheContentType_WhenASVGFileIsSpecified() throws Excep assertEquals("image/svg+xml", contentType); } - @Test - void getContentType_ThrowsAnException_WhenANullUrlIsSpecified() throws Exception { - try { - ImageUtils.getContentType((String)null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A URL must be specified.", iae.getMessage()); - } - } - - @Test - void getContentType_ReturnsTheContentType_WhenAPNGUrlIsSpecified() throws Exception { - String contentType = ImageUtils.getContentType(new File("../structurizr-core/test/unit/com/structurizr/util/image.png").toURI().toURL().toExternalForm()); - assertEquals("image/png", contentType); - } - - @Test - void getContentType_ReturnsTheContentType_WhenASVGUrlIsSpecified() throws Exception { - String contentType = ImageUtils.getContentType(new File("../structurizr-core/test/unit/com/structurizr/util/image.svg").toURI().toURL().toExternalForm()); - assertEquals("image/svg+xml", contentType); - } - @Test void getContentTypeFromDataUri_ThrowsAnException_WhenANullDataUriIsSpecified() throws Exception { try { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java index 5d41ec735..d45b3a2f6 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java @@ -1,33 +1,4 @@ package com.structurizr.dsl; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.io.entity.EntityUtils; - abstract class AbstractParser { - - private static final int HTTP_OK_STATUS = 200; - - String removeNonWordCharacters(String name) { - return name.replaceAll("\\W", ""); - } - - protected RemoteContent readFromUrl(String url) { - try (CloseableHttpClient httpClient = HttpClients.createSystem()) { - HttpGet httpGet = new HttpGet(url); - CloseableHttpResponse response = httpClient.execute(httpGet); - - int httpStatus = response.getCode(); - if (httpStatus == HTTP_OK_STATUS) { - return new RemoteContent(EntityUtils.toString(response.getEntity()), response.getEntity().getContentType()); - } else { - throw new RuntimeException("The content from " + url + " could not be loaded: HTTP status=" + httpStatus); - } - } catch (Exception ioe) { - throw new RuntimeException("The content from " + url + " could not be loaded: " + ioe.getMessage()); - } - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java index 5b82b6594..add3a4712 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java @@ -1,6 +1,8 @@ package com.structurizr.dsl; +import com.structurizr.util.FeatureNotEnabledException; import com.structurizr.util.ImageUtils; +import com.structurizr.util.Url; import com.structurizr.view.Font; import java.io.File; @@ -15,7 +17,7 @@ final class BrandingParser extends AbstractParser { private static final int FONT_NAME_INDEX = 1; private static final int FONT_URL_INDEX = 2; - void parseLogo(BrandingDslContext context, Tokens tokens, boolean restricted) { + void parseLogo(BrandingDslContext context, Tokens tokens) { // logo if (tokens.hasMoreThan(LOGO_FILE_INDEX)) { @@ -23,11 +25,25 @@ void parseLogo(BrandingDslContext context, Tokens tokens, boolean restricted) { } else if (tokens.includes(LOGO_FILE_INDEX)) { String path = tokens.get(1); - if (path.startsWith("data:image/") || path.startsWith("https://") || path.startsWith("http://")) { + if (path.startsWith("data:image/")) { ImageUtils.validateImage(path); context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(path); + } else if (Url.isHttpsUrl(path)) { + if (context.getFeatures().isEnabled(Features.HTTPS)) { + ImageUtils.validateImage(path); + context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(path); + } else { + throw new FeatureNotEnabledException(Features.HTTPS, "Icons via HTTPS are not permitted"); + } + } else if (Url.isHttpUrl(path)) { + if (context.getFeatures().isEnabled(Features.HTTP)) { + ImageUtils.validateImage(path); + context.getWorkspace().getViews().getConfiguration().getBranding().setLogo(path); + } else { + throw new FeatureNotEnabledException(Features.HTTP, "Icons via HTTP are not permitted"); + } } else { - if (!restricted) { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { File file = new File(context.getFile().getParent(), path); if (file.exists() && !file.isDirectory()) { context.setDslPortable(false); @@ -40,6 +56,8 @@ void parseLogo(BrandingDslContext context, Tokens tokens, boolean restricted) { } else { throw new RuntimeException(path + " does not exist"); } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "!branding is not permitted"); } } } else { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java index 4998d6864..3c31f3e76 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java @@ -1,6 +1,7 @@ package com.structurizr.dsl; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; import com.structurizr.model.Element; import com.structurizr.model.Relationship; @@ -21,6 +22,9 @@ abstract class DslContext { protected IdentifiersRegister identifiersRegister = new IdentifiersRegister(); + private Features features = new Features(); + private HttpClient httpClient = new HttpClient(); + Workspace getWorkspace() { return workspace; } @@ -116,6 +120,22 @@ Relationship getRelationship(String identifier) { return identifiersRegister.getRelationship(identifier.toLowerCase()); } + Features getFeatures() { + return features; + } + + void setFeatures(Features features) { + this.features = features; + } + + HttpClient getHttpClient() { + return httpClient; + } + + void setHttpClient(HttpClient httpClient) { + this.httpClient = httpClient; + } + protected Class loadClass(String fqn, File dslFile) throws Exception { File pluginsDirectory = new File(dslFile.getParent(), PLUGINS_DIRECTORY_NAME); URL[] urls = new URL[0]; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java index ed964b481..8713345de 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java @@ -6,12 +6,10 @@ final class DslParserContext extends DslContext { private final StructurizrDslParser parser; private final File file; - private final boolean restricted; - DslParserContext(StructurizrDslParser parser, File file, boolean restricted) { + DslParserContext(StructurizrDslParser parser, File file) { this.parser = parser; this.file = file; - this.restricted = restricted; } StructurizrDslParser getParser() { @@ -22,10 +20,6 @@ File getFile() { return file; } - boolean isRestricted() { - return restricted; - } - void copyFrom(IdentifiersRegister identifersRegister) { for (String identifier : identifersRegister.getElementIdentifiers()) { this.identifiersRegister.register(identifier, identifersRegister.getElement(identifier)); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java index dd83c431e..4fe23bde3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java @@ -1,8 +1,10 @@ package com.structurizr.dsl; import com.structurizr.Workspace; +import com.structurizr.util.FeatureNotEnabledException; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; +import com.structurizr.util.Url; import com.structurizr.view.*; import java.io.File; @@ -284,7 +286,7 @@ void parseDescription(ElementStyleDslContext context, Tokens tokens) { } } - void parseIcon(ElementStyleDslContext context, Tokens tokens, boolean restricted) { + void parseIcon(ElementStyleDslContext context, Tokens tokens) { ElementStyle style = context.getStyle(); if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) { @@ -294,11 +296,25 @@ void parseIcon(ElementStyleDslContext context, Tokens tokens, boolean restricted if (tokens.includes(FIRST_PROPERTY_INDEX)) { String path = tokens.get(1); - if (path.startsWith("data:image/") || path.startsWith("https://") || path.startsWith("http://")) { + if (path.startsWith("data:image/")) { ImageUtils.validateImage(path); style.setIcon(path); + } else if (Url.isHttpsUrl(path)) { + if (context.getFeatures().isEnabled(Features.HTTPS)) { + ImageUtils.validateImage(path); + style.setIcon(path); + } else { + throw new FeatureNotEnabledException(Features.HTTPS, "Icons via HTTPS are not permitted"); + } + } else if (Url.isHttpUrl(path)) { + if (context.getFeatures().isEnabled(Features.HTTP)) { + ImageUtils.validateImage(path); + style.setIcon(path); + } else { + throw new FeatureNotEnabledException(Features.HTTP, "Icons via HTTP are not permitted"); + } } else { - if (!restricted) { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { File file = new File(context.getFile().getParent(), path); if (file.exists() && !file.isDirectory()) { try { @@ -310,6 +326,8 @@ void parseIcon(ElementStyleDslContext context, Tokens tokens, boolean restricted } else { throw new RuntimeException(path + " does not exist"); } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "!icon is not permitted"); } } } else { diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java index 3d40aabb4..4e96d07ac 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java @@ -1,4 +1,20 @@ package com.structurizr.dsl; public final class Features extends com.structurizr.util.Features { + + public static final String PLUGINS = "structurizr.feature.dsl.plugins"; + public static final String SCRIPTS = "structurizr.feature.dsl.scripts"; + + public static final String COMPONENT_FINDER = "structurizr.feature.dsl.componentfinder"; + + public static final String INCLUDE = "structurizr.feature.dsl.include"; + + public static final String DOCUMENTATION = "structurizr.feature.dsl.documentation"; + public static final String DECISIONS = "structurizr.feature.dsl.decisions"; + + public static final String ENVIRONMENT = "structurizr.feature.dsl.environment"; + public static final String FILE_SYSTEM = "structurizr.feature.dsl.filesystem"; + public static final String HTTP = "structurizr.feature.dsl.http"; + public static final String HTTPS = "structurizr.feature.dsl.https"; + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java index b37b55dc8..e78d72ba3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -2,10 +2,12 @@ import com.structurizr.export.mermaid.MermaidDiagramExporter; import com.structurizr.export.plantuml.StructurizrPlantUMLExporter; +import com.structurizr.http.RemoteContent; import com.structurizr.importer.diagrams.image.ImageImporter; import com.structurizr.importer.diagrams.kroki.KrokiImporter; import com.structurizr.importer.diagrams.mermaid.MermaidImporter; import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter; +import com.structurizr.util.FeatureNotEnabledException; import com.structurizr.util.StringUtils; import com.structurizr.util.Url; import com.structurizr.view.ColorScheme; @@ -30,10 +32,7 @@ final class ImageViewContentParser extends AbstractParser { private static final int IMAGE_SOURCE_INDEX = 1; - private boolean restricted = false; - - ImageViewContentParser(boolean restricted) { - this.restricted = restricted; + ImageViewContentParser() { } void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { @@ -53,15 +52,15 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { try { if (source.contains("\n")) { // inline source - new PlantUMLImporter().importDiagram(context.getView(), source, colorScheme); + new PlantUMLImporter(context.getHttpClient()).importDiagram(context.getView(), source, colorScheme); } else { View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); if (viewWithKey instanceof ModelView) { String plantumlLight = new StructurizrPlantUMLExporter(ColorScheme.Light).export((ModelView) viewWithKey).getDefinition(); - new PlantUMLImporter().importDiagram(context.getView(), plantumlLight, ColorScheme.Light); + new PlantUMLImporter(context.getHttpClient()).importDiagram(context.getView(), plantumlLight, ColorScheme.Light); String plantumlDark = new StructurizrPlantUMLExporter(ColorScheme.Dark).export((ModelView) viewWithKey).getDefinition(); - new PlantUMLImporter().importDiagram(context.getView(), plantumlDark, ColorScheme.Dark); + new PlantUMLImporter(context.getHttpClient()).importDiagram(context.getView(), plantumlDark, ColorScheme.Dark); if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { context.getView().setTitle(viewWithKey.getTitle()); @@ -70,21 +69,28 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) { } context.getView().setDescription(viewWithKey.getDescription()); } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new PlantUMLImporter().importDiagram(context.getView(), content.getContent(), colorScheme); + if (Url.isHttpsUrl(source) || Url.isHttpUrl(source)) { + if (Url.isHttpsUrl(source) && !context.getFeatures().isEnabled(Features.HTTPS)) { + throw new FeatureNotEnabledException(Features.HTTPS, "Image views via HTTPS are not permitted"); + } + if (Url.isHttpUrl(source) && !context.getFeatures().isEnabled(Features.HTTP)) { + throw new FeatureNotEnabledException(Features.HTTP, "Image views via HTTP are not permitted"); + } + + RemoteContent content = context.getHttpClient().get(source); + new PlantUMLImporter(context.getHttpClient()).importDiagram(context.getView(), content.getContentAsString(), colorScheme); context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - if (!restricted) { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { context.setDslPortable(false); - new PlantUMLImporter().importDiagram(context.getView(), file, colorScheme); + new PlantUMLImporter(context.getHttpClient()).importDiagram(context.getView(), file, colorScheme); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } } else { - throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode"); + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "plantuml is not permitted"); } } } @@ -111,13 +117,13 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { try { if (source.contains("\n")) { // inline source - new MermaidImporter().importDiagram(context.getView(), source, colorScheme); + new MermaidImporter(context.getHttpClient()).importDiagram(context.getView(), source, colorScheme); } else { View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source); if (viewWithKey instanceof ModelView) { MermaidDiagramExporter exporter = new MermaidDiagramExporter(); String mermaid = exporter.export((ModelView) viewWithKey).getDefinition(); - new MermaidImporter().importDiagram(context.getView(), mermaid, colorScheme); + new MermaidImporter(context.getHttpClient()).importDiagram(context.getView(), mermaid, colorScheme); if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { context.getView().setTitle(viewWithKey.getTitle()); @@ -126,21 +132,28 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) { } context.getView().setDescription(viewWithKey.getDescription()); } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new MermaidImporter().importDiagram(context.getView(), content.getContent(), colorScheme); + if (Url.isHttpsUrl(source) || Url.isHttpUrl(source)) { + if (Url.isHttpsUrl(source) && !context.getFeatures().isEnabled(Features.HTTPS)) { + throw new FeatureNotEnabledException(Features.HTTPS, "Image views via HTTPS are not permitted"); + } + if (Url.isHttpUrl(source) && !context.getFeatures().isEnabled(Features.HTTP)) { + throw new FeatureNotEnabledException(Features.HTTP, "Image views via HTTP are not permitted"); + } + + RemoteContent content = context.getHttpClient().get(source); + new MermaidImporter(context.getHttpClient()).importDiagram(context.getView(), content.getContentAsString(), colorScheme); context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - if (!restricted) { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { context.setDslPortable(false); - new MermaidImporter().importDiagram(context.getView(), file, colorScheme); + new MermaidImporter(context.getHttpClient()).importDiagram(context.getView(), file, colorScheme); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } } else { - throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode"); + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "mermaid is not permitted"); } } } @@ -168,23 +181,30 @@ void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) { try { if (source.contains("\n")) { // inline source - new KrokiImporter().importDiagram(context.getView(), format, source, colorScheme); + new KrokiImporter(context.getHttpClient()).importDiagram(context.getView(), format, source, colorScheme); } else { - if (Url.isUrl(source)) { - RemoteContent content = readFromUrl(source); - new KrokiImporter().importDiagram(context.getView(), format, content.getContent(), colorScheme); + if (Url.isHttpsUrl(source) || Url.isHttpUrl(source)) { + if (Url.isHttpsUrl(source) && !context.getFeatures().isEnabled(Features.HTTPS)) { + throw new FeatureNotEnabledException(Features.HTTPS, "Image views via HTTPS are not permitted"); + } + if (Url.isHttpUrl(source) && !context.getFeatures().isEnabled(Features.HTTP)) { + throw new FeatureNotEnabledException(Features.HTTP, "Image views via HTTP are not permitted"); + } + + RemoteContent content = context.getHttpClient().get(source); + new KrokiImporter(context.getHttpClient()).importDiagram(context.getView(), format, content.getContentAsString(), colorScheme); context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1)); } else { - if (!restricted) { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { context.setDslPortable(false); - new KrokiImporter().importDiagram(context.getView(), format, file, colorScheme); + new KrokiImporter(context.getHttpClient()).importDiagram(context.getView(), format, file, colorScheme); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } } else { - throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode"); + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "kroki " + format + " is not permitted"); } } } @@ -208,19 +228,26 @@ void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { ColorScheme colorScheme = context.getColorScheme(); try { - if (Url.isUrl(source)) { - new ImageImporter().importDiagram(context.getView(), source, colorScheme); + if (Url.isHttpsUrl(source) || Url.isHttpUrl(source)) { + if (Url.isHttpsUrl(source) && !context.getFeatures().isEnabled(Features.HTTPS)) { + throw new FeatureNotEnabledException(Features.HTTPS, "Image views via HTTPS are not permitted"); + } + if (Url.isHttpUrl(source) && !context.getFeatures().isEnabled(Features.HTTP)) { + throw new FeatureNotEnabledException(Features.HTTP, "Image views via HTTP are not permitted"); + } + + new ImageImporter(context.getHttpClient()).importDiagram(context.getView(), source, colorScheme); } else { - if (!restricted) { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { File file = new File(dslFile.getParentFile(), source); if (file.exists()) { context.setDslPortable(false); - new ImageImporter().importDiagram(context.getView(), file, colorScheme); + new ImageImporter(context.getHttpClient()).importDiagram(context.getView(), file, colorScheme); } else { throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); } } else { - throw new RuntimeException("Images must be specified as a URL when running in restricted mode"); + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "image is not permitted"); } } } catch (Exception e) { @@ -228,18 +255,4 @@ void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) { } } - private ColorScheme calculateColorScheme(Tokens tokens, int index) { - if (tokens.includes(index)) { - if (ColorScheme.Dark.toString().equalsIgnoreCase(tokens.get(index))) { - return ColorScheme.Dark; - } else if (ColorScheme.Light.toString().equalsIgnoreCase(tokens.get(index))) { - return ColorScheme.Light; - } else { - throw new RuntimeException("Invalid color scheme \"" + tokens.get(index) + "\" - expected: light or dark"); - } - } - - return null; - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java index e880f25a7..086fdb9b3 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java @@ -3,6 +3,7 @@ import com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy; import com.structurizr.model.DefaultImpliedRelationshipsStrategy; import com.structurizr.model.ImpliedRelationshipsStrategy; +import com.structurizr.util.FeatureNotEnabledException; import java.io.File; import java.lang.reflect.Constructor; @@ -22,7 +23,7 @@ final class ImpliedRelationshipsParser extends AbstractParser { private static final String TRUE = "true"; private static final String FALSE = "false"; - void parse(DslContext context, Tokens tokens, File dslFile, boolean restricted) { + void parse(DslContext context, Tokens tokens, File dslFile) { // impliedRelationships if (tokens.hasMoreThan(OPTION_INDEX)) { @@ -40,9 +41,9 @@ void parse(DslContext context, Tokens tokens, File dslFile, boolean restricted) } else if (option.equalsIgnoreCase(TRUE)) { context.getWorkspace().getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); } else { - if (restricted) { + if (!context.getFeatures().isEnabled(Features.PLUGINS)) { if (!BUILT_IN_IMPLIED_RELATIONSHIPS_STRATEGIES.contains(option)) { - throw new RuntimeException("The implied relationships strategy " + option + " is not available when the DSL parser is running in restricted mode"); + throw new FeatureNotEnabledException(Features.PLUGINS, "The implied relationships strategy " + option + " is not available"); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java index bfea57783..d8ac5caef 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java @@ -1,5 +1,9 @@ package com.structurizr.dsl; +import com.structurizr.http.RemoteContent; +import com.structurizr.util.FeatureNotEnabledException; +import com.structurizr.util.Url; + import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -17,6 +21,10 @@ final class IncludeParser extends AbstractParser { List parse(DslContext context, File dslFile, Tokens tokens) { // !include + if (!context.getFeatures().isEnabled(Features.INCLUDE)) { + throw new FeatureNotEnabledException(Features.INCLUDE, "!include is not permitted"); + } + List includedFiles = new ArrayList<>(); if (tokens.hasMoreThan(SOURCE_INDEX)) { @@ -28,24 +36,40 @@ List parse(DslContext context, File dslFile, Tokens tokens) { } String source = tokens.get(SOURCE_INDEX); - if (source.startsWith("https://") || source.startsWith("http://")) { - RemoteContent content = readFromUrl(source); - List lines = Arrays.asList(content.getContent().split("\n")); - includedFiles.add(new IncludedFile(dslFile, lines)); + if (Url.isHttpsUrl(source)) { + if (context.getFeatures().isEnabled(Features.HTTPS)) { + RemoteContent content = context.getHttpClient().get(source); + List lines = Arrays.asList(content.getContentAsString().split("\n")); + includedFiles.add(new IncludedFile(dslFile, lines)); + } else { + throw new FeatureNotEnabledException(Features.HTTPS, "Includes via HTTPS are not permitted"); + } + } else if (Url.isHttpUrl(source)) { + if (context.getFeatures().isEnabled(Features.HTTP)) { + RemoteContent content = context.getHttpClient().get(source); + List lines = Arrays.asList(content.getContentAsString().split("\n")); + includedFiles.add(new IncludedFile(dslFile, lines)); + } else { + throw new FeatureNotEnabledException(Features.HTTP, "Includes via HTTP are not permitted"); + } } else { - if (dslFile != null) { - File path = new File(dslFile.getParent(), source); - - try { - if (!path.exists()) { - throw new RuntimeException(path.getCanonicalPath() + " could not be found"); + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + if (dslFile != null) { + File path = new File(dslFile.getParent(), source); + + try { + if (!path.exists()) { + throw new RuntimeException(path.getCanonicalPath() + " could not be found"); + } + + includedFiles.addAll(readFiles(path)); + context.setDslPortable(false); + } catch (IOException e) { + throw new RuntimeException("Error including " + path.getAbsolutePath() + ": " + e.getMessage()); } - - includedFiles.addAll(readFiles(path)); - context.setDslPortable(false); - } catch (IOException e) { - throw new RuntimeException("Error including " + path.getAbsolutePath() + ": " + e.getMessage()); } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "!include is not permitted"); } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RemoteContent.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RemoteContent.java deleted file mode 100644 index 929943296..000000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RemoteContent.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.structurizr.dsl; - -final class RemoteContent { - - static final String CONTENT_TYPE_JSON = "application/json"; - static final String TEXT_PLAIN_JSON = "text/plain"; - - private final String content; - private final String contentType; - - RemoteContent(String content, String contentType) { - this.content = content; - this.contentType = contentType; - } - - String getContent() { - return content; - } - - String getContentType() { - return contentType; - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 61e9503df..faf98ce36 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -2,7 +2,9 @@ import com.structurizr.PropertyHolder; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; import com.structurizr.model.*; +import com.structurizr.util.FeatureNotEnabledException; import com.structurizr.util.StringUtils; import com.structurizr.view.*; import org.apache.commons.logging.Log; @@ -46,7 +48,8 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private final Set parsedTokens = new HashSet<>(); private final IdentifiersRegister identifiersRegister; private Map constantsAndVariables; - private final Features features = new Features(); + private Features features = new Features(); + private HttpClient httpClient = new HttpClient(); private Map> archetypes = Map.of( StructurizrDslTokens.GROUP_TOKEN, new HashMap<>(), @@ -65,8 +68,6 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private Workspace workspace; private boolean extendingWorkspace = false; - private boolean restricted = false; - /** * Creates a new instance of the parser. */ @@ -74,6 +75,20 @@ public StructurizrDslParser() { contextStack = new Stack<>(); identifiersRegister = new IdentifiersRegister(); constantsAndVariables = new HashMap<>(); + + features.enable(Features.ENVIRONMENT); + features.enable(Features.FILE_SYSTEM); + features.enable(Features.HTTP); + features.enable(Features.HTTPS); + + features.enable(Features.PLUGINS); + features.enable(Features.SCRIPTS); + features.enable(Features.COMPONENT_FINDER); + + features.enable(Features.DOCUMENTATION); + features.enable(Features.DECISIONS); + + features.enable(Features.INCLUDE); } void configureFrom(StructurizrDslParser parser) { @@ -113,8 +128,17 @@ private void setIdentifierScope(IdentifierScope identifierScope) { * * @param restricted true for restricted mode, false otherwise */ + @Deprecated public void setRestricted(boolean restricted) { - this.restricted = restricted; + features.configure(Features.ENVIRONMENT, !restricted); + features.configure(Features.FILE_SYSTEM, !restricted); + + features.configure(Features.PLUGINS, !restricted); + features.configure(Features.SCRIPTS, !restricted); + features.configure(Features.COMPONENT_FINDER, !restricted); + + features.configure(Features.DOCUMENTATION, !restricted); + features.configure(Features.DECISIONS, !restricted); } /** @@ -269,28 +293,24 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn endContext(); } else if (INCLUDE_FILE_TOKEN.equalsIgnoreCase(firstToken)) { - if (!restricted || tokens.get(1).startsWith("https://") || tokens.get(1).startsWith("http://")) { - String leadingSpace = line.substring(0, line.indexOf(INCLUDE_FILE_TOKEN)); - - List files = new IncludeParser().parse(getContext(), dslFile, tokens); - for (IncludedFile includedFile : files) { - List paddedLines = new ArrayList<>(); - for (String unpaddedLine : includedFile.getLines()) { - if (unpaddedLine.startsWith(BOM)) { - // this caters for files encoded as "UTF-8 with BOM" - unpaddedLine = unpaddedLine.substring(1); - } - paddedLines.add(leadingSpace + unpaddedLine); + String leadingSpace = line.substring(0, line.indexOf(INCLUDE_FILE_TOKEN)); + + List files = new IncludeParser().parse(getContext(), dslFile, tokens); + for (IncludedFile includedFile : files) { + List paddedLines = new ArrayList<>(); + for (String unpaddedLine : includedFile.getLines()) { + if (unpaddedLine.startsWith(BOM)) { + // this caters for files encoded as "UTF-8 with BOM" + unpaddedLine = unpaddedLine.substring(1); } - - parse(paddedLines, includedFile.getFile(), true, false); + paddedLines.add(leadingSpace + unpaddedLine); } - } else { - throwRestrictedModeException(firstToken + " "); + + parse(paddedLines, includedFile.getFile(), true, false); } } else if (PLUGIN_TOKEN.equalsIgnoreCase(firstToken)) { - if (!restricted) { + if (features.isEnabled(Features.PLUGINS)) { String fullyQualifiedClassName = new PluginParser().parse(getContext(), tokens.withoutContextStartToken()); startContext(new PluginDslContext(fullyQualifiedClassName, dslFile, this)); if (!shouldStartContext(tokens)) { @@ -298,14 +318,14 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn endContext(); } } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.PLUGINS, firstToken + " is not permitted"); } } else if (inContext(PluginDslContext.class)) { new PluginParser().parseParameter(getContext(PluginDslContext.class), tokens); } else if (SCRIPT_TOKEN.equalsIgnoreCase(firstToken)) { - if (!restricted) { + if (features.isEnabled(Features.SCRIPTS)) { ScriptParser scriptParser = new ScriptParser(); if (scriptParser.isInlineScript(tokens)) { String language = scriptParser.parseInline(tokens.withoutContextStartToken()); @@ -321,7 +341,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } } } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.SCRIPTS, firstToken + " is not permitted"); } } else if (inContext(ExternalScriptDslContext.class)) { @@ -504,12 +524,12 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn registerIdentifier(identifier, component); } else if (COMPONENT_FINDER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.COMPONENT_FINDER)) { if (shouldStartContext(tokens)) { startContext(new ComponentFinderDslContext(this, getContext(ContainerDslContext.class))); } } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.COMPONENT_FINDER, firstToken + " is not permitted"); } } else if (COMPONENT_FINDER_CLASSES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { @@ -674,8 +694,10 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn if (parsedTokens.contains(WORKSPACE_TOKEN)) { throw new RuntimeException("Multiple workspaces are not permitted in a DSL definition"); } - DslParserContext dslParserContext = new DslParserContext(this, dslFile, restricted); + DslParserContext dslParserContext = new DslParserContext(this, dslFile); dslParserContext.setIdentifierRegister(identifiersRegister); + dslParserContext.setFeatures(features); + dslParserContext.setHttpClient(httpClient); workspace = new WorkspaceParser().parse(dslParserContext, tokens.withoutContextStartToken()); extendingWorkspace = !workspace.getModel().isEmpty(); @@ -685,7 +707,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn parsedTokens.add(WORKSPACE_TOKEN); } else if (IMPLIED_RELATIONSHIPS_TOKEN.equalsIgnoreCase(firstToken) || IMPLIED_RELATIONSHIPS_TOKEN.substring(1).equalsIgnoreCase(firstToken)) { - new ImpliedRelationshipsParser().parse(getContext(), tokens, dslFile, restricted); + new ImpliedRelationshipsParser().parse(getContext(), tokens, dslFile); } else if (NAME_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { new WorkspaceParser().parseName(getContext(), tokens); @@ -817,7 +839,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn startContext(new BrandingDslContext(dslFile)); } else if (BRANDING_LOGO_TOKEN.equalsIgnoreCase(firstToken) && inContext(BrandingDslContext.class)) { - new BrandingParser().parseLogo(getContext(BrandingDslContext.class), tokens, restricted); + new BrandingParser().parseLogo(getContext(BrandingDslContext.class), tokens); } else if (BRANDING_FONT_TOKEN.equalsIgnoreCase(firstToken) && inContext(BrandingDslContext.class)) { new BrandingParser().parseFont(getContext(BrandingDslContext.class), tokens); @@ -872,7 +894,7 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new ElementStyleParser().parseDescription(getContext(ElementStyleDslContext.class), tokens); } else if (ELEMENT_STYLE_ICON_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { - new ElementStyleParser().parseIcon(getContext(ElementStyleDslContext.class), tokens, restricted); + new ElementStyleParser().parseIcon(getContext(ElementStyleDslContext.class), tokens); } else if (ELEMENT_STYLE_ICON_POSITION_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) { new ElementStyleParser().parseIconPosition(getContext(ElementStyleDslContext.class), tokens); @@ -1088,16 +1110,16 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new ViewParser().parseDescription(getContext(ViewDslContext.class), tokens); } else if (PLANTUML_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { - new ImageViewContentParser(restricted).parsePlantUML(getContext(ImageViewDslContext.class), dslFile, tokens); + new ImageViewContentParser().parsePlantUML(getContext(ImageViewDslContext.class), dslFile, tokens); } else if (MERMAID_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { - new ImageViewContentParser(restricted).parseMermaid(getContext(ImageViewDslContext.class), dslFile, tokens); + new ImageViewContentParser().parseMermaid(getContext(ImageViewDslContext.class), dslFile, tokens); } else if (KROKI_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { - new ImageViewContentParser(restricted).parseKroki(getContext(ImageViewDslContext.class), dslFile, tokens); + new ImageViewContentParser().parseKroki(getContext(ImageViewDslContext.class), dslFile, tokens); } else if (IMAGE_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { - new ImageViewContentParser(restricted).parseImage(getContext(ImageViewDslContext.class), dslFile, tokens); + new ImageViewContentParser().parseImage(getContext(ImageViewDslContext.class), dslFile, tokens); } else if (LIGHT_COLOR_SCHEME_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class) && shouldStartContext(tokens)) { ImageViewDslContext context = getContext(ImageViewDslContext.class); @@ -1124,10 +1146,10 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new DynamicViewRelationshipParser().parseUrl(getContext(DynamicViewRelationshipContext.class), tokens.withoutContextStartToken()); } else if (THEME_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { - new ThemeParser().parseTheme(getContext(), dslFile, tokens, restricted); + new ThemeParser().parseTheme(getContext(), dslFile, tokens); } else if (THEMES_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { - new ThemeParser().parseThemes(getContext(), dslFile, tokens, restricted); + new ThemeParser().parseThemes(getContext(), dslFile, tokens); } else if (TERMINOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { startContext(new TerminologyDslContext()); @@ -1172,59 +1194,59 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new UserRoleParser().parse(getContext(), tokens); } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DOCUMENTATION)) { new DocsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); } } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DOCUMENTATION)) { new DocsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); } } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DOCUMENTATION)) { new DocsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); } } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DOCUMENTATION)) { new DocsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(WorkspaceDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DECISIONS)) { new DecisionsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(SoftwareSystemDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DECISIONS)) { new DecisionsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ContainerDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DECISIONS)) { new DecisionsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); } } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ComponentDslContext.class)) { - if (!restricted) { + if (features.isEnabled(Features.DECISIONS)) { new DecisionsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); } else { - throwRestrictedModeException(firstToken); + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); } } else if (CONSTANT_TOKEN.equalsIgnoreCase(firstToken)) { @@ -1360,10 +1382,6 @@ private List preProcessLines(List lines) { return dslLines; } - private void throwRestrictedModeException(String firstToken) { - throw new RuntimeException(firstToken + " is not available when the parser is running in restricted mode"); - } - private String substituteStrings(String token) { Matcher m = STRING_SUBSTITUTION_PATTERN.matcher(token); while (m.find()) { @@ -1379,7 +1397,7 @@ private String substituteStrings(String token) { after = nameValuePair.getValue(); } } else { - if (!restricted) { + if (getFeatures().isEnabled(Features.ENVIRONMENT)) { String environmentVariable = System.getenv().get(name); if (environmentVariable != null) { after = environmentVariable; @@ -1403,6 +1421,8 @@ private void startContext(DslContext context) { context.setWorkspace(workspace); context.setIdentifierRegister(identifiersRegister); context.setExtendingWorkspace(extendingWorkspace); + context.setFeatures(features); + context.setHttpClient(httpClient); contextStack.push(context); } @@ -1445,6 +1465,18 @@ public Features getFeatures() { return features; } + void setFeatures(Features features) { + this.features = features; + } + + public HttpClient getHttpClient() { + return httpClient; + } + + void setHttpClient(HttpClient httpClient) { + this.httpClient = httpClient; + } + private boolean isElementKeywordOrArchetype(String token, String keyword) { if (token.equalsIgnoreCase(keyword)) { return true; diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java index d228972ca..90b5e3e73 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.util.FeatureNotEnabledException; import com.structurizr.util.Url; import com.structurizr.view.ThemeUtils; @@ -12,7 +13,7 @@ final class ThemeParser extends AbstractParser { private final static int FIRST_THEME_INDEX = 1; - void parseTheme(DslContext context, File dslFile, Tokens tokens, boolean restricted) { + void parseTheme(DslContext context, File dslFile, Tokens tokens) { // theme if (tokens.hasMoreThan(FIRST_THEME_INDEX)) { throw new RuntimeException("Too many tokens, expected: theme "); @@ -22,21 +23,21 @@ void parseTheme(DslContext context, File dslFile, Tokens tokens, boolean restric throw new RuntimeException("Expected: theme "); } - addTheme(context, dslFile, tokens.get(FIRST_THEME_INDEX), restricted); + addTheme(context, dslFile, tokens.get(FIRST_THEME_INDEX)); } - void parseThemes(DslContext context, File dslFile, Tokens tokens, boolean restricted) { + void parseThemes(DslContext context, File dslFile, Tokens tokens) { // themes [url|file] ... [url|file] if (!tokens.includes(FIRST_THEME_INDEX)) { throw new RuntimeException("Expected: themes [url|file] ... [url|file]"); } for (int i = FIRST_THEME_INDEX; i < tokens.size(); i++) { - addTheme(context, dslFile, tokens.get(i), restricted); + addTheme(context, dslFile, tokens.get(i)); } } - private void addTheme(DslContext context, File dslFile, String theme, boolean restricted) { + private void addTheme(DslContext context, File dslFile, String theme) { if (DEFAULT_THEME_NAME.equalsIgnoreCase(theme)) { theme = DEFAULT_THEME_URL; } @@ -45,7 +46,7 @@ private void addTheme(DslContext context, File dslFile, String theme, boolean re // this adds the theme to the list of theme URLs in the workspace context.getWorkspace().getViews().getConfiguration().addTheme(theme); } else { - if (!restricted) { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { context.setDslPortable(false); // this inlines the file-based theme into the workspace @@ -64,7 +65,7 @@ private void addTheme(DslContext context, File dslFile, String theme, boolean re throw new RuntimeException(file.getAbsolutePath() + " does not exist"); } } else { - throw new RuntimeException("File-based themes are not supported when the DSL parser is running in restricted mode"); + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "File-based themes are not permitted"); } } } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java index e116de60a..59ebc4a54 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java @@ -1,9 +1,12 @@ package com.structurizr.dsl; import com.structurizr.Workspace; +import com.structurizr.http.RemoteContent; import com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy; import com.structurizr.model.Element; import com.structurizr.model.Relationship; +import com.structurizr.util.FeatureNotEnabledException; +import com.structurizr.util.Url; import com.structurizr.util.WorkspaceUtils; import java.io.File; @@ -36,24 +39,32 @@ Workspace parse(DslParserContext context, Tokens tokens) { String source = tokens.get(SECOND_INDEX); try { - if (source.startsWith("https://") || source.startsWith("http://")) { - RemoteContent content = readFromUrl(source); + if (Url.isHttpsUrl(source) || Url.isHttpUrl(source)) { + if (Url.isHttpsUrl(source) && !context.getFeatures().isEnabled(Features.HTTPS)) { + throw new FeatureNotEnabledException(Features.HTTPS, "Extends via HTTPS are not permitted"); + } + if (Url.isHttpUrl(source) && !context.getFeatures().isEnabled(Features.HTTP)) { + throw new FeatureNotEnabledException(Features.HTTP, "Extends via HTTP are not permitted"); + } + + RemoteContent remoteContent = context.getHttpClient().get(source); - if (source.endsWith(".json") || content.getContentType().startsWith(RemoteContent.CONTENT_TYPE_JSON)) { - String json = content.getContent(); + if (source.toLowerCase().endsWith(".json") || remoteContent.getContentType().startsWith(RemoteContent.CONTENT_TYPE_JSON)) { + String json = remoteContent.getContentAsString(); workspace = WorkspaceUtils.fromJson(json); registerIdentifiers(workspace, context); } else { - String dsl = content.getContent(); - StructurizrDslParser structurizrDslParser = new StructurizrDslParser(); - structurizrDslParser.setRestricted(context.isRestricted()); + String dsl = remoteContent.getContentAsString(); + + StructurizrDslParser structurizrDslParser = createParser(context); structurizrDslParser.parse(context, dsl); + workspace = structurizrDslParser.getWorkspace(); context.getParser().configureFrom(structurizrDslParser); } } else { - if (context.isRestricted()) { - throw new RuntimeException("Cannot import workspace from a file when running in restricted mode"); + if (!context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "Extending a file-based workspace is not permitted"); } if (context.getFile() != null) { @@ -66,12 +77,13 @@ Workspace parse(DslParserContext context, Tokens tokens) { throw new RuntimeException(file.getCanonicalPath() + " should be a single file"); } - if (source.endsWith(".json")) { + if (source.toLowerCase().endsWith(".json")) { workspace = WorkspaceUtils.loadWorkspaceFromJson(file); registerIdentifiers(workspace, context); } else { - StructurizrDslParser structurizrDslParser = new StructurizrDslParser(); + StructurizrDslParser structurizrDslParser = createParser(context); structurizrDslParser.parse(context, file); + workspace = structurizrDslParser.getWorkspace(); context.getParser().configureFrom(structurizrDslParser); } @@ -100,6 +112,14 @@ Workspace parse(DslParserContext context, Tokens tokens) { return workspace; } + private StructurizrDslParser createParser(DslParserContext context) { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.setFeatures(context.getFeatures()); + parser.setHttpClient(context.getHttpClient()); + + return parser; + } + private void registerIdentifiers(Workspace workspace, DslParserContext context) { for (Element element : workspace.getModel().getElements()) { if (element.getProperties().containsKey(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java index dec579440..1bf47b5a1 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java @@ -15,7 +15,7 @@ void test_parseLogo_ThrowsAnException_WhenThereAreTooManyTokens() { BrandingDslContext context = new BrandingDslContext(null); try { - parser.parseLogo(context, tokens("logo", "path", "extra"), false); + parser.parseLogo(context, tokens("logo", "path", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: logo ", e.getMessage()); @@ -27,7 +27,7 @@ void test_parseLogo_ThrowsAnException_WhenNoPathIsSpecified() { BrandingDslContext context = new BrandingDslContext(null); try { - parser.parseLogo(context, tokens("logo"), false); + parser.parseLogo(context, tokens("logo")); fail(); } catch (Exception e) { assertEquals("Expected: logo ", e.getMessage()); @@ -37,9 +37,10 @@ void test_parseLogo_ThrowsAnException_WhenNoPathIsSpecified() { @Test void test_parseLogo_ThrowsAnException_WhenTheLogoDoesNotExist() { BrandingDslContext context = new BrandingDslContext(new File(".")); + context.getFeatures().enable(Features.FILE_SYSTEM); try { - parser.parseLogo(context, tokens("logo", "hello.png"), false); + parser.parseLogo(context, tokens("logo", "hello.png")); fail(); } catch (Exception e) { assertEquals("hello.png does not exist", e.getMessage()); @@ -49,9 +50,10 @@ void test_parseLogo_ThrowsAnException_WhenTheLogoDoesNotExist() { @Test void test_parseLogo_ThrowsAnException_WhenTheFileIsNotSupported() { BrandingDslContext context = new BrandingDslContext(new File(".")); + context.getFeatures().enable(Features.FILE_SYSTEM); try { - parser.parseLogo(context, tokens("logo", "src/test/resources/dsl/getting-started.dsl"), false); + parser.parseLogo(context, tokens("logo", "src/test/resources/dsl/getting-started.dsl")); fail(); } catch (Exception e) { e.printStackTrace(); @@ -63,8 +65,9 @@ void test_parseLogo_ThrowsAnException_WhenTheFileIsNotSupported() { void test_parseLogo_SetsTheLogo_WhenTheLogoDoesExist() { BrandingDslContext context = new BrandingDslContext(new File(".")); context.setWorkspace(workspace); + context.getFeatures().enable(Features.FILE_SYSTEM); - parser.parseLogo(context, tokens("logo", "src/test/resources/dsl/logo.png"), false); + parser.parseLogo(context, tokens("logo", "src/test/resources/dsl/logo.png")); assertTrue(workspace.getViews().getConfiguration().getBranding().getLogo().startsWith("data:image/png;base64,")); } @@ -73,25 +76,55 @@ void test_parseLogo_SetsTheLogoFromADataUri() { BrandingDslContext context = new BrandingDslContext(new File(".")); context.setWorkspace(workspace); - parser.parseLogo(context, tokens("logo", "data:image/png;base64,123456789012345678901234567890"), true); + parser.parseLogo(context, tokens("logo", "data:image/png;base64,123456789012345678901234567890")); assertTrue(workspace.getViews().getConfiguration().getBranding().getLogo().startsWith("data:image/png;base64,123456789012345678901234567890")); } + @Test + void test_parseLogo_ThrowsAnException_WithAHttpIconAndHttpIsNotEnabled() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTP); + + try { + parser.parseLogo(context, tokens("logo", "http://structurizr.com/logo.png")); + fail(); + } catch (Exception e) { + assertEquals("Icons via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); + } + } + @Test void test_parseLogo_SetsTheLogoFromAHttpUrl() { BrandingDslContext context = new BrandingDslContext(new File(".")); context.setWorkspace(workspace); + context.getFeatures().enable(Features.HTTP); - parser.parseLogo(context, tokens("logo", "http://structurizr.com/logo.png"), true); + parser.parseLogo(context, tokens("logo", "http://structurizr.com/logo.png")); assertEquals("http://structurizr.com/logo.png", workspace.getViews().getConfiguration().getBranding().getLogo()); } + @Test + void test_parseLogo_ThrowsAnException_WithAHttpsIconAndHttpsIsNotEnabled() { + BrandingDslContext context = new BrandingDslContext(new File(".")); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTPS); + + try { + parser.parseLogo(context, tokens("logo", "https://structurizr.com/logo.png")); + fail(); + } catch (Exception e) { + assertEquals("Icons via HTTPS are not permitted (feature structurizr.feature.dsl.https is not enabled)", e.getMessage()); + } + } + @Test void test_parseLogo_SetsTheLogoFromAHttpsUrl() { BrandingDslContext context = new BrandingDslContext(new File(".")); context.setWorkspace(workspace); + context.getFeatures().enable(Features.HTTPS); - parser.parseLogo(context, tokens("logo", "https://structurizr.com/logo.png"), true); + parser.parseLogo(context, tokens("logo", "https://structurizr.com/logo.png")); assertEquals("https://structurizr.com/logo.png", workspace.getViews().getConfiguration().getBranding().getLogo()); } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index f45817bd5..fd3284149 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -4,7 +4,6 @@ import com.structurizr.documentation.Section; import com.structurizr.model.*; import com.structurizr.util.StringUtils; -import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -23,6 +22,7 @@ class DslTests extends AbstractTests { @Test void test_test() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); + parser.getFeatures().configure(Features.FILE_SYSTEM, true); parser.parse(new File("src/test/resources/dsl/test.dsl")); assertFalse(parser.getWorkspace().isEmpty()); @@ -374,6 +374,7 @@ void test_includeLocalDirectory_WhenThereAreHiddenFiles() throws Exception { @Tag("IntegrationTest") void test_includeUrl() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); + parser.getHttpClient().allow(".*"); parser.parse(new File("src/test/resources/dsl/include-url.dsl")); Workspace workspace = parser.getWorkspace(); @@ -392,21 +393,6 @@ void test_includeUrl() throws Exception { }""", DslUtils.getDsl(workspace)); } - @Test - void test_includeLocalFile_ThrowsAnException_WhenRunningInRestrictedMode() { - try { - StructurizrDslParser parser = new StructurizrDslParser(); - parser.setRestricted(true); - - // the model include will be ignored, so no software systems - parser.parse(new File("src/test/resources/dsl/include-file.dsl")); - fail(); - } catch (Exception e) { - System.out.println(e.getMessage()); - assertTrue(e.getMessage().startsWith("!include is not available when the parser is running in restricted mode")); - } - } - @Test @Tag("IntegrationTest") void test_extendWorkspaceFromJsonFile() throws Exception { @@ -437,6 +423,7 @@ void test_extendWorkspaceFromJsonFile() throws Exception { void test_extendWorkspaceFromJsonUrl() throws Exception { String dslFile = "src/test/resources/dsl/extend/extend-workspace-from-json-url.dsl"; StructurizrDslParser parser = new StructurizrDslParser(); + parser.getHttpClient().allow(".*"); parser.parse(new File(dslFile)); Workspace workspace = parser.getWorkspace(); @@ -487,7 +474,7 @@ void test_extendWorkspaceFromJsonFile_WhenRunningInRestrictedMode() throws Excep parser.parse(dslFile); fail(); } catch (StructurizrDslParserException e) { - assertEquals("Cannot import workspace from a file when running in restricted mode at line 1 of " + dslFile.getAbsolutePath() + ": workspace extends workspace.json {", e.getMessage()); + assertEquals("Extending a file-based workspace is not permitted (feature structurizr.feature.dsl.filesystem is not enabled) at line 1 of " + dslFile.getAbsolutePath() + ": workspace extends workspace.json {", e.getMessage()); } } @@ -496,6 +483,7 @@ void test_extendWorkspaceFromJsonFile_WhenRunningInRestrictedMode() throws Excep @ValueSource(strings = { "src/test/resources/dsl/extend/extend-workspace-from-dsl-file.dsl", "src/test/resources/dsl/extend/extend-workspace-from-dsl-url.dsl" }) void test_extendWorkspaceFromDsl(String dslFile) throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); + parser.getHttpClient().allow(".*"); parser.parse(new File(dslFile)); Workspace workspace = parser.getWorkspace(); @@ -526,7 +514,7 @@ void test_extendWorkspaceFromDslFile_WhenRunningInRestrictedMode() throws Except parser.parse(dslFile); fail(); } catch (StructurizrDslParserException e) { - assertEquals("Cannot import workspace from a file when running in restricted mode at line 1 of " + dslFile.getAbsolutePath() +": workspace extends workspace.dsl {", e.getMessage()); + assertEquals("Extending a file-based workspace is not permitted (feature structurizr.feature.dsl.filesystem is not enabled) at line 1 of " + dslFile.getAbsolutePath() +": workspace extends workspace.dsl {", e.getMessage()); } } @@ -725,15 +713,15 @@ void test_hierarchicalIdentifiersAndDeploymentNodes_WhenSoftwareContainerClashes } @Test - void test_plugin_ThrowsAnException_WhenTheParserIsRunningInRestrictedMode() { + void test_plugin_ThrowsAnException_WhenPluginsAreNotEnabled() { try { StructurizrDslParser parser = new StructurizrDslParser(); - parser.setRestricted(true); + parser.getFeatures().disable(Features.PLUGINS); parser.parse(new File("src/test/resources/dsl/plugin-without-parameters.dsl")); fail(); } catch (Exception e) { System.out.println(e.getMessage()); - assertTrue(e.getMessage().startsWith("!plugin is not available when the parser is running in restricted mode")); + assertTrue(e.getMessage().startsWith("!plugin is not permitted (feature structurizr.feature.dsl.plugins is not enabled)")); } } @@ -760,14 +748,15 @@ void test_pluginWithParameters() throws Exception { } @Test - void test_script_ThrowsAnException_WhenTheParserIsInRestrictedMode() { + void test_script_ThrowsAnException_WhenScriptsAreNotEnabled() { try { StructurizrDslParser parser = new StructurizrDslParser(); - parser.setRestricted(true); + parser.getFeatures().disable(Features.SCRIPTS); parser.parse(new File("src/test/resources/dsl/script-external.dsl")); fail(); } catch (Exception e) { - assertTrue(e.getMessage().startsWith("!script is not available when the parser is running in restricted mode")); + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!script is not permitted (feature structurizr.feature.dsl.scripts is not enabled)")); } } @@ -863,7 +852,8 @@ void test_docs_ThrowsAnException_WhenTheParserIsInRestrictedMode() { parser.parse(new File("src/test/resources/dsl/docs/workspace.dsl")); fail(); } catch (Exception e) { - assertTrue(e.getMessage().startsWith("!docs is not available when the parser is running in restricted mode")); + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!docs is not permitted (feature structurizr.feature.dsl.documentation is not enabled)")); } } @@ -898,7 +888,8 @@ void test_decisions_ThrowsAnException_WhenTheParserIsInRestrictedMode() { parser.parse(new File("src/test/resources/dsl/decisions/workspace.dsl")); fail(); } catch (Exception e) { - assertTrue(e.getMessage().startsWith("!adrs is not available when the parser is running in restricted mode")); + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!adrs is not permitted (feature structurizr.feature.dsl.decisions is not enabled)")); } } @@ -1101,6 +1092,7 @@ void test_relationshipWithoutIdentifier() throws Exception { @Test void test_imageViews_ViaFiles() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); + parser.getFeatures().configure(Features.FILE_SYSTEM, true); parser.parse(new File("src/test/resources/dsl/image-views/workspace-via-file.dsl")); Workspace workspace = parser.getWorkspace(); @@ -1139,6 +1131,7 @@ void test_imageViews_ViaFiles() throws Exception { @Tag("IntegrationTest") void test_imageViews_ViaUrls() throws Exception { StructurizrDslParser parser = new StructurizrDslParser(); + parser.getHttpClient().allow(".*"); parser.parse(new File("src/test/resources/dsl/image-views/workspace-via-url.dsl")); Workspace workspace = parser.getWorkspace(); @@ -1534,7 +1527,7 @@ void test_ImageView_WhenParserIsInRestrictedMode() { parser.parse(dslFile); fail(); } catch (StructurizrDslParserException e) { - assertEquals("Images must be specified as a URL when running in restricted mode at line 5 of " + dslFile.getAbsolutePath() + ": image image.png", e.getMessage()); + assertEquals("image is not permitted (feature structurizr.feature.dsl.filesystem is not enabled) at line 5 of " + dslFile.getAbsolutePath() + ": image image.png", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java index 83e72cb77..65baa3c09 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -12,7 +12,7 @@ class ElementStyleParserTests extends AbstractTests { - private ElementStyleParser parser = new ElementStyleParser(); + private final ElementStyleParser parser = new ElementStyleParser(); private ElementStyle elementStyle; private ElementStyleDslContext elementStyleDslContext() { @@ -499,7 +499,7 @@ void test_parseDescription_SetsTheDescription() { @Test void test_parseIcon_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parseIcon(elementStyleDslContext(), tokens("icon", "file", "extra"), false); + parser.parseIcon(elementStyleDslContext(), tokens("icon", "file", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: icon ", e.getMessage()); @@ -509,7 +509,7 @@ void test_parseIcon_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseIcon_ThrowsAnException_WhenTheIconIsMissing() { try { - parser.parseIcon(elementStyleDslContext(), tokens("icon"), false); + parser.parseIcon(elementStyleDslContext(), tokens("icon")); fail(); } catch (Exception e) { assertEquals("Expected: icon ", e.getMessage()); @@ -519,7 +519,10 @@ void test_parseIcon_ThrowsAnException_WhenTheIconIsMissing() { @Test void test_parseIcon_ThrowsAnException_WhenTheIconDoesNotExist() { try { - parser.parseIcon(elementStyleDslContext(), tokens("icon", "hello.png"), false); + ElementStyleDslContext context = elementStyleDslContext(); + context.getFeatures().enable(Features.FILE_SYSTEM); + + parser.parseIcon(context, tokens("icon", "hello.png")); fail(); } catch (Exception e) { assertEquals("hello.png does not exist", e.getMessage()); @@ -528,29 +531,77 @@ void test_parseIcon_ThrowsAnException_WhenTheIconDoesNotExist() { @Test void test_parseIcon_SetsTheIconFromADataUri() { - parser.parseIcon(elementStyleDslContext(), tokens("icon", "data:image/png;base64,123456789012345678901234567890"), true); + parser.parseIcon(elementStyleDslContext(), tokens("icon", "data:image/png;base64,123456789012345678901234567890")); assertTrue(elementStyle.getIcon().startsWith("data:image/png;base64,123456789012345678901234567890")); } + @Test + void test_parseIcon_ThrowsAnException_WithAHttpIconAndHttpIsNotEnabled() { + try { + ElementStyleDslContext context = elementStyleDslContext(); + context.getFeatures().disable(Features.HTTP); + + parser.parseIcon(context, tokens("icon", "http://structurizr.com/logo.png")); + fail(); + } catch (Exception e) { + assertEquals("Icons via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); + } + } + @Test void test_parseIcon_SetsTheIconFromAHttpUrl() { - parser.parseIcon(elementStyleDslContext(), tokens("icon", "http://structurizr.com/logo.png"), true); + ElementStyleDslContext context = elementStyleDslContext(); + context.getFeatures().enable(Features.HTTP); + + parser.parseIcon(context, tokens("icon", "http://structurizr.com/logo.png")); assertEquals("http://structurizr.com/logo.png", elementStyle.getIcon()); } + @Test + void test_parseIcon_ThrowsAnException_WithAHttpsIconAndHttpsIsNotEnabled() { + try { + ElementStyleDslContext context = elementStyleDslContext(); + context.getFeatures().disable(Features.HTTPS); + + parser.parseIcon(context, tokens("icon", "https://structurizr.com/logo.png")); + fail(); + } catch (Exception e) { + assertEquals("Icons via HTTPS are not permitted (feature structurizr.feature.dsl.https is not enabled)", e.getMessage()); + } + } + @Test void test_parseIcon_SetsTheIconFromAHttpsUrl() { - parser.parseIcon(elementStyleDslContext(), tokens("icon", "https://structurizr.com/logo.png"), true); + ElementStyleDslContext context = elementStyleDslContext(); + context.getFeatures().enable(Features.HTTPS); + + parser.parseIcon(context, tokens("icon", "https://structurizr.com/logo.png")); assertEquals("https://structurizr.com/logo.png", elementStyle.getIcon()); } @Test void test_parseIcon_SetsTheIconFromAFile() { - parser.parseIcon(elementStyleDslContext(), tokens("icon", "src/test/resources/dsl/logo.png"), false); + ElementStyleDslContext context = elementStyleDslContext(); + context.getFeatures().enable(Features.FILE_SYSTEM); + + parser.parseIcon(context, tokens("icon", "src/test/resources/dsl/logo.png")); System.out.println(elementStyle.getIcon()); assertTrue(elementStyle.getIcon().startsWith("data:image/png;base64,")); } + @Test + void test_parseIcon_ThrowsAnException_WhenFileSystemAccessIsNotEnabled() { + try { + ElementStyleDslContext context = elementStyleDslContext(); + context.getFeatures().disable(Features.FILE_SYSTEM); + + parser.parseIcon(context, tokens("icon", "src/test/resources/dsl/logo.png")); + fail(); + } catch (Exception e) { + assertEquals("!icon is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + @Test void test_parseIconPosition_ThrowsAnException_WhenThereAreTooManyTokens() { try { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index 28b826974..90c274bc6 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -25,7 +25,7 @@ void test_parsePlantUML_ThrowsAnException_WithTooFewTokens() { try { ImageViewDslContext context = new ImageViewDslContext(imageView); context.setWorkspace(workspace); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parsePlantUML(context, null, tokens("plantuml")); fail(); } catch (Exception e) { @@ -38,11 +38,39 @@ void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { ImageViewDslContext context = new ImageViewDslContext(imageView); context.setWorkspace(workspace); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parsePlantUML(context, null, tokens("plantuml", "image.puml")); fail(); } catch (Exception e) { - assertEquals("PlantUML source must be specified as a URL when running in restricted mode", e.getMessage()); + assertEquals("plantuml is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + + @Test + void test_parsePlantUML_ThrowsAnException_WhenUsingAHttpsUrlAndHttpsIsNotEnabled() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTPS); + parser = new ImageViewContentParser(); + parser.parsePlantUML(context, null, tokens("plantuml", "https://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Image views via HTTPS are not permitted (feature structurizr.feature.dsl.https is not enabled)", e.getMessage()); + } + } + + @Test + void test_parsePlantUML_ThrowsAnException_WhenUsingAHttpUrlAndHttpIsNotEnabled() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTP); + parser = new ImageViewContentParser(); + parser.parsePlantUML(context, null, tokens("plantuml", "http://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Image views via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); } } @@ -54,7 +82,7 @@ void test_parsePlantUML_Source() { @enduml"""; workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parsePlantUML(new ImageViewDslContext(imageView), null, tokens("plantuml", source)); assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", imageView.getContent()); } @@ -67,7 +95,7 @@ void test_parsePlantUML_Source_Light() { @enduml"""; workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Light); parser.parsePlantUML(context, null, tokens("plantuml", source)); @@ -82,7 +110,7 @@ void test_parsePlantUML_Source_Dark() { @enduml"""; workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Dark); parser.parsePlantUML(context, null, tokens("plantuml", source)); @@ -97,7 +125,7 @@ void test_parsePlantUML_WithViewKey() { workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements(); workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape")); assertEquals("System Landscape View", imageView.getTitle()); assertEquals("Description", imageView.getDescription()); @@ -111,7 +139,7 @@ void test_parseMermaid_ThrowsAnException_WithTooFewTokens() { try { ImageViewDslContext context = new ImageViewDslContext(imageView); context.setWorkspace(workspace); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parseMermaid(context, null, tokens("mermaid")); fail(); } catch (Exception e) { @@ -124,11 +152,39 @@ void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { ImageViewDslContext context = new ImageViewDslContext(imageView); context.setWorkspace(workspace); - parser = new ImageViewContentParser(true); - parser.parseMermaid(context, null, tokens("mermaid", "image.puml")); + parser = new ImageViewContentParser(); + parser.parseMermaid(context, null, tokens("mermaid", "image.mmd")); + fail(); + } catch (Exception e) { + assertEquals("mermaid is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + + @Test + void test_parseMermaid_ThrowsAnException_WhenUsingAHttpsUrlAndHttpsIsNotEnabled() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTPS); + parser = new ImageViewContentParser(); + parser.parseMermaid(context, null, tokens("mermaid", "https://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Image views via HTTPS are not permitted (feature structurizr.feature.dsl.https is not enabled)", e.getMessage()); + } + } + + @Test + void test_parseMermaid_ThrowsAnException_WhenUsingAHttpUrlAndHttpIsNotEnabled() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTP); + parser = new ImageViewContentParser(); + parser.parseMermaid(context, null, tokens("mermaid", "http://example.com")); fail(); } catch (Exception e) { - assertEquals("Mermaid source must be specified as a URL when running in restricted mode", e.getMessage()); + assertEquals("Image views via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); } } @@ -143,7 +199,7 @@ void test_parseMermaid_Source() { C -->|Three| F[fa:fa-car Car]"""; workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parseMermaid(new ImageViewDslContext(imageView), null, tokens("mermaid", source)); assertEquals("https://mermaid.ink/svg/pako:eJxVj70OgjAUhV_lppMm8gIMJlKUhUQHtspwAxfbSH9SaoihvLsgi571-85JzgSssS2xlHW9HRuJPkCV3w0sOQkuvRqCxqGGJDnGggJoa-gdIdsVFgZpnVPmsd_8bJWAT-WqEQSpzHPeEP_2r4Yi5KJEF6yrf0k12ghnoW5ymf8n0tPSuogO0w6TBj1w9DU7ANPkNaqWpRMLkvR6oqUOX31g8_wBLY9E1w==", imageView.getContent()); } @@ -159,7 +215,7 @@ void test_parseMermaid_Source_Light() { C -->|Three| F[fa:fa-car Car]"""; workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Light); parser.parseMermaid(context, null, tokens("mermaid", source)); @@ -177,7 +233,7 @@ void test_parseMermaid_Source_Dark() { C -->|Three| F[fa:fa-car Car]"""; workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Dark); parser.parseMermaid(context, null, tokens("mermaid", source)); @@ -192,7 +248,7 @@ void test_parseMermaid_WithViewKey() { workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements(); workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parseMermaid(context, null, tokens("mermaid", "SystemLandscape")); assertEquals("System Landscape View", imageView.getTitle()); assertEquals("Description", imageView.getDescription()); @@ -204,7 +260,7 @@ void test_parseKroki_ThrowsAnException_WithTooFewTokens() { try { ImageViewDslContext context = new ImageViewDslContext(imageView); context.setWorkspace(workspace); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parseKroki(context, null, tokens("kroki")); fail(); } catch (Exception e) { @@ -215,11 +271,39 @@ void test_parseKroki_ThrowsAnException_WithTooFewTokens() { @Test void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parseKroki(new ImageViewDslContext(imageView), null, tokens("kroki", "plantuml", "image.puml")); fail(); } catch (Exception e) { - assertEquals("Kroki source must be specified as a URL when running in restricted mode", e.getMessage()); + assertEquals("kroki plantuml is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + + @Test + void test_parseKroki_ThrowsAnException_WhenUsingAHttpsUrlAndHttpsIsNotEnabled() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTPS); + parser = new ImageViewContentParser(); + parser.parseKroki(context, null, tokens("kroki", "plantuml", "https://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Image views via HTTPS are not permitted (feature structurizr.feature.dsl.https is not enabled)", e.getMessage()); + } + } + + @Test + void test_parseKroki_ThrowsAnException_WhenUsingAHttpUrlAndHttpIsNotEnabled() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + context.getFeatures().disable(Features.HTTP); + parser = new ImageViewContentParser(); + parser.parseKroki(context, null, tokens("kroki", "plantuml", "http://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Image views via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); } } @@ -234,7 +318,7 @@ void test_parseKroki_Source() { C -->|Three| F[fa:fa-car Car]"""; workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parseKroki(new ImageViewDslContext(imageView), null, tokens("kroki", "mermaid", source)); assertEquals("https://kroki.io/mermaid/png/eNpVjLEOwiAURXe_4o068AMOJpZqlyZ16EYYXhrwES2PAEljxH-XdtK7nnOuffIyEcYMY7uDurOSFF3KMyYNQpxKZzLM7M2rQLPvGBJxCM7fD5verA7Id79aBjI5__hsRG714E2BVvUYMgf9A8aFC1yUu1H9_gMUTW2uyuLRopgwgsSovzbHM0c=", imageView.getContent()); } @@ -250,7 +334,7 @@ void test_parseKroki_Source_Light() { C -->|Three| F[fa:fa-car Car]"""; workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Light); parser.parseKroki(context, null, tokens("kroki", "mermaid", source)); @@ -268,7 +352,7 @@ void test_parseKroki_Source_Dark() { C -->|Three| F[fa:fa-car Car]"""; workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Dark); parser.parseKroki(context, null, tokens("kroki", "mermaid", source)); @@ -278,37 +362,47 @@ void test_parseKroki_Source_Dark() { @Test void test_parseImage_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { try { - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); parser.parseImage(new ImageViewDslContext(imageView), null, tokens("image", "image.png")); fail(); } catch (Exception e) { - assertEquals("Images must be specified as a URL when running in restricted mode", e.getMessage()); + assertEquals("image is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); } } @Test void test_parseImage() { - parser = new ImageViewContentParser(true); - parser.parseImage(new ImageViewDslContext(imageView), null, tokens("image", "https://example.com/image.png")); - assertEquals("https://example.com/image.png", imageView.getContent()); + parser = new ImageViewContentParser(); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.getFeatures().enable(Features.HTTPS); + context.getHttpClient().allow(".*"); + + parser.parseImage(context, null, tokens("image", "https://static.structurizr.com/img/structurizr-banner.png")); + assertEquals("https://static.structurizr.com/img/structurizr-banner.png", imageView.getContent()); } @Test void test_parseImage_Url_Light() { - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Light); - parser.parseImage(context, null, tokens("image", "https://example.com/image.png")); - assertEquals("https://example.com/image.png", imageView.getContentLight()); + context.getFeatures().enable(Features.HTTPS); + context.getHttpClient().allow(".*"); + + parser.parseImage(context, null, tokens("image", "https://static.structurizr.com/img/structurizr-banner.png")); + assertEquals("https://static.structurizr.com/img/structurizr-banner.png", imageView.getContentLight()); } @Test void test_parseImage_Url_Dark() { - parser = new ImageViewContentParser(true); + parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); context.setColorScheme(ColorScheme.Dark); - parser.parseImage(context, null, tokens("image", "https://example.com/image.png")); - assertEquals("https://example.com/image.png", imageView.getContentDark()); + context.getFeatures().enable(Features.HTTPS); + context.getHttpClient().allow(".*"); + + parser.parseImage(context, null, tokens("image", "https://static.structurizr.com/img/structurizr-banner.png")); + assertEquals("https://static.structurizr.com/img/structurizr-banner.png", imageView.getContentDark()); } } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java index eae9a875c..04d17f55a 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java @@ -9,12 +9,12 @@ class ImpliedRelationshipsParserTests extends AbstractTests { - private ImpliedRelationshipsParser parser = new ImpliedRelationshipsParser(); + private final ImpliedRelationshipsParser parser = new ImpliedRelationshipsParser(); @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(), tokens("!impliedRelationships", "boolean", "extra"), null, false); + parser.parse(context(), tokens("!impliedRelationships", "boolean", "extra"), null); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: !impliedRelationships ", e.getMessage()); @@ -24,7 +24,7 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenNoFlagIsSpecified() { try { - parser.parse(context(), tokens("!impliedRelationships"), null, false); + parser.parse(context(), tokens("!impliedRelationships"), null); fail(); } catch (Exception e) { assertEquals("Expected: !impliedRelationships ", e.getMessage()); @@ -33,46 +33,52 @@ void test_parse_ThrowsAnException_WhenNoFlagIsSpecified() { @Test void test_parse_SetsTheStrategy_WhenFalseIsSpecified() { - parser.parse(context(), tokens("!impliedRelationships", "false"), null, false); + parser.parse(context(), tokens("!impliedRelationships", "false"), null); assertEquals("com.structurizr.model.DefaultImpliedRelationshipsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); } @Test void test_parse_SetsTheStrategy_WhenTrueIsSpecified() { - parser.parse(context(), tokens("!impliedRelationships", "true"), null, false); + parser.parse(context(), tokens("!impliedRelationships", "true"), null); assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); } @Test void test_parse_SetsTheStrategy_WhenABuiltInStrategyIsUsedInUnrestrictedMode() { - parser.parse(context(), tokens("!impliedRelationships", "com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy"), new File("."), false); + parser.parse(context(), tokens("!impliedRelationships", "com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy"), new File(".")); assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); } @Test - void test_parse_SetsTheStrategy_WhenABuiltInStrategyIsUsedInRestrictedMode() { - parser.parse(context(), tokens("!impliedRelationships", "com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy"), new File("."), true); + void test_parse_SetsTheStrategy_WhenABuiltInStrategyIsUsedAndCustomStrategiesAreNotEnabled() { + DslContext context = context(); + context.getFeatures().disable(Features.PLUGINS); + parser.parse(context, tokens("!impliedRelationships", "com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy"), new File(".")); assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); } @Test - void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedInRestrictedMode() { + void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedAndCustomStrategiesAreNotEnabled() { try { - parser.parse(context(), tokens("!impliedRelationships", "com.example.CustomImpliedRelationshipsStrategy"), new File("."), true); + DslContext context = context(); + context.getFeatures().disable(Features.PLUGINS); + parser.parse(context, tokens("!impliedRelationships", "com.example.CustomImpliedRelationshipsStrategy"), new File(".")); fail(); } catch (Exception e) { - assertEquals("The implied relationships strategy com.example.CustomImpliedRelationshipsStrategy is not available when the DSL parser is running in restricted mode", e.getMessage()); + assertEquals("The implied relationships strategy com.example.CustomImpliedRelationshipsStrategy is not available (feature structurizr.feature.dsl.plugins is not enabled)", e.getMessage()); } } @Test - void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedInUnrestrictedModeButCannotBeLoaded() { + void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedButCannotBeLoaded() { try { - parser.parse(context(), tokens("!impliedRelationships", "com.example.CustomImpliedRelationshipsStrategy"), new File("."), false); + DslContext context = context(); + context.getFeatures().enable(Features.PLUGINS); + parser.parse(context, tokens("!impliedRelationships", "com.example.CustomImpliedRelationshipsStrategy"), new File(".")); fail(); } catch (Exception e) { assertEquals("Error loading implied relationships strategy: com.example.CustomImpliedRelationshipsStrategy was not found", e.getMessage()); @@ -80,8 +86,10 @@ void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedInUnrestrictedModeBut } @Test - void test_parse_SetsTheStrategy_WhenACustomStrategyIsUsedInUnrestrictedMode() { - parser.parse(context(), tokens("!impliedRelationships", "com.structurizr.dsl.example.CustomImpliedRelationshipsStrategy"), new File("."), false); + void test_parse_SetsTheStrategy_WhenACustomStrategyIsUsed() { + DslContext context = context(); + context.getFeatures().enable(Features.PLUGINS); + parser.parse(context, tokens("!impliedRelationships", "com.structurizr.dsl.example.CustomImpliedRelationshipsStrategy"), new File(".")); assertEquals("com.structurizr.dsl.example.CustomImpliedRelationshipsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java index 3a7e7e41c..803a9a545 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java @@ -9,10 +9,24 @@ class IncludeParserTests extends AbstractTests { private final IncludeParser parser = new IncludeParser(); + @Test + void test_parse_ThrowsAnException_WhenTheIncludeFeatureIsNotEnabled() { + try { + DslContext context = context(); + context.getFeatures().disable(Features.INCLUDE); + parser.parse(context, null, tokens("!include", "file", "extra")); + fail(); + } catch (Exception e) { + assertEquals("!include is not permitted (feature structurizr.feature.dsl.include is not enabled)", e.getMessage()); + } + } + @Test void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parse(context(), null, tokens("!include", "file", "extra")); + DslContext context = context(); + context.getFeatures().enable(Features.INCLUDE); + parser.parse(context, null, tokens("!include", "file", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: !include ", e.getMessage()); @@ -22,11 +36,52 @@ void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parse_ThrowsAnException_WhenAFileIsNotSpecified() { try { - parser.parse(context(), null, tokens("!include")); + DslContext context = context(); + context.getFeatures().enable(Features.INCLUDE); + parser.parse(context, null, tokens("!include")); fail(); } catch (Exception e) { assertEquals("Expected: !include ", e.getMessage()); } } + @Test + void test_parse_ThrowsAnException_WhenTheFileSystemAccessFeatureIsNotEnabled() { + try { + DslContext context = context(); + context.getFeatures().enable(Features.INCLUDE); + context.getFeatures().disable(Features.FILE_SYSTEM); + parser.parse(context, null, tokens("!include", "file")); + fail(); + } catch (Exception e) { + assertEquals("!include is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenIncludingFromHttpsAndHttpsIsNotEnabled() { + try { + DslContext context = context(); + context.getFeatures().enable(Features.INCLUDE); + context.getFeatures().disable(Features.HTTPS); + parser.parse(context, null, tokens("!include", "https://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Includes via HTTPS are not permitted (feature structurizr.feature.dsl.https is not enabled)", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenIncludingFromHttpAndHttpIsNotEnabled() { + try { + DslContext context = context(); + context.getFeatures().enable(Features.INCLUDE); + context.getFeatures().disable(Features.HTTP); + parser.parse(context, null, tokens("!include", "http://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Includes via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java index 5a0075aa6..28d3daa2b 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java @@ -13,7 +13,7 @@ class ThemeParserTests extends AbstractTests { @Test void test_parseTheme_ThrowsAnException_WhenThereAreTooManyTokens() { try { - parser.parseTheme(context(), null, tokens("theme", "url", "extra"), false); + parser.parseTheme(context(), null, tokens("theme", "url", "extra")); fail(); } catch (Exception e) { assertEquals("Too many tokens, expected: theme ", e.getMessage()); @@ -23,7 +23,7 @@ void test_parseTheme_ThrowsAnException_WhenThereAreTooManyTokens() { @Test void test_parseTheme_ThrowsAnException_WhenNoThemeIsSpecified() { try { - parser.parseTheme(context(), null, tokens("theme"), false); + parser.parseTheme(context(), null, tokens("theme")); fail(); } catch (Exception e) { assertEquals("Expected: theme ", e.getMessage()); @@ -32,7 +32,7 @@ void test_parseTheme_ThrowsAnException_WhenNoThemeIsSpecified() { @Test void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { - parser.parseTheme(context(), null, tokens("theme", "http://example.com/1"), false); + parser.parseTheme(context(), null, tokens("theme", "http://example.com/1")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -40,7 +40,7 @@ void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { @Test void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { - parser.parseTheme(context(), null, tokens("theme", "default"), false); + parser.parseTheme(context(), null, tokens("theme", "default")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); @@ -49,7 +49,7 @@ void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { @Test void test_parseThemes_ThrowsAnException_WhenNoThemesAreSpecified() { try { - parser.parseThemes(context(), null, tokens("themes"), false); + parser.parseThemes(context(), null, tokens("themes")); fail(); } catch (Exception e) { assertEquals("Expected: themes [url|file] ... [url|file]", e.getMessage()); @@ -58,7 +58,7 @@ void test_parseThemes_ThrowsAnException_WhenNoThemesAreSpecified() { @Test void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { - parser.parseThemes(context(), null, tokens("themes", "http://example.com/1"), false); + parser.parseThemes(context(), null, tokens("themes", "http://example.com/1")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -66,7 +66,7 @@ void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { @Test void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { - parser.parseThemes(context(), null, tokens("themes", "http://example.com/1", "http://example.com/2", "http://example.com/3"), false); + parser.parseThemes(context(), null, tokens("themes", "http://example.com/1", "http://example.com/2", "http://example.com/3")); assertEquals(3, workspace.getViews().getConfiguration().getThemes().length); assertEquals("http://example.com/1", workspace.getViews().getConfiguration().getThemes()[0]); @@ -76,7 +76,7 @@ void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { @Test void test_parseThemes_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { - parser.parseThemes(context(), null, tokens("themes", "default"), false); + parser.parseThemes(context(), null, tokens("themes", "default")); assertEquals(1, workspace.getViews().getConfiguration().getThemes().length); assertEquals("https://static.structurizr.com/themes/default/theme.json", workspace.getViews().getConfiguration().getThemes()[0]); @@ -84,9 +84,13 @@ void test_parseThemes_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { @Test void test_parseTheme_ThrowsAnException_WhenTheThemeFileDoesNotExist() { - File dslFile = new File("src/test/resources/themes/workspace.dsl"); try { - parser.parseTheme(context(), dslFile, tokens("theme", "my-theme.json"), false); + DslContext context = context(); + context.getFeatures().enable(Features.FILE_SYSTEM); + + File dslFile = new File("src/test/resources/themes/workspace.dsl"); + + parser.parseTheme(context, dslFile, tokens("theme", "my-theme.json")); fail(); } catch (Exception e) { assertTrue(e.getMessage().endsWith("/src/test/resources/themes/my-theme.json does not exist")); @@ -95,9 +99,13 @@ void test_parseTheme_ThrowsAnException_WhenTheThemeFileDoesNotExist() { @Test void test_parseTheme_ThrowsAnException_WhenTheThemeFileIsADirectory() { - File dslFile = new File("src/test/resources/workspace.dsl"); try { - parser.parseTheme(context(), dslFile, tokens("theme", "themes"), false); + DslContext context = context(); + context.getFeatures().enable(Features.FILE_SYSTEM); + + File dslFile = new File("src/test/resources/workspace.dsl"); + + parser.parseTheme(context, dslFile, tokens("theme", "themes")); fail(); } catch (Exception e) { assertTrue(e.getMessage().endsWith("/src/test/resources/themes is not a file")); @@ -106,8 +114,12 @@ void test_parseTheme_ThrowsAnException_WhenTheThemeFileIsADirectory() { @Test void test_parseTheme_InlinesTheTheme_WhenAThemeFileIsSpecified() { + DslContext context = context(); + context.getFeatures().enable(Features.FILE_SYSTEM); + File dslFile = new File("src/test/resources/themes/workspace.dsl"); - parser.parseTheme(context(), dslFile, tokens("theme", "theme.json"), false); + + parser.parseTheme(context, dslFile, tokens("theme", "theme.json")); assertEquals(0, workspace.getViews().getConfiguration().getThemes().length); assertEquals("#ff0000", workspace.getViews().getConfiguration().getStyles().getElementStyle("Tag").getBackground()); @@ -115,13 +127,16 @@ void test_parseTheme_InlinesTheTheme_WhenAThemeFileIsSpecified() { } @Test - void test_parseTheme_ThrowsAnException_WhenAThemeFileIsSpecifiedAndTheParserIsRunningInRestrictedMode() { + void test_parseTheme_ThrowsAnException_WhenAThemeFileIsSpecifiedAndFileSystemAccessIsNotEnabled() { try { + DslContext context = context(); + context.getFeatures().disable(Features.FILE_SYSTEM); + File dslFile = new File("src/test/resources/themes/workspace.dsl"); - parser.parseTheme(context(), dslFile, tokens("theme", "theme.json"), true); + parser.parseTheme(context, dslFile, tokens("theme", "theme.json")); fail(); } catch (Exception e) { - assertEquals("File-based themes are not supported when the DSL parser is running in restricted mode", e.getMessage()); + assertEquals("File-based themes are not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); } } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java index 009498f92..3f3ba44b4 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java @@ -7,7 +7,7 @@ class WorkspaceParserTests extends AbstractTests { - private WorkspaceParser parser = new WorkspaceParser(); + private final WorkspaceParser parser = new WorkspaceParser(); @Test void test_parseTitle_ThrowsAnException_WhenThereAreTooManyTokens() { @@ -46,6 +46,32 @@ void test_parse_SetsTheWorkspaceNameAndDescription_WhenANameAndDescriptionAreSpe assertEquals("New Description", workspace.getDescription()); } + @Test + void test_parse_ThrowsAnException_WhenExtendingAHttpsUrlAndHttpsIsNotEnabled() { + try { + DslParserContext context = new DslParserContext(null, null); + context.getFeatures().disable(Features.HTTPS); + + parser.parse(context, tokens("workspace", "extends", "https://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Extends via HTTPS are not permitted (feature structurizr.feature.dsl.https is not enabled)", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenExtendingAHttpUrlAndHttpIsNotEnabled() { + try { + DslParserContext context = new DslParserContext(null, null); + context.getFeatures().disable(Features.HTTPS); + + parser.parse(context, tokens("workspace", "extends", "http://example.com")); + fail(); + } catch (Exception e) { + assertEquals("Extends via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); + } + } + @Test void test_parseName_ThrowsAnException_WhenThereAreTooManyTokens() { try { diff --git a/structurizr-import/build.gradle b/structurizr-import/build.gradle index 7b5c3b32f..53caa555b 100644 --- a/structurizr-import/build.gradle +++ b/structurizr-import/build.gradle @@ -1,6 +1,6 @@ dependencies { - api project(':structurizr-core') + api project(':structurizr-client') } diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java index 04682876a..16c951e31 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java @@ -1,5 +1,6 @@ package com.structurizr.importer.diagrams; +import com.structurizr.http.HttpClient; import com.structurizr.view.View; import com.structurizr.view.ViewSet; @@ -21,6 +22,16 @@ public abstract class AbstractDiagramImporter { CONTENT_TYPES_BY_FORMAT.put(SVG_FORMAT, CONTENT_TYPE_IMAGE_SVG); } + protected HttpClient httpClient; + + public AbstractDiagramImporter() { + this.httpClient = new HttpClient(); + } + + public AbstractDiagramImporter(HttpClient httpClient) { + this.httpClient = httpClient; + } + protected String getViewOrViewSetProperty(View view, String name) { ViewSet views = view.getViewSet(); @@ -30,4 +41,4 @@ protected String getViewOrViewSetProperty(View view, String name) { ); } -} +} \ No newline at end of file diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java index ea1584d6c..600f10d21 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java @@ -1,19 +1,25 @@ package com.structurizr.importer.diagrams.image; +import com.structurizr.http.HttpClient; +import com.structurizr.http.RemoteContent; import com.structurizr.importer.diagrams.AbstractDiagramImporter; import com.structurizr.util.ImageUtils; import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import java.io.File; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; public class ImageImporter extends AbstractDiagramImporter { public static final String IMAGE_INLINE_PROPERTY = "image.inline"; + public ImageImporter() { + } + + public ImageImporter(HttpClient httpClient) { + super(httpClient); + } + public void importDiagram(ImageView view, File file) throws Exception { importDiagram(view, file, null); } @@ -29,23 +35,27 @@ public void importDiagram(ImageView view, String url) throws Exception { } public void importDiagram(ImageView view, String url, ColorScheme colorScheme) throws Exception { + RemoteContent remoteContent = httpClient.get(url, false); + String inline = getViewOrViewSetProperty(view, IMAGE_INLINE_PROPERTY); if ("true".equals(inline)) { - String imageFormat = ImageUtils.getContentType(url); - if (!imageFormat.equals(CONTENT_TYPE_IMAGE_PNG) && !imageFormat.equals(CONTENT_TYPE_IMAGE_SVG)) { - throw new IllegalArgumentException(String.format("Found %s - expected a format of %s or %s", imageFormat, PNG_FORMAT, SVG_FORMAT)); + + String contentType = remoteContent.getContentType(); + + if (!contentType.equals(CONTENT_TYPE_IMAGE_PNG) && !contentType.equals(CONTENT_TYPE_IMAGE_SVG)) { + throw new IllegalArgumentException(String.format("Found %s - expected a format of %s or %s", contentType, PNG_FORMAT, SVG_FORMAT)); } - if (imageFormat.equals(CONTENT_TYPE_IMAGE_SVG)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), false), colorScheme); + if (contentType.equals(CONTENT_TYPE_IMAGE_SVG)) { + view.setContent(ImageUtils.getSvgAsDataUri(remoteContent.getContentAsString()), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), false), colorScheme); + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); } - view.setContentType(imageFormat); + view.setContentType(contentType); } else { view.setContent(url, colorScheme); - view.setContentType(ImageUtils.getContentType(url)); + view.setContentType(remoteContent.getContentType()); } view.setTitle(url.substring(url.lastIndexOf("/")+1)); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java index d47ff0c30..fd06aa1c3 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java @@ -1,5 +1,7 @@ package com.structurizr.importer.diagrams.kroki; +import com.structurizr.http.HttpClient; +import com.structurizr.http.RemoteContent; import com.structurizr.importer.diagrams.AbstractDiagramImporter; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; @@ -7,7 +9,6 @@ import com.structurizr.view.ImageView; import java.io.File; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -17,6 +18,13 @@ public class KrokiImporter extends AbstractDiagramImporter { public static final String KROKI_FORMAT_PROPERTY = "kroki.format"; public static final String KROKI_INLINE_PROPERTY = "kroki.inline"; + public KrokiImporter() { + } + + public KrokiImporter(HttpClient httpClient) { + super(httpClient); + } + public void importDiagram(ImageView view, String format, File file) throws Exception { importDiagram(view, format, file, null); } @@ -52,10 +60,12 @@ public void importDiagram(ImageView view, String format, String content, ColorSc String inline = getViewOrViewSetProperty(view, KROKI_INLINE_PROPERTY); if ("true".equals(inline)) { + RemoteContent remoteContent = httpClient.get(url, true); + if (imageFormat.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true), colorScheme); + view.setContent(ImageUtils.getSvgAsDataUri(remoteContent.getContentAsString()), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true), colorScheme); + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); } } else { view.setContent(url, colorScheme); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java index 49711aef9..190b2b2d5 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java @@ -1,5 +1,7 @@ package com.structurizr.importer.diagrams.mermaid; +import com.structurizr.http.HttpClient; +import com.structurizr.http.RemoteContent; import com.structurizr.importer.diagrams.AbstractDiagramImporter; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; @@ -7,7 +9,6 @@ import com.structurizr.view.ImageView; import java.io.File; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -18,6 +19,13 @@ public class MermaidImporter extends AbstractDiagramImporter { public static final String MERMAID_COMPRESS_PROPERTY = "mermaid.compress"; public static final String MERMAID_INLINE_PROPERTY = "mermaid.inline"; + public MermaidImporter() { + } + + public MermaidImporter(HttpClient httpClient) { + super(httpClient); + } + public void importDiagram(ImageView view, File file) throws Exception { importDiagram(view, file, null); } @@ -63,10 +71,12 @@ public void importDiagram(ImageView view, String content, ColorScheme colorSchem String inline = getViewOrViewSetProperty(view, MERMAID_INLINE_PROPERTY); if ("true".equals(inline)) { + RemoteContent remoteContent = httpClient.get(url, true); + if (format.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true), colorScheme); + view.setContent(ImageUtils.getSvgAsDataUri(remoteContent.getContentAsString()), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true), colorScheme); + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); } } else { view.setContent(url, colorScheme); diff --git a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java index 33605447a..0e52bcad6 100644 --- a/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -1,14 +1,14 @@ package com.structurizr.importer.diagrams.plantuml; +import com.structurizr.http.HttpClient; +import com.structurizr.http.RemoteContent; import com.structurizr.importer.diagrams.AbstractDiagramImporter; import com.structurizr.util.ImageUtils; import com.structurizr.util.StringUtils; -import com.structurizr.util.Url; import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import java.io.File; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -20,6 +20,13 @@ public class PlantUMLImporter extends AbstractDiagramImporter { private static final String TITLE_STRING = "title "; private static final String NEWLINE = "\n"; + public PlantUMLImporter() { + } + + public PlantUMLImporter(HttpClient httpClient) { + super(httpClient); + } + public void importDiagram(ImageView view, File file) throws Exception { importDiagram(view, file, null); } @@ -55,10 +62,12 @@ public void importDiagram(ImageView view, String content, ColorScheme colorSchem String inline = getViewOrViewSetProperty(view, PLANTUML_INLINE_PROPERTY); if ("true".equals(inline)) { + RemoteContent remoteContent = httpClient.get(url, true); + if (format.equals(SVG_FORMAT)) { - view.setContent(ImageUtils.getSvgAsDataUri(new URL(url), true), colorScheme); + view.setContent(ImageUtils.getSvgAsDataUri(remoteContent.getContentAsString()), colorScheme); } else { - view.setContent(ImageUtils.getPngAsDataUri(new URL(url), true), colorScheme); + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); } } else { view.setContent(url, colorScheme); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java index bf8762878..0aebd6a12 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java @@ -59,7 +59,7 @@ public void importDiagram_AsInlinePNG() throws Exception { assertNull(view.getElement()); assertNull(view.getElementId()); assertEquals("flowchart.mmd", view.getTitle()); - assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("data:image/png;base64,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z3XzCTy2cpoDbiKB16ldu/bVsLCwq/a/ly9fHsyNLd933nknLdTy5ctnJYB70K5du7jGjRsnyPXExMSCM2bMsJUsWTLlxRdfjLOvI435mzdvovZ9Bx4fctyt95OAe/bZZxP69OkT43jff/7zn4SPPvqowJgxYwpMmzbtcnh4eK716CIjI5tywF1EwHkvbkwlFy5cOG2m46+//mrx9fX14cbPHWc/8nicDRNP4G7Kli3rOHPxwhdffBHGJUxrxu3r1KlTJoIseXzIbdy4MTAoKMjG3fYbGe8zGo305ptvxrz66qtFV65cmYe79DeOHj3q07t37yLjxo27wuEYvHXr1oCCBQtan3jiicTXX3/9ho/Prf8lV69eNX722Wd5pfwpLSUOtaSOHTvGlipVytK/f/++JpPJzGNwvQngDnibKdqkSZP4V155RbW+r1+/bujUqVMQVxZ8uIyZ9ksEL730UtGmTZvGdejQIT4hIcEwefLkfHv37vWPi4szcms9pWHDhgktW7ZMIPBa3Diy8NCInETgelbrTJw4Md/PP/8cVKBAASuXLhN5X6f2i/b93rvvvhvNJfb8ISEhVu4ZXk5JSaG5c+eGyH6Q93kmGeLh7TCe94dpla/srOPOPH7iyf79+/2qVauWFBgYmGnTWAKsUqVKyQcOHJDaNnFLW603ZcqU/PXr109csWLF+X79+l3j8ZU8GzZsUGehMJvNxCXOQvv27fPnYIzhjeKylJ14vSJcjgrmVfwQcJAd1atXT5KGkv1vrjz4FypUyMbbo6/9Nh5DNl27ds3I47tqp/Hee+8VvHDhgs+QIUOieedy8fHHH785a9asfLw9+hJ4NW64J3JDqWhm9y1cuDCkatWqySNHjrzavHnzuHXr1uX55Zdf1K9dcONd7fek6iX39erVSwXl1KlT8/G+L1hCa86cORelsT927NiC3HlI+5WM7Kzjzjw+5K5cuWLincYdy5BcUrJIC8TxNvmgGjRocJNDT0qPyaGhoZbDhw+rncju3bv9zp075/P2229fk7E8fn6r9ALz5s1r+/777wO5FziGALKhRo0aSdLAspcnedvy523KIr01CTe5LSoqyo+3LWvFihXNf/31l7+EIre6r1epUiVFWuTc84uThtrXX38dQuD1eFu5wkFXJOPtERERSTxskyjlzLZt28bLfk+qAXKf/ZAV2R7l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", view.getContent()); assertEquals("image/png", view.getContentType()); } @@ -95,7 +95,7 @@ public void importDiagram_AsInlineSVG() throws Exception { assertNull(view.getElement()); assertNull(view.getElementId()); assertEquals("flowchart.mmd", view.getTitle()); - assertEquals("data:image/svg+xml;base64,PHN2ZyBpZD0ibWVybWFpZC1zdmciIHdpZHRoPSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJmbG93Y2hhcnQiIHN0eWxlPSJtYXgtd2lkdGg6IDQ0MXB4OyIgdmlld0JveD0iMCAwIDQ0MSA1MTcuMTU2MjUiIHJvbGU9ImdyYXBoaWNzLWRvY3VtZW50IGRvY3VtZW50IiBhcmlhLXJvbGVkZXNjcmlwdGlvbj0iZmxvd2NoYXJ0LXYyIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHN0eWxlIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIj5AaW1wb3J0IHVybCgiaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9hamF4L2xpYnMvZm9udC1hd2Vzb21lLzYuNy4yL2Nzcy9hbGwubWluLmNzcyIpOzwvc3R5bGU+PHN0eWxlPiNtZXJtYWlkLXN2Z3tmb250LWZhbWlseToidHJlYnVjaGV0IG1zIix2ZXJkYW5hLGFyaWFsLHNhbnMtc2VyaWY7Zm9udC1zaXplOjE2cHg7ZmlsbDojMzMzO31Aa2V5ZnJhbWVzIGVkZ2UtYW5pbWF0aW9uLWZyYW1le2Zyb217c3Ryb2tlLWRhc2hvZmZzZXQ6MDt9fUBrZXlmcmFtZXMgZGFzaHt0b3tzdHJva2UtZGFzaG9mZnNldDowO319I21lcm1haWQtc3ZnIC5lZGdlLWFuaW1hdGlvbi1zbG93e3N0cm9rZS1kYXNoYXJyYXk6OSw1IWltcG9ydGFudDtzdHJva2UtZGFzaG9mZnNldDo5MDA7YW5pbWF0aW9uOmRhc2ggNTBzIGxpbmVhciBpbmZpbml0ZTtzdHJva2UtbGluZWNhcDpyb3VuZDt9I21lcm1haWQtc3ZnIC5lZGdlLWFuaW1hdGlvbi1mYXN0e3N0cm9rZS1kYXNoYXJyYXk6OSw1IWltcG9ydGFudDtzdHJva2UtZGFzaG9mZnNldDo5MDA7YW5pbWF0aW9uOmRhc2ggMjBzIGxpbmVhciBpbmZpbml0ZTtzdHJva2UtbGluZWNhcDpyb3VuZDt9I21lcm1haWQtc3ZnIC5lcnJvci1pY29ue2ZpbGw6IzU1MjIyMjt9I21lcm1haWQtc3ZnIC5lcnJvci10ZXh0e2ZpbGw6IzU1MjIyMjtzdHJva2U6IzU1MjIyMjt9I21lcm1haWQtc3ZnIC5lZGdlLXRoaWNrbmVzcy1ub3JtYWx7c3Ryb2tlLXdpZHRoOjFweDt9I21lcm1haWQtc3ZnIC5lZGdlLXRoaWNrbmVzcy10aGlja3tzdHJva2Utd2lkdGg6My41cHg7fSNtZXJtYWlkLXN2ZyAuZWRnZS1wYXR0ZXJuLXNvbGlke3N0cm9rZS1kYXNoYXJyYXk6MDt9I21lcm1haWQtc3ZnIC5lZGdlLXRoaWNrbmVzcy1pbnZpc2libGV7c3Ryb2tlLXdpZHRoOjA7ZmlsbDpub25lO30jbWVybWFpZC1zdmcgLmVkZ2UtcGF0dGVybi1kYXNoZWR7c3Ryb2tlLWRhc2hhcnJheTozO30jbWVybWFpZC1zdmcgLmVkZ2UtcGF0dGVybi1kb3R0ZWR7c3Ryb2tlLWRhc2hhcnJheToyO30jbWVybWFpZC1zdmcgLm1hcmtlcntmaWxsOiMzMzMzMzM7c3Ryb2tlOiMzMzMzMzM7fSNtZXJtYWlkLXN2ZyAubWFya2VyLmNyb3Nze3N0cm9rZTojMzMzMzMzO30jbWVybWFpZC1zdmcgc3Zne2ZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDt9I21lcm1haWQtc3ZnIHB7bWFyZ2luOjA7fSNtZXJtYWlkLXN2ZyAubGFiZWx7Zm9udC1mYW1pbHk6InRyZWJ1Y2hldCBtcyIsdmVyZGFuYSxhcmlhbCxzYW5zLXNlcmlmO2NvbG9yOiMzMzM7fSNtZXJtYWlkLXN2ZyAuY2x1c3Rlci1sYWJlbCB0ZXh0e2ZpbGw6IzMzMzt9I21lcm1haWQtc3ZnIC5jbHVzdGVyLWxhYmVsIHNwYW57Y29sb3I6IzMzMzt9I21lcm1haWQtc3ZnIC5jbHVzdGVyLWxhYmVsIHNwYW4gcHtiYWNrZ3JvdW5kLWNvbG9yOnRyYW5zcGFyZW50O30jbWVybWFpZC1zdmcgLmxhYmVsIHRleHQsI21lcm1haWQtc3ZnIHNwYW57ZmlsbDojMzMzO2NvbG9yOiMzMzM7fSNtZXJtYWlkLXN2ZyAubm9kZSByZWN0LCNtZXJtYWlkLXN2ZyAubm9kZSBjaXJjbGUsI21lcm1haWQtc3ZnIC5ub2RlIGVsbGlwc2UsI21lcm1haWQtc3ZnIC5ub2RlIHBvbHlnb24sI21lcm1haWQtc3ZnIC5ub2RlIHBhdGh7ZmlsbDojRUNFQ0ZGO3N0cm9rZTojOTM3MERCO3N0cm9rZS13aWR0aDoxcHg7fSNtZXJtYWlkLXN2ZyAucm91Z2gtbm9kZSAubGFiZWwgdGV4dCwjbWVybWFpZC1zdmcgLm5vZGUgLmxhYmVsIHRleHQsI21lcm1haWQtc3ZnIC5pbWFnZS1zaGFwZSAubGFiZWwsI21lcm1haWQtc3ZnIC5pY29uLXNoYXBlIC5sYWJlbHt0ZXh0LWFuY2hvcjptaWRkbGU7fSNtZXJtYWlkLXN2ZyAubm9kZSAua2F0ZXggcGF0aHtmaWxsOiMwMDA7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjFweDt9I21lcm1haWQtc3ZnIC5yb3VnaC1ub2RlIC5sYWJlbCwjbWVybWFpZC1zdmcgLm5vZGUgLmxhYmVsLCNtZXJtYWlkLXN2ZyAuaW1hZ2Utc2hhcGUgLmxhYmVsLCNtZXJtYWlkLXN2ZyAuaWNvbi1zaGFwZSAubGFiZWx7dGV4dC1hbGlnbjpjZW50ZXI7fSNtZXJtYWlkLXN2ZyAubm9kZS5jbGlja2FibGV7Y3Vyc29yOnBvaW50ZXI7fSNtZXJtYWlkLXN2ZyAucm9vdCAuYW5jaG9yIHBhdGh7ZmlsbDojMzMzMzMzIWltcG9ydGFudDtzdHJva2Utd2lkdGg6MDtzdHJva2U6IzMzMzMzMzt9I21lcm1haWQtc3ZnIC5hcnJvd2hlYWRQYXRoe2ZpbGw6IzMzMzMzMzt9I21lcm1haWQtc3ZnIC5lZGdlUGF0aCAucGF0aHtzdHJva2U6IzMzMzMzMztzdHJva2Utd2lkdGg6Mi4wcHg7fSNtZXJtYWlkLXN2ZyAuZmxvd2NoYXJ0LWxpbmt7c3Ryb2tlOiMzMzMzMzM7ZmlsbDpub25lO30jbWVybWFpZC1zdmcgLmVkZ2VMYWJlbHtiYWNrZ3JvdW5kLWNvbG9yOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7dGV4dC1hbGlnbjpjZW50ZXI7fSNtZXJtYWlkLXN2ZyAuZWRnZUxhYmVsIHB7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO30jbWVybWFpZC1zdmcgLmVkZ2VMYWJlbCByZWN0e29wYWNpdHk6MC41O2JhY2tncm91bmQtY29sb3I6cmdiYSgyMzIsMjMyLDIzMiwgMC44KTtmaWxsOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7fSNtZXJtYWlkLXN2ZyAubGFiZWxCa2d7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwgMjMyLCAyMzIsIDAuNSk7fSNtZXJtYWlkLXN2ZyAuY2x1c3RlciByZWN0e2ZpbGw6I2ZmZmZkZTtzdHJva2U6I2FhYWEzMztzdHJva2Utd2lkdGg6MXB4O30jbWVybWFpZC1zdmcgLmNsdXN0ZXIgdGV4dHtmaWxsOiMzMzM7fSNtZXJtYWlkLXN2ZyAuY2x1c3RlciBzcGFue2NvbG9yOiMzMzM7fSNtZXJtYWlkLXN2ZyBkaXYubWVybWFpZFRvb2x0aXB7cG9zaXRpb246YWJzb2x1dGU7dGV4dC1hbGlnbjpjZW50ZXI7bWF4LXdpZHRoOjIwMHB4O3BhZGRpbmc6MnB4O2ZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjtmb250LXNpemU6MTJweDtiYWNrZ3JvdW5kOmhzbCg4MCwgMTAwJSwgOTYuMjc0NTA5ODAzOSUpO2JvcmRlcjoxcHggc29saWQgI2FhYWEzMztib3JkZXItcmFkaXVzOjJweDtwb2ludGVyLWV2ZW50czpub25lO3otaW5kZXg6MTAwO30jbWVybWFpZC1zdmcgLmZsb3djaGFydFRpdGxlVGV4dHt0ZXh0LWFuY2hvcjptaWRkbGU7Zm9udC1zaXplOjE4cHg7ZmlsbDojMzMzO30jbWVybWFpZC1zdmcgcmVjdC50ZXh0e2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6MDt9I21lcm1haWQtc3ZnIC5pY29uLXNoYXBlLCNtZXJtYWlkLXN2ZyAuaW1hZ2Utc2hhcGV7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO3RleHQtYWxpZ246Y2VudGVyO30jbWVybWFpZC1zdmcgLmljb24tc2hhcGUgcCwjbWVybWFpZC1zdmcgLmltYWdlLXNoYXBlIHB7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO3BhZGRpbmc6MnB4O30jbWVybWFpZC1zdmcgLmljb24tc2hhcGUgcmVjdCwjbWVybWFpZC1zdmcgLmltYWdlLXNoYXBlIHJlY3R7b3BhY2l0eTowLjU7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO2ZpbGw6cmdiYSgyMzIsMjMyLDIzMiwgMC44KTt9I21lcm1haWQtc3ZnIC5sYWJlbC1pY29ue2Rpc3BsYXk6aW5saW5lLWJsb2NrO2hlaWdodDoxZW07b3ZlcmZsb3c6dmlzaWJsZTt2ZXJ0aWNhbC1hbGlnbjotMC4xMjVlbTt9I21lcm1haWQtc3ZnIC5ub2RlIC5sYWJlbC1pY29uIHBhdGh7ZmlsbDpjdXJyZW50Q29sb3I7c3Ryb2tlOnJldmVydDtzdHJva2Utd2lkdGg6cmV2ZXJ0O30jbWVybWFpZC1zdmcgOnJvb3R7LS1tZXJtYWlkLWZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT48Zz48bWFya2VyIGlkPSJtZXJtYWlkLXN2Z19mbG93Y2hhcnQtdjItcG9pbnRFbmQiIGNsYXNzPSJtYXJrZXIgZmxvd2NoYXJ0LXYyIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjUiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjgiIG1hcmtlckhlaWdodD0iOCIgb3JpZW50PSJhdXRvIj48cGF0aCBkPSJNIDAgMCBMIDEwIDUgTCAwIDEwIHoiIGNsYXNzPSJhcnJvd01hcmtlclBhdGgiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7IHN0cm9rZS1kYXNoYXJyYXk6IDEsIDA7Ii8+PC9tYXJrZXI+PG1hcmtlciBpZD0ibWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50U3RhcnQiIGNsYXNzPSJtYXJrZXIgZmxvd2NoYXJ0LXYyIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjQuNSIgcmVmWT0iNSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iOCIgbWFya2VySGVpZ2h0PSI4IiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0gMCA1IEwgMTAgMTAgTCAxMCAwIHoiIGNsYXNzPSJhcnJvd01hcmtlclBhdGgiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7IHN0cm9rZS1kYXNoYXJyYXk6IDEsIDA7Ii8+PC9tYXJrZXI+PG1hcmtlciBpZD0ibWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLWNpcmNsZUVuZCIgY2xhc3M9Im1hcmtlciBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iMTEiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjExIiBtYXJrZXJIZWlnaHQ9IjExIiBvcmllbnQ9ImF1dG8iPjxjaXJjbGUgY3g9IjUiIGN5PSI1IiByPSI1IiBjbGFzcz0iYXJyb3dNYXJrZXJQYXRoIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyBzdHJva2UtZGFzaGFycmF5OiAxLCAwOyIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9Im1lcm1haWQtc3ZnX2Zsb3djaGFydC12Mi1jaXJjbGVTdGFydCIgY2xhc3M9Im1hcmtlciBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iLTEiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjExIiBtYXJrZXJIZWlnaHQ9IjExIiBvcmllbnQ9ImF1dG8iPjxjaXJjbGUgY3g9IjUiIGN5PSI1IiByPSI1IiBjbGFzcz0iYXJyb3dNYXJrZXJQYXRoIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyBzdHJva2UtZGFzaGFycmF5OiAxLCAwOyIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9Im1lcm1haWQtc3ZnX2Zsb3djaGFydC12Mi1jcm9zc0VuZCIgY2xhc3M9Im1hcmtlciBjcm9zcyBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMSAxMSIgcmVmWD0iMTIiIHJlZlk9IjUuMiIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTEiIG1hcmtlckhlaWdodD0iMTEiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAxLDEgbCA5LDkgTSAxMCwxIGwgLTksOSIgY2xhc3M9ImFycm93TWFya2VyUGF0aCIgc3R5bGU9InN0cm9rZS13aWR0aDogMjsgc3Ryb2tlLWRhc2hhcnJheTogMSwgMDsiLz48L21hcmtlcj48bWFya2VyIGlkPSJtZXJtYWlkLXN2Z19mbG93Y2hhcnQtdjItY3Jvc3NTdGFydCIgY2xhc3M9Im1hcmtlciBjcm9zcyBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMSAxMSIgcmVmWD0iLTEiIHJlZlk9IjUuMiIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTEiIG1hcmtlckhlaWdodD0iMTEiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAxLDEgbCA5LDkgTSAxMCwxIGwgLTksOSIgY2xhc3M9ImFycm93TWFya2VyUGF0aCIgc3R5bGU9InN0cm9rZS13aWR0aDogMjsgc3Ryb2tlLWRhc2hhcnJheTogMSwgMDsiLz48L21hcmtlcj48ZyBjbGFzcz0icm9vdCI+PGcgY2xhc3M9ImNsdXN0ZXJzIi8+PGcgY2xhc3M9ImVkZ2VQYXRocyI+PHBhdGggZD0iTTIyMS44NTIsNjJMMjIxLjg1Miw2OC4xNjdDMjIxLjg1Miw3NC4zMzMsMjIxLjg1Miw4Ni42NjcsMjIxLjkyNiw5OC40MTdDMjIyLDExMC4xNjcsMjIyLjE0OSwxMjEuMzM0LDIyMi4yMjQsMTI2LjkxN0wyMjIuMjk4LDEzMi41IiBpZD0iTF9BX0JfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjxwYXRoIGQ9Ik0yMjIuMzUyLDE5MC41TDIyMi4yNjgsMTk0LjU4M0MyMjIuMTg1LDE5OC42NjcsMjIyLjAxOCwyMDYuODMzLDIyMi4wMDUsMjE0LjVDMjIxLjk5MiwyMjIuMTY3LDIyMi4xMzMsMjI5LjMzNCwyMjIuMjAzLDIzMi45MTdMMjIyLjI3MywyMzYuNTAxIiBpZD0iTF9CX0NfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjxwYXRoIGQ9Ik0xODAuMjE1LDMzOS41MTlMMTYwLjU5LDM1Mi42MjVDMTQwLjk2NiwzNjUuNzMyLDEwMS43MTcsMzkxLjk0NCw4Mi4wOTMsNDEwLjU1QzYyLjQ2OSw0MjkuMTU2LDYyLjQ2OSw0NDAuMTU2LDYyLjQ2OSw0NDUuNjU2TDYyLjQ2OSw0NTEuMTU2IiBpZD0iTF9DX0RfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjxwYXRoIGQ9Ik0yMjIuMzUyLDM4MS42NTZMMjIyLjI2OCwzODcuNzRDMjIyLjE4NSwzOTMuODIzLDIyMi4wMTgsNDA1Ljk5LDIyMS45MzUsNDE3LjU3M0MyMjEuODUyLDQyOS4xNTYsMjIxLjg1Miw0NDAuMTU2LDIyMS44NTIsNDQ1LjY1NkwyMjEuODUyLDQ1MS4xNTYiIGlkPSJMX0NfRV8wIiBjbGFzcz0iIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZWRnZS10aGlja25lc3Mtbm9ybWFsIGVkZ2UtcGF0dGVybi1zb2xpZCBmbG93Y2hhcnQtbGluayIgc3R5bGU9IiIgbWFya2VyLWVuZD0idXJsKCNtZXJtYWlkLXN2Z19mbG93Y2hhcnQtdjItcG9pbnRFbmQpIi8+PHBhdGggZD0iTTI2NC4zNDQsMzM5LjY2NEwyODMuNiwzNTIuNzQ2QzMwMi44NTcsMzY1LjgyOCwzNDEuMzcsMzkxLjk5MiwzNjAuNjI2LDQxMC41NzRDMzc5Ljg4Myw0MjkuMTU2LDM3OS44ODMsNDQwLjE1NiwzNzkuODgzLDQ0NS42NTZMMzc5Ljg4Myw0NTEuMTU2IiBpZD0iTF9DX0ZfMCIgY2xhc3M9IiBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSIiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZC1zdmdfZmxvd2NoYXJ0LXYyLXBvaW50RW5kKSIvPjwvZz48ZyBjbGFzcz0iZWRnZUxhYmVscyI+PGcgY2xhc3M9ImVkZ2VMYWJlbCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIxLjg1MTU2MjUsIDk5KSI+PGcgY2xhc3M9ImxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzkuMTMyODEyNSwgLTEyKSI+PGZvcmVpZ25PYmplY3Qgd2lkdGg9Ijc4LjI2NTYyNSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgY2xhc3M9ImxhYmVsQmtnIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9ImVkZ2VMYWJlbCAiPjxwPkdldCBtb25leTwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9ImVkZ2VMYWJlbCI+PGcgY2xhc3M9ImxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLCAwKSI+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjAiIGhlaWdodD0iMCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgY2xhc3M9ImxhYmVsQmtnIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9ImVkZ2VMYWJlbCAiPjwvc3Bhbj48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9nPjwvZz48ZyBjbGFzcz0iZWRnZUxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Mi40Njg3NSwgNDE4LjE1NjI1KSI+PGcgY2xhc3M9ImxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTUuMTI1LCAtMTIpIj48Zm9yZWlnbk9iamVjdCB3aWR0aD0iMzAuMjUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIGNsYXNzPSJsYWJlbEJrZyIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJlZGdlTGFiZWwgIj48cD5PbmU8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJlZGdlTGFiZWwiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMS44NTE1NjI1LCA0MTguMTU2MjUpIj48ZyBjbGFzcz0ibGFiZWwiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNC42NzE4NzUsIC0xMikiPjxmb3JlaWduT2JqZWN0IHdpZHRoPSIyOS4zNDM3NSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgY2xhc3M9ImxhYmVsQmtnIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9ImVkZ2VMYWJlbCAiPjxwPlR3bzwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9ImVkZ2VMYWJlbCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzc5Ljg4MjgxMjUsIDQxOC4xNTYyNSkiPjxnIGNsYXNzPSJsYWJlbCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIwLjg5ODQzNzUsIC0xMikiPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI0MS43OTY4NzUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIGNsYXNzPSJsYWJlbEJrZyIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJlZGdlTGFiZWwgIj48cD5UaHJlZTwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PC9nPjxnIGNsYXNzPSJub2RlcyI+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCAgIiBpZD0iZmxvd2NoYXJ0LUEtMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIxLjg1MTU2MjUsIDM1KSI+PHJlY3QgY2xhc3M9ImJhc2ljIGxhYmVsLWNvbnRhaW5lciIgc3R5bGU9IiIgeD0iLTY2LjAwNzgxMjUiIHk9Ii0yNyIgd2lkdGg9IjEzMi4wMTU2MjUiIGhlaWdodD0iNTQiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zNi4wMDc4MTI1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjcyLjAxNTYyNSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJub2RlTGFiZWwgIj48cD5DaHJpc3RtYXM8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJub2RlIGRlZmF1bHQgICIgaWQ9ImZsb3djaGFydC1CLTEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMS44NTE1NjI1LCAxNjMpIj48ZyBjbGFzcz0iYmFzaWMgbGFiZWwtY29udGFpbmVyIG91dGVyLXBhdGgiPjxwYXRoIGQ9Ik0tNTUuMzY3MTg3NSAtMjcgQy0zMC4yMDQ5NTY4MTg5MTIyMzcgLTI3LCAtNS4wNDI3MjYxMzc4MjQ0NzQgLTI3LCA1NS4zNjcxODc1IC0yNyBDNTUuMzY3MTg3NSAtMjcsIDU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNyBDNTUuNDgxOTY0MTg0NzQ3NjkgLTI2Ljk5NTI1MjgwMTIwMzY4LCA1NS41OTY3NDA4Njk0OTUzOCAtMjYuOTkwNTA1NjAyNDA3MzY0LCA1NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEM1NS45MzU4OTE1NDUzNjU0NSAtMjYuOTYzNTAxMTAzMDk4MDYsIDU2LjA5MTY5ODg2MzM2OTIzIC0yNi45NDQwNzk3NDExNjI3NzQsIDU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQzU2LjMwNTQ4MjkxMDI0NDczIC0yNi45MDc2MjU5NDAyMDMzNzgsIDU2LjQyMDgwNTM2OTA4NTc4NCAtMjYuODgzNDQ1MzYzMzkzMTQzLCA1Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNzIyNDc4MjM5MzM3MjQgLTI2LjgwODkzNDc3NDkwMDkyNiwgNTYuODUwMzQxNTQyOTcwNSAtMjYuNzcwODY4MjIwMTA1MTk2LCA1Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEM1Ny4wNjg1Njk5NzEzMDQ2NzYgLTI2LjY5ODY5NTMzODMxMjEzLCA1Ny4xNDY0NTUwOTY1ODU5MyAtMjYuNjY4MzA0NDY4MTIxMDg2LCA1Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuNDk5MzQ0NDA2MTA4MjIgLTI2LjUxODQwMzI3MTc0NDQzLCA1Ny42MjMwMjQxODg5NTE2MDQgLTI2LjQ1NzkzOTkxMDIxMzU3NSwgNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQzU3Ljg0MjcxNTMwMjI0MzkyIC0yNi4zNDAyODk4MDY0NzMzMTUsIDU3LjkzODUwNjEzOTMwMjQ3NiAtMjYuMjgzMjEwODU2OTE0MTg0LCA1OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMjA3MjQ3MDcwNTc5MiAtMjYuMTEwNjM2MjI3NzQyNDA0LCA1OC4zMTI1NjU4NTA1NDYyNyAtMjYuMDM1NDQwMDY0MTcyMTY0LCA1OC40MzgyNTEwNjM0NDgzNCAtMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjU1NDEyNjAwMDEwNzc2NSAtMjUuODQ3NTYxNDgxODM0NTMsIDU4LjY3MDAwMDkzNjc2NzE5IC0yNS43NDk0MjA0MTY2ODcwODQsIDU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IEM1OC44NTUwMzgwMjg2MDczNiAtMjUuNTc3MTc2ODgyODg3MDAyLCA1OC45NTY0ODA2OTkwODYwMTUgLTI1LjQ3NTczNDIxMjQwODM0OCwgNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBDNTkuMTAwMjg1MzM2Njc2NjggLTI1LjMyMjA4NTQ3MDU2ODU2NywgNTkuMTU0NzYzNjE5OTg3NyAtMjUuMjU3NzYzMDgzMDA4NDMsIDU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBDNTkuMzY1NDk3Nzc5ODYwNTkgLTI0Ljk5NzM4MTg0OTEwODgyNywgNTkuNDE4MTA1NTEyNzM5MjIgLTI0LjkyMzcwMDEzNDc2OTMxLCA1OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzNiBDNTkuNjM2MTkyMjU2NjkxNzg1IC0yNC41OTUxNTk1NTE2MjA0NDQsIDU5LjcxOTM2NDYyMjA3MDkzNCAtMjQuNDU1NTc4MzEyNjI4NzU2LCA1OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzcgQzU5LjgyMTI3NTk1MTk2NDQ3IC0yNC4yNjM3MTQ5NzI3MjI0NSwgNTkuODc3OTk1NjQ3ODk2NTA1IC0yNC4xNDc2OTI5ODAyNTk1MzUsIDU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQzU5Ljk4NjIxNTc4NTA3MDAyIC0yMy45MDU1NTE2MzA3Nzc0LCA2MC4wMjYzNzc0MzY4NjQ3NiAtMjMuODAyNjI2MTM4Mjg5OTUyLCA2MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMTMyNzU0ODU4NTU5ODIgLTIzLjUwMDk1OTMyMzAzNjcyNCwgNjAuMTY5MjM2MDA4NjE2NDY1IC0yMy4zNzg0MjEzMDAwNTAwMzIsIDYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQzYwLjIzMzE0MDkyMjIwMzk3IC0yMy4xMzcwNDA3NjE5MTcwMiwgNjAuMjUyMDkzMDE0NzExMyAtMjMuMDQ2NjU0MDg4MTMwMDQ3LCA2MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgQzYwLjMxNjkwMjMyNTk2MjgxIC0yMi42NzkzMDQwNTc0OTkzOSwgNjAuMzM0ODEwNjM0OTEyIC0yMi41MzU2MzUxNjM1OTUxMSwgNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzU0NDU4NTI3OTA5ODkgLTIyLjMwNzc1ODE3MDU0MDc1OCwgNjAuMzU4ODA3MDkwNzg2NDQgLTIyLjIwMjYxOTYxMzcxOTg1NCwgNjAuMzY3MTg3NSAtMjIgQzYwLjM2NzE4NzUgLTIyLCA2MC4zNjcxODc1IC0yMiwgNjAuMzY3MTg3NSAtMjIgQzYwLjM2NzE4NzUgLTYuNjA5NzgyODc3MTEyOTA4LCA2MC4zNjcxODc1IDguNzgwNDM0MjQ1Nzc0MTg1LCA2MC4zNjcxODc1IDIyIEM2MC4zNjcxODc1IDIyLCA2MC4zNjcxODc1IDIyLCA2MC4zNjcxODc1IDIyIEM2MC4zNjMxMDk0MTQzMTA1NjQgMjIuMDk4NTk5MDIxMzY2Njg1LCA2MC4zNTkwMzEzMjg2MjExMzYgMjIuMTk3MTk4MDQyNzMzMzczLCA2MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgQzYwLjMzODgxNDQxMzMzMjkxIDIyLjUwMzUxNDk2NjI3MjA3NSwgNjAuMzI3NTE4ODYxNjMyNDg0IDIyLjU5NDEzMzIwNTE4MjQ4NywgNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQzYwLjI2NTY3MzY2NDU5NjI3IDIyLjk4MTg4NDk5ODg4NTk5LCA2MC4yMzIzNTMzMTIxNzg5NCAyMy4xNDA3OTcwNDYzNjgzMSwgNjAuMjE0MTg4ODI5Njk2NjUgMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4xODQ1MDc0NDQxNTI1NDQgMjMuMzI3MTI1NDUyMzA3OTk2LCA2MC4xNTQ4MjYwNTg2MDg0NCAyMy40MjY4MjM0Njg5MTIsIDYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQzYwLjA0MjY3MTUwOTM2NTI5IDIzLjc2MDg2ODAwOTI4Mzg0LCA1OS45ODkwNjkzMTAyMjc0MSAyMy44OTgyMzg2NzI1NDQyNjcsIDU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuOTAyMDYyMjEyOTQ3NjU1IDI0LjA5ODQ2NDAzNDU5ODA2LCA1OS44NTgwNzAyOTI2MjAwMiAyNC4xODg0NTA5NDU5MzEyNzYsIDU5Ljc2NDU1NjI1NjAzMjQ1IDI0LjM3OTczNjk2NTE4NTM2NiBDNTkuNjg1NjQ4MDMxNTg5MTUgMjQuNTEyMTYyMDUyMzQyODg3LCA1OS42MDY3Mzk4MDcxNDU4NSAyNC42NDQ1ODcxMzk1MDA0MDUsIDU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQzU5LjQ1OTc1Njc2ODgzODQyNSAyNC44NjUzNjM5MjA5NDk0OCwgNTkuMzY2NDkzNjQ2MzY0MjA1IDI0Ljk5NTk4NzA1MTI4NjgyNSwgNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQzU5LjIzNzczNjUyMDE2NTk0IDI1LjE1OTc5NzE2NDg4NjgxLCA1OS4xNjI1ODI5OTMzNDk5MSAyNS4yNDg1MzA3NjYzMjUyODUsIDU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBDNTguOTMzMTA4MjAxNDY3NzkgMjUuNDk5MTA2NzEwMDI2NTc0LCA1OC44MjA0MDkzNDk1Njk5MiAyNS42MTE4MDU1NjE5MjQ0NCwgNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBDNTguNjUxOTM0NzA3MDgxMDU0IDI1Ljc2NDcyMTczMjcwNjg0NiwgNTguNTUwMjc0MDU2MDMzNCAyNS44NTA4MjM5MTIwNDgwNCwgNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjM2MjY5NzIzMjc3NTEgMjUuOTk5NjQ2OTQ1MjMxMTcsIDU4LjI4NzE0MzQwMjEwMTg1NCAyNi4wNTM1OTEzNDM0ODAzNywgNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMDAxNzI2ODI4NjczODIgMjYuMjQ1NTM5NTAyODQxODIyLCA1Ny45MDE1MjUzNjY3MzU1IDI2LjMwNTI0NjYxNDM3MSwgNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBDNTcuNjU5NzUyNzU1OTA3NzI1IDI2LjQzOTk4NDQwNzkzNzI2LCA1Ny41NzI1ODEwNDY2MzAwOSAyNi40ODI2MDAwNTk4NDIwNzQsIDU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgQzU3LjI4NzYxMjIwOTIyMDkzIDI2LjYxMzIyNDc5MTg1MjQ2MywgNTcuMTk5NTU5Nzk1MTc3MDIgMjYuNjQ3NTgyOTUwNDI5NjM3LCA1Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQzU2Ljg5MDc2NzQ0NzIzMTc1IDI2Ljc1ODgzMjkwNjk1NDM1LCA1Ni43OTA4NTAwNDg0NDAwODUgMjYuNzg4NTc5NjA1NDA1NTI3LCA1Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIEM1Ni40OTAzMjk4MTI5NjQwNSAyNi44Njg4Njc2MjA0OTQ4MTgsIDU2LjM4NjA0NDY5MDIyNDEgMjYuODkwNzMzOTExMjkyOTg3LCA1Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQzU2LjAyOTUyMzkyMjY4NTQ3IDI2Ljk1MTgyOTgzOTQzNjY4NywgNTUuODY4ODg3MzkzOTY3MjggMjYuOTcxODUzMTYxODU5NzYyLCA1NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgQzU1LjY1MTg4NTcyODY5NTI2IDI2Ljk4ODIyNDc5NDE1OTY1NiwgNTUuNTIzNjg3MjMwMDI4ODYgMjYuOTkzNTI3MTIzMjg1OTcsIDU1LjM2NzE4NzUgMjcgQzU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcgQzI4LjU1MDIzMDcyMDQwMjE2NSAyNywgMS43MzMyNzM5NDA4MDQzMzEgMjcsIC01NS4zNjcxODc1IDI3IEMtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcsIC01NS4zNjcxODc1IDI3IEMtNTUuNDkwNDAxNzQxOTI5Nzk0IDI2Ljk5NDkwMzgyMTI1NzIxOCwgLTU1LjYxMzYxNTk4Mzg1OTU4IDI2Ljk4OTgwNzY0MjUxNDQzLCAtNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuODcwMDc2NjQ2NjAxMTM0IDI2Ljk3MTcwNDkyMTY3MzU0NSwgLTU1Ljk2MDA2OTA2NTg0MDYgMjYuOTYwNDg3Mzc4MzEzNzQ0LCAtNTYuMTkwMTYwNDUxNDAzNjcgMjYuOTMxODA2NTE3MDEzNjEyIEMtNTYuMzA0MDA5NDM2MDQ0NDk1IDI2LjkwNzkzNDg5NTI1MzcxNywgLTU2LjQxNzg1ODQyMDY4NTMyIDI2Ljg4NDA2MzI3MzQ5MzgyMywgLTU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgQy01Ni42ODIyNTYyMjI4OTgwOCAyNi44MjA5MDkzODgwMTcxOTIsIC01Ni43Njk4OTc1MTAwOTIxNyAyNi43OTQ4MTc0NDYzMzc3MywgLTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDLTU3LjE0MDk1ODA2MzM4MzEyIDI2LjY3MDQ0OTQxNzEzMjg0MywgLTU3LjI5MTIzMTI4MDc0MjgxNCAyNi42MTE4MTI2MjU3NjI1MTQsIC01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNDUxOTQ5NzMxMjY5NDUgMjYuNTQxNTczMTE2NDMxNTIyLCAtNTcuNTI4MjM0ODM5Mjc0MDU2IDI2LjUwNDI3OTU5OTU4Nzc1NCwgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgQy01Ny44NDc2ODIwMzkzMTI2NyAyNi4zMzczMzAyNzM1NjQ2NzgsIC01Ny45NDg0Mzk2MTM0Mzk5OCAyNi4yNzcyOTE3OTEwOTY5MDcsIC01OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTguMjI4NDIzMjg3ODk3MzEgMjYuMDk1NTE2Njk5MTg1ODcsIC01OC4zNTQ5MTgyODUxODI0ODUgMjYuMDA1MjAxMDA3MDU5MDkyLCAtNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQy01OC41NjIxNTI4MDA2NzQ1NDYgMjUuODQwNzYzMTI4NTQwMzYzLCAtNTguNjg2MDU0NTM3OTAwNzUgMjUuNzM1ODIzNzEwMDk4NzU3LCAtNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IEMtNTguODE2MzM1MTU3ODUxMjg1IDI1LjYxNTg3OTc1MzY0MzA4MiwgLTU4Ljg3OTA3NDk1NzU3Mzg1NiAyNS41NTMxMzk5NTM5MjA1MDcsIC01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQy01OS4xMzc0NDE0MTkyMjEwOCAyNS4yNzgyMTUzNjY4MDY0MjgsIC01OS4yMjkwNzU3ODUwNzY1IDI1LjE3MDAyMjg3NTQ4NDE1LCAtNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4zNzkwNzc1MDY0MTUxIDI0Ljk3ODM2MjI1ODY3ODkwMywgLTU5LjQ0NTI2NDk2NTg0ODI0IDI0Ljg4NTY2MDk1MzkwOTQ2NSwgLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQy01OS42MTg1NzgzNDUyNTk3NiAyNC42MjQ3MTk1MDkwNjUwMTMsIC01OS42ODQxMzY3OTkyMDY4OCAyNC41MTQ2OTgyMjc1MTc4OTQsIC01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBDLTU5LjgxMzYxMTQ1NzU2NzY3IDI0LjI3OTM5Mjk0NjU5OTUyLCAtNTkuODYyNjY2NjU5MTAyOSAyNC4xNzkwNDg5MjgwMTM2NjgsIC01OS45NDYwNTQxMzMyNzUyOCAyNC4wMDg0NzcxMjMyNjQ4NSBDLTU5Ljk5MDQ3OTQzNTIyMDk2IDIzLjg5NDYyNDgzMTkwNDExNCwgLTYwLjAzNDkwNDczNzE2NjY1IDIzLjc4MDc3MjU0MDU0MzM4LCAtNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBDLTYwLjEyNDc3MzE2MTY2NzAxNSAyMy41Mjc3NjkzNzAzMDIzNzMsIC02MC4xNTMyNzI2MTQ4MzA4NSAyMy40MzIwNDEzOTQ1ODEzMjYsIC02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4yMzY3MDE1MjUyNjU3IDIzLjEyMDA1OTQ2NzI0OTkyNCwgLTYwLjI1OTIxNDIyMDgzNDc1IDIzLjAxMjY5MTQ5ODc5NTg1LCAtNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQy02MC4zMTEyNjI3MjE5NDc4NyAyMi43MjQ1NDc2MTg2NDA5MSwgLTYwLjMyMzUzMTQyNjg4MjEzIDIyLjYyNjEyMjI4NTg3ODE0NywgLTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBDLTYwLjM1NjM5NjM0OTE4NDI0IDIyLjI2MDkwNTk3MTc5MjQyMiwgLTYwLjM2MjY4MjczMzMzNTEzNCAyMi4xMDg5MTUyMTYyMjMxNzgsIC02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSAyMiwgLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSA5LjcyMDAzMjY0MjAyNzc2MiwgLTYwLjM2NzE4NzUgLTIuNTU5OTM0NzE1OTQ0NDc1NCwgLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzY3MTg3NSAtMjIsIC02MC4zNjcxODc1IC0yMiwgLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYyODU3NDk5OTA1MzQgLTIyLjEwNDY4OTc1MDEzMzc5NCwgLTYwLjM1ODUyNzQ5OTgxMDY4NCAtMjIuMjA5Mzc5NTAwMjY3NTg0LCAtNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IEMtNjAuMzM1ODAwMTMxODUyODYgLTIyLjUyNzY5Njk1MjgyMjgxNSwgLTYwLjMyMTQ5MDI5ODY3MjM2NiAtMjIuNjQyNDk3MTc4MjgzOTc1LCAtNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMjgxMzQyNDkxMTA3NzM2IC0yMi45MDcxNTY5Mzg2NDgzNzQsIC02MC4yNjM2OTA5NjUyMDE4NSAtMjIuOTkxMzQwOTI1ODkzMDgsIC02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEMtNjAuMTY4MDA3MzI5NjMwMjg1IC0yMy4zODI1NDgzNjAwMTQzNzIsIC02MC4xMjE4MjU4Mjk1NjM5MiAtMjMuNTM3NjY5Mjg0MzI0NzQ2LCAtNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgQy02MC4wNTQ2NTAzNjUzNTQ0NSAtMjMuNzMwMTY4ODMyNDA2ODIsIC02MC4wMTMwMjcwMjIyMDU3MSAtMjMuODM2ODQwMzE4NzkwMjI3LCAtNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBDLTU5Ljg3NTQxNjk3NjMwOTc2IC0yNC4xNTI5Njc3MzcyOTkxMzYsIC01OS44MDQ3Nzk4MTkzNDQyMjYgLTI0LjI5NzQ1ODM1MTMzMzQyLCAtNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM2NiBDLTU5LjcwMDYyMTA5NTI1Nzk5IC0yNC40ODcwMzQwMDkyODU5LCAtNTkuNjM2Njg1OTM0NDgzNTQgLTI0LjU5NDMzMTA1MzM4NjQzLCAtNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzMgQy01OS40OTM3MDc1NjU0MzEwMiAtMjQuODE3ODEyODcxNzE2NzI1LCAtNTkuNDM0Mzk1MjM5NTQ5NCAtMjQuOTAwODg0OTUyODIxMzIsIC01OS4zMTI4OTAwNDY5ODE5NyAtMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4yMjI0MTUyNTI0OTI2MSAtMjUuMTc3ODg2OTUxNDQ1Nzk2LCAtNTkuMTMxOTQwNDU4MDAzMjYgLTI1LjI4NDcxMDMzOTQ0MzI1NCwgLTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgQy01OC45NTQ4NjY5NzI1Njg4NDQgLTI1LjQ3NzM0NzkzODkyNTUyLCAtNTguODYzOTI2ODkxNzcyMDMgLTI1LjU2ODI4ODAxOTcyMjMzLCAtNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgQy01OC42ODU3ODExMzMyMTU0OSAtMjUuNzM2MDU1MjcyMDU1MTI1LCAtNTguNjE3OTY2OTA4MzAyMjggLTI1Ljc5MzQ5MDk5MDc0NDU5OCwgLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgQy01OC4zNjQ4NDkyMDg0NjE2MzUgLTI1Ljk5ODExMDQ2NDEyMzI5MywgLTU4LjI5MTQ0NzM1MzQ3NDkyIC0yNi4wNTA1MTgzODEyNjQ2MiwgLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTcuOTc1NTMyNDU5MzkwMDIgLTI2LjI2MTE0Nzk1OTAyNjA5NywgLTU3Ljg0OTEzNjYyODE2NzkwNSAtMjYuMzM2NDYzNTI2NzM5NTQ2LCAtNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MDkzMjgwNzM5MTc1NSAtMjYuNDY0NjM1NTMyODU5Njg1LCAtNTcuNDcxNzMxNjgyNjQ5NzQgLTI2LjUzMTkwMjMwOTY4NjkyLCAtNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQy01Ny4yNzQzNzA3ODI1MDY2OSAtMjYuNjE4MzkxNjEyNTg5NTg3LCAtNTcuMTczMDc2OTQxNzQ4NTQgLTI2LjY1NzkxNjU5MTkwMzg4NCwgLTU2Ljk5MDY4NDg0NjAyMzQyIC0yNi43MjkwODYyMDg1MDMxNzMgQy01Ni44ODU5NjU3MDA2Njk2MTUgLTI2Ljc2MDI2MjQ0ODg0MzQ1MiwgLTU2Ljc4MTI0NjU1NTMxNTgyIC0yNi43OTE0Mzg2ODkxODM3MywgLTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNDkwMjQ4NTEyNjIwOTIgLTI2Ljg2ODg4NDY2NzM4NDEzNywgLTU2LjM4NTg4MjA4OTUzNzg2IC0yNi44OTA3NjgwMDUwNzE2MTcsIC01Ni4xOTAxNjA0NTE0MDM2NyAtMjYuOTMxODA2NTE3MDEzNjEyIEMtNTYuMDg2MzE4NTkyNjY3NjEgLTI2Ljk0NDc1MDM5MTIwNjMzLCAtNTUuOTgyNDc2NzMzOTMxNTU2IC0yNi45NTc2OTQyNjUzOTkwNTIsIC01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuNjk1MzIyNTMxNTM1NjIgLTI2Ljk4NjQyODIzNDcwNDk4LCAtNTUuNjEwNTYwODM1NzA5NTg1IC0yNi45ODk5MzQwMDQzNzY2MTgsIC01NS4zNjcxODc1IC0yNyBDLTU1LjM2NzE4NzUgLTI3LCAtNTUuMzY3MTg3NSAtMjcsIC01NS4zNjcxODc1IC0yNyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjAiIGZpbGw9IiNFQ0VDRkYiIHN0eWxlPSIiLz48cGF0aCBkPSJNLTU1LjM2NzE4NzUgLTI3IEMtMjAuMjQyNzk3ODM4MTEyNzA4IC0yNywgMTQuODgxNTkxODIzNzc0NTg1IC0yNywgNTUuMzY3MTg3NSAtMjcgTS01NS4zNjcxODc1IC0yNyBDLTEyLjEwNTEzNzc2Mzc0NzU2NCAtMjcsIDMxLjE1NjkxMTk3MjUwNDg3MiAtMjcsIDU1LjM2NzE4NzUgLTI3IE01NS4zNjcxODc1IC0yNyBDNTUuMzY3MTg3NSAtMjcsIDU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNyBNNTUuMzY3MTg3NSAtMjcgQzU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNywgNTUuMzY3MTg3NSAtMjcgTTU1LjM2NzE4NzUgLTI3IEM1NS40Njg1MTUyMzY1NTI1NCAtMjYuOTk1ODA5MDUzODE1NjUsIDU1LjU2OTg0Mjk3MzEwNTA4IC0yNi45OTE2MTgxMDc2MzEyOTcsIDU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgTTU1LjM2NzE4NzUgLTI3IEM1NS40ODYyNzY3MzcyNDMzMSAtMjYuOTk1MDc0NDMyNzEzMDY4LCA1NS42MDUzNjU5NzQ0ODY2MiAtMjYuOTkwMTQ4ODY1NDI2MTMyLCA1NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IE01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEM1NS44NjU4ODcxNDgzNDkwNCAtMjYuOTcyMjI3MTQyMDg2MDYyLCA1NS45NTE2OTAwNjkzMzY0MSAtMjYuOTYxNTMxODE5MTM4NzgsIDU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgTTU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgQzU1LjkxNjIwMzA1NzQ0NDk3NCAtMjYuOTY1OTU1MjcwNTU3OTc3LCA1Ni4wNTIzMjE4ODc1MjgyOSAtMjYuOTQ4OTg4MDc2MDgyNjEsIDU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgTTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQzU2LjMzMTIyODgxNDEwMDc3NiAtMjYuOTAyMjI3NTkxODUwNDUsIDU2LjQ3MjI5NzE3Njc5Nzg4IC0yNi44NzI2NDg2NjY2ODcyODcsIDU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni4xOTAxNjA0NTE0MDM2NyAtMjYuOTMxODA2NTE3MDEzNjEyIEM1Ni4yNzkzNjMxMzA3MTE4MiAtMjYuOTEzMTAyNjgyMDQxOTUsIDU2LjM2ODU2NTgxMDAxOTk3IC0yNi44OTQzOTg4NDcwNzAyODUsIDU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNzE1NTI0NTQwNjYzNTQgLTI2LjgxMTAwNDk4MDY5MTU5NiwgNTYuODM2NDM0MTQ1NjIzMDcgLTI2Ljc3NTAwODYzMTY4NjUzNiwgNTYuOTkwNjg0ODQ2MDIzNDIgLTI2LjcyOTA4NjIwODUwMzE3MyBNNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgQzU2LjcxNDI3NDc0MDkzMDA4IC0yNi44MTEzNzcwNjIxOTMzNjUsIDU2LjgzMzkzNDU0NjE1NjE2NSAtMjYuNzc1NzUyNzk0NjkwMDc4LCA1Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIE01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEM1Ny4xMzU2MTAyMTcwNjk4NSAtMjYuNjcyNTM2MTUzMjM5NjU1LCA1Ny4yODA1MzU1ODgxMTYyOSAtMjYuNjE1OTg2MDk3OTc2MTQsIDU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEM1Ny4xNDAxNTYyNTQzNzQxNyAtMjYuNjcwNzYyMjgzOTc5LCA1Ny4yODk2Mjc2NjI3MjQ5MSAtMjYuNjEyNDM4MzU5NDU0ODIsIDU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE01Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuNDc0MzUyNDc4NzY0MTggLTI2LjUzMDYyMTA4MDUzNzU1NywgNTcuNTczMDQwMzM0MjYzNTI0IC0yNi40ODIzNzU1Mjc3OTk4MjUsIDU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IE01Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuNDg3Mjg4NjYyNjM2NiAtMjYuNTI0Mjk2OTY1NTk4NzU0LCA1Ny41OTg5MTI3MDIwMDgzNTYgLTI2LjQ2OTcyNzI5NzkyMjIyMywgNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgTTU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IEM1Ny44MzkwOTM1MDM0MzY3IC0yNi4zNDI0NDc5MzAxMjg3NTYsIDU3LjkzMTI2MjU0MTY4ODAzIC0yNi4yODc1MjcxMDQyMjUwNjcsIDU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IE01Ny43NDY5MjQ0NjUxODUzNjYgLTI2LjM5NzM2ODc1NjAzMjQ0NiBDNTcuODI5OTI3ODU5OTM0MDMgLTI2LjM0NzkwOTQ2ODE5NjgwNCwgNTcuOTEyOTMxMjU0NjgyNzA0IC0yNi4yOTg0NTAxODAzNjExNjUsIDU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IE01OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMTczNjUzMzA4ODYyNTUgLTI2LjEzNDYyMTcxMjU4OTIyLCA1OC4yNDUzNzgzMjcxMTI5NiAtMjYuMDgzNDExMDMzODY1Nzk0LCA1OC40MzgyNTEwNjM0NDgzNCAtMjUuOTQ1NzAyNTQ2OTgxOTcgTTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEM1OC4xOTE4OTA1Njc1MDcwMSAtMjYuMTIxNjAwNTYwMTkzMzE1LCA1OC4yODE4NTI4NDQ0MDE4OSAtMjYuMDU3MzY4NzI5MDczOTksIDU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NyBNNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk3IEM1OC41MjYwOTM4MjY2NzgzNyAtMjUuODcxMzAzNTIxNjQxMTU2LCA1OC42MTM5MzY1ODk5MDg0MDUgLTI1Ljc5NjkwNDQ5NjMwMDM0LCA1OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBNNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk3IEM1OC41MDk4MTA0MjQ2Mjc2IC0yNS44ODUwOTQ4NTk2NzgxNiwgNTguNTgxMzY5Nzg1ODA2ODY0IC0yNS44MjQ0ODcxNzIzNzQzNDcsIDU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IE01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBDNTguODQzNDgyODQ4MjU2NDcgLTI1LjU4ODczMjA2MzIzNzg5NSwgNTguOTMzMzcwMzM4Mzg0MjMgLTI1LjQ5ODg0NDU3MzExMDEzLCA1OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IE01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBDNTguODcwMDQxMjA3OTAxOTIgLTI1LjU2MjE3MzcwMzU5MjQ0MywgNTguOTg2NDg3MDU3Njc1MTM0IC0yNS40NDU3Mjc4NTM4MTkyMzIsIDU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgTTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgQzU5LjEzMjAyMzI2ODgzMjY3NSAtMjUuMjg0NjEyNTY0ODgxNzEyLCA1OS4yMTgyMzk0ODQyOTk2OTQgLTI1LjE4MjgxNzI3MTYzNDcyLCA1OS4zMTI4OTAwNDY5ODE5NyAtMjUuMDcxMDYzNTYzNDQ4MzQgTTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgQzU5LjExMTE5MDI0MTMxNjEyIC0yNS4zMDkyMTAwNzM5OTA0NDIsIDU5LjE3NjU3MzQyOTI2NjU5IC0yNS4yMzIwMTIyODk4NTIxNzUsIDU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBNNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4zNjY3MjA5MTA1NTE5OSAtMjQuOTk1NjY4NzQ3OTg3MTQzLCA1OS40MjA1NTE3NzQxMjIwMSAtMjQuOTIwMjczOTMyNTI1OTQ0LCA1OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzNiBNNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4zOTA3NTQwMDY5MjY1NiAtMjQuOTYyMDA4MzAyMTc2ODI3LCA1OS40Njg2MTc5NjY4NzExMzYgLTI0Ljg1Mjk1MzA0MDkwNTMxMiwgNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzYgTTU5LjU1MzAxOTg5MTMxMjY0NCAtMjQuNzM0NzQwNzkwNjEyMTM2IEM1OS42MjMxMzIzMzUwOTU3MSAtMjQuNjE3MDc2OTI4MDEzNDkzLCA1OS42OTMyNDQ3Nzg4Nzg3OCAtMjQuNDk5NDEzMDY1NDE0ODUzLCA1OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzcgTTU5LjU1MzAxOTg5MTMxMjY0NCAtMjQuNzM0NzQwNzkwNjEyMTM2IEM1OS42MDA5NjkxMzYzMTM3NjUgLTI0LjY1NDI3MTU3NDcxOTc3LCA1OS42NDg5MTgzODEzMTQ4OSAtMjQuNTczODAyMzU4ODI3NDA0LCA1OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzcgTTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNyBDNTkuODMzNzk2NTI5MzcwNTQgLTI0LjIzODEwMzcyMjI4MTcxLCA1OS45MDMwMzY4MDI3MDg2MzQgLTI0LjA5NjQ3MDQ3OTM3ODA1NiwgNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBNNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM3IEM1OS44MTI4NTE0MTA5OTIxNiAtMjQuMjgwOTQ3NjQ2NzE5MjQ1LCA1OS44NjExNDY1NjU5NTE4NyAtMjQuMTgyMTU4MzI4MjUzMTIsIDU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgTTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQzU5Ljk5Nzk5OTg5MjYzNjU2IC0yMy44NzUzNTE1NTEzMzMyMjUsIDYwLjA0OTk0NTY1MTk5NzgyNCAtMjMuNzQyMjI1OTc5NDAxNiwgNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgTTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQzU5Ljk5MjQ0NTA2MDg3MDQ3IC0yMy44ODk1ODczNjUwOTIzNTMsIDYwLjAzODgzNTk4ODQ2NTY2NCAtMjMuNzcwNjk3NjA2OTE5ODYsIDYwLjA5NjI3MzcwODUwMzE3NiAtMjMuNjIzNDk3MzQ2MDIzNDE3IE02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMTIzNjI3MTk1OTcxNDE2IC0yMy41MzE2MTg2MDEyMjEwOTgsIDYwLjE1MDk4MDY4MzQzOTY2IC0yMy40Mzk3Mzk4NTY0MTg3OCwgNjAuMjE0MTg4ODI5Njk2NjUgLTIzLjIyNzQyNzQzNTcwMzk5NCBNNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgQzYwLjEzNjgzMTc1MjQ0MDk3NiAtMjMuNDg3MjY1Mjc3ODgzMzI4LCA2MC4xNzczODk3OTYzNzg3OCAtMjMuMzUxMDMzMjA5NzQzMjQsIDYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgTTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQzYwLjIzNjE2MTUzMjk3Nzk0NSAtMjMuMTIyNjM0ODA4NTYwNzQ3LCA2MC4yNTgxMzQyMzYyNTkyNCAtMjMuMDE3ODQyMTgxNDE3NSwgNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IE02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4yMzE0OTkzMTQ3MTY3OCAtMjMuMTQ0ODY5OTQ3MDI1NDQsIDYwLjI0ODgwOTc5OTczNjkgLTIzLjA2MjMxMjQ1ODM0Njg4OCwgNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IE02MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgQzYwLjMxNjY3MzgwODg0NDE4IC0yMi42ODExMzczMjk1MTY1NzcsIDYwLjMzNDM1MzYwMDY3NDc0NiAtMjIuNTM5MzAxNzA3NjI5NDgsIDYwLjM1MDEwOTk2NTAzMzM1IC0yMi40MTI4OTY3MjczNjE2NjIgTTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDNjAuMzEwMzg3NTU5MjA5NiAtMjIuNzMxNTY4NTg2NTU4NjgsIDYwLjMyMTc4MTEwMTQwNTU4NCAtMjIuNjQwMTY0MjIxNzEzNjkzLCA2MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYyIE02MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYyIEM2MC4zNTY3MzA5NDkwMDU5IC0yMi4yNTI4MTYwOTM5NzI5MDcsIDYwLjM2MzM1MTkzMjk3ODQ0NiAtMjIuMDkyNzM1NDYwNTg0MTQ4LCA2MC4zNjcxODc1IC0yMiBNNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzUzOTgyMzQwMjUyODcgLTIyLjMxOTI3MTMyNjYwMDk0LCA2MC4zNTc4NTQ3MTU0NzI0IC0yMi4yMjU2NDU5MjU4NDAyMiwgNjAuMzY3MTg3NSAtMjIgTTYwLjM2NzE4NzUgLTIyIEM2MC4zNjcxODc1IC0yMiwgNjAuMzY3MTg3NSAtMjIsIDYwLjM2NzE4NzUgLTIyIE02MC4zNjcxODc1IC0yMiBDNjAuMzY3MTg3NSAtMjIsIDYwLjM2NzE4NzUgLTIyLCA2MC4zNjcxODc1IC0yMiBNNjAuMzY3MTg3NSAtMjIgQzYwLjM2NzE4NzUgLTEwLjM4OTE3MTUxNDIyNDUzMiwgNjAuMzY3MTg3NSAxLjIyMTY1Njk3MTU1MDkzNTEsIDYwLjM2NzE4NzUgMjIgTTYwLjM2NzE4NzUgLTIyIEM2MC4zNjcxODc1IC0xMC45NTU2NjIwMjIwMzk5OTcsIDYwLjM2NzE4NzUgMC4wODg2NzU5NTU5MjAwMDY1NCwgNjAuMzY3MTg3NSAyMiBNNjAuMzY3MTg3NSAyMiBDNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiBNNjAuMzY3MTg3NSAyMiBDNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiwgNjAuMzY3MTg3NSAyMiBNNjAuMzY3MTg3NSAyMiBDNjAuMzYyOTg3NDI5NzQ2ODEgMjIuMTAxNTQ4MzM2MTk4MjEzLCA2MC4zNTg3ODczNTk0OTM2MjYgMjIuMjAzMDk2NjcyMzk2NDMsIDYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNNjAuMzY3MTg3NSAyMiBDNjAuMzYzMTk3NDcwMDg2NTA0IDIyLjA5NjQ3MDAyODk5Mjc1LCA2MC4zNTkyMDc0NDAxNzMwMSAyMi4xOTI5NDAwNTc5ODU1MDYsIDYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIEM2MC4zMzAwMTYwODE0MTExOSAyMi41NzQwOTkzMzEyNzE3NCwgNjAuMzA5OTIyMTk3Nzg5MDM1IDIyLjczNTMwMTkzNTE4MTgxMywgNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgTTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzMyNzE3NDc5NDU0ODIgMjIuNTUyNDI3NDQzMDk4MjUsIDYwLjMxNTMyNDk5Mzg3NjI5IDIyLjY5MTk1ODE1ODgzNDg0LCA2MC4yOTg5OTQwMTcwMTM2MSAyMi44MjI5NzI5NTE0MDM2NyBNNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQzYwLjI3NjIzMTM1Mzg3MjA2IDIyLjkzMTUzMzA2OTg3NjI0NSwgNjAuMjUzNDY4NjkwNzMwNTEgMjMuMDQwMDkzMTg4MzQ4ODE2LCA2MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgTTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEM2MC4yNzcyOTI1Nzc2OTc2IDIyLjkyNjQ3MTg2MTM3Mzk3NiwgNjAuMjU1NTkxMTM4MzgxNTggMjMuMDI5OTcwNzcxMzQ0MjgsIDYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBNNjAuMjE0MTg4ODI5Njk2NjUgMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4xNzg5NzE4ODQzMjMyMiAyMy4zNDU3MTkwNzAwMTM0OTQsIDYwLjE0Mzc1NDkzODk0OTc5IDIzLjQ2NDAxMDcwNDMyMjk5LCA2MC4wOTYyNzM3MDg1MDMxNzYgMjMuNjIzNDk3MzQ2MDIzNDE3IE02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQzYwLjE3NzA4NTY1MzM3NzczIDIzLjM1MjA1NDgwODA3NTQyLCA2MC4xMzk5ODI0NzcwNTg4MSAyMy40NzY2ODIxODA0NDY4NDcsIDYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgTTYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQzYwLjA2MzM2NzU4NjA0NTQ2NiAyMy43MDc4Mjg1MTA0MTY4MSwgNjAuMDMwNDYxNDYzNTg3NzU2IDIzLjc5MjE1OTY3NDgxMDIsIDU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBNNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMDQyMTU5NDc4NjU3ODIgMjMuNzYyMTgwMjMxNTI1MiwgNTkuOTg4MDQ1MjQ4ODEyNDcgMjMuOTAwODYzMTE3MDI2OTgzLCA1OS45NDYwNTQxMzMyNzUyOSAyNC4wMDg0NzcxMjMyNjQ4NDYgTTU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuODg5MTA1NTc1NDIyNjEgMjQuMTI0OTY3MjYwMjY5MDM1LCA1OS44MzIxNTcwMTc1Njk5MjYgMjQuMjQxNDU3Mzk3MjczMjIzLCA1OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgTTU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuOTAwNDc0Njg2NTEzNzYgMjQuMTAxNzExMzcxODIyMywgNTkuODU0ODk1MjM5NzUyMjQgMjQuMTk0OTQ1NjIwMzc5NzU1LCA1OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgTTU5Ljc2NDU1NjI1NjAzMjQ1IDI0LjM3OTczNjk2NTE4NTM2NiBDNTkuNzEzNjQxNjIyNTI1NTggMjQuNDY1MTgyNzQ1MTA2MzgyLCA1OS42NjI3MjY5ODkwMTg3MSAyNC41NTA2Mjg1MjUwMjc0LCA1OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIE01OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgQzU5LjY4OTUxMzkzNzYxMDk1NCAyNC41MDU2NzQyMjQ5MjcwMywgNTkuNjE0NDcxNjE5MTg5NDUgMjQuNjMxNjExNDg0NjY4NjkyLCA1OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIE01OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIEM1OS40NzAwNzQ1ODc5ODEzOCAyNC44NTA5MTI5MTYxMDQ3NzUsIDU5LjM4NzEyOTI4NDY1MDExIDI0Ljk2NzA4NTA0MTU5NzQyLCA1OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBNNTkuNTUzMDE5ODkxMzEyNjQ0IDI0LjczNDc0MDc5MDYxMjEzMyBDNTkuNDg3MTkzNjE4OTYzNTY2IDI0LjgyNjkzNjIyMTM5NTA5OCwgNTkuNDIxMzY3MzQ2NjE0NDkgMjQuOTE5MTMxNjUyMTc4MDY2LCA1OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBNNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQzU5LjI1NzY2OTMyNDY0ODMyNiAyNS4xMzYyNjI1NDcxMDYwNTMsIDU5LjIwMjQ0ODYwMjMxNDY4IDI1LjIwMTQ2MTUzMDc2Mzc2NSwgNTkuMDQ1ODA3MDUzMzY1NjYgMjUuMzg2NDA3ODU4MTI4NzA2IE01OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBDNTkuMjExMjI3MzQzNTcyODIgMjUuMTkxMDk2NDkwNTQ2MzIsIDU5LjEwOTU2NDY0MDE2MzY3NiAyNS4zMTExMjk0MTc2NDQzLCA1OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgTTU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBDNTguOTM4NzA5MzQ0NzMyODMgMjUuNDkzNTA1NTY2NzYxNTM0LCA1OC44MzE2MTE2MzYxIDI1LjYwMDYwMzI3NTM5NDM2MywgNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBNNTkuMDQ1ODA3MDUzMzY1NjYgMjUuMzg2NDA3ODU4MTI4NzA2IEM1OC45MzYzMjM5ODExODM2OSAyNS40OTU4OTA5MzAzMTA2NzIsIDU4LjgyNjg0MDkwOTAwMTcyNCAyNS42MDUzNzQwMDI0OTI2NCwgNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBNNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBDNTguNjc1NDMwMTIwNzQzMzIgMjUuNzQ0ODIyMTMyNDE5MDYzLCA1OC41OTcyNjQ4ODMzNTc5NCAyNS44MTEwMjQ3MTE0NzI0NzQsIDU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IE01OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjU3IEM1OC42Njk3NTAwNDg2MjYxOCAyNS43NDk2MzI5MDgxMDMyOCwgNTguNTg1OTA0NzM5MTIzNjQgMjUuODIwNjQ2MjYyODQwOTAzLCA1OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBNNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjM1ODc4NjE5NjMyNjY3IDI2LjAwMjQzOTM3MTU4MjgzNSwgNTguMjc5MzIxMzI5MjA1IDI2LjA1OTE3NjE5NjE4MzcwMywgNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBNNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjM2MDgyODk3NTc0MzgzNiAyNi4wMDA5ODA4NTUxMTI3NSwgNTguMjgzNDA2ODg4MDM5MzMgMjYuMDU2MjU5MTYzMjQzNTM1LCA1OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IE01OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IEM1Ny45NzM1NTYxMjY4OTg1NSAyNi4yNjIzMjU1OTc1Nzc1MSwgNTcuODQ1MTgzOTYzMTg0OTY1IDI2LjMzODgxODgwMzg0MjM4MywgNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBNNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMDI3NTc5OTQ3MDEzMzEgMjYuMjMwMTM0Mzg4MDgzODgzLCA1Ny45NTMyMzE2MDM0MTQ0OCAyNi4yNzQ0MzYzODQ4NTUxMjUsIDU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgTTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgQzU3LjYyMDQxMDYxMDE0MzQ0IDI2LjQ1OTIxNzYxMTAzNjg5OCwgNTcuNDkzODk2NzU1MTAxNTIgMjYuNTIxMDY2NDY2MDQxMzUsIDU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgTTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgQzU3LjY0MjUxNzk3NDcwMTAyIDI2LjQ0ODQwOTk3OTA2MTczMywgNTcuNTM4MTExNDg0MjE2NjggMjYuNDk5NDUxMjAyMDkxMDIsIDU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgTTU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgQzU3LjI4NTE1MjMyMzM5MjcxIDI2LjYxNDE4NDY0MjI4MTE2LCA1Ny4xOTQ2NDAwMjM1MjA1NyAyNi42NDk1MDI2NTEyODcwMzMsIDU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBNNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuMjg2NDI3OTE5MjY2NzkgMjYuNjEzNjg2OTAzMjI2NTUsIDU3LjE5NzE5MTIxNTI2ODc0IDI2LjY0ODUwNzE3MzE3NzgxNiwgNTYuOTkwNjg0ODQ2MDIzNDIgMjYuNzI5MDg2MjA4NTAzMTczIE01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQzU2Ljg4NDIzMDIxMDE5OTY0IDI2Ljc2MDc3OTEyNjc0MjM5MiwgNTYuNzc3Nzc1NTc0Mzc1ODY2IDI2Ljc5MjQ3MjA0NDk4MTYxLCA1Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQzU2Ljg3NzI1NTQyMTI0MTQxNSAyNi43NjI4NTU2MTEzODI4MDgsIDU2Ljc2MzgyNTk5NjQ1OTQxIDI2Ljc5NjYyNTAxNDI2MjQ0LCA1Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIE01Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIEM1Ni41MDYxNTUwMjAwMDczIDI2Ljg2NTU0OTQyMzUxNjYsIDU2LjQxNzY5NTEwNDMxMDYgMjYuODg0MDk3NTE3MzM2NTQ2LCA1Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgTTU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgQzU2LjQ3ODc1NDU5NDg0Nzk3IDI2Ljg3MTI5NDY4ODUwNTM3LCA1Ni4zNjI4OTQyNTM5OTE5NCAyNi44OTU1ODgwNDczMTQwODUsIDU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBNNTYuMTkwMTYwNDUxNDAzNjcgMjYuOTMxODA2NTE3MDEzNjEyIEM1Ni4wNDg4Njg5MjU1Mzg2MzUgMjYuOTQ5NDE4NDg3MzQ1NzYsIDU1LjkwNzU3NzM5OTY3MzU5IDI2Ljk2NzAzMDQ1NzY3NzkwNCwgNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IE01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQzU2LjA3Nzk5NTA0Mzk5MzAyIDI2Ljk0NTc4NzkyMDQ2NjYxNCwgNTUuOTY1ODI5NjM2NTgyMzggMjYuOTU5NzY5MzIzOTE5NjE2LCA1NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgTTU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBDNTUuNjgwNTI4MDE2NjgyNzg2IDI2Ljk4NzA0MDE0MDM2NTU4LCA1NS41ODA5NzE4MDYwMDM5MSAyNi45OTExNTc4MTU2OTc4MDcsIDU1LjM2NzE4NzUgMjcgTTU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBDNTUuNjgyMjQ3Nzc4OTM5NjMgMjYuOTg2OTY5MDEwNDcyNDg1LCA1NS41ODQ0MTEzMzA1MTc2IDI2Ljk5MTAxNTU1NTkxMTYyMywgNTUuMzY3MTg3NSAyNyBNNTUuMzY3MTg3NSAyNyBDNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNyBNNTUuMzY3MTg3NSAyNyBDNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNyBNNTUuMzY3MTg3NSAyNyBDMTEuMzI3ODIxNDU3NTYzMjEgMjcsIC0zMi43MTE1NDQ1ODQ4NzM1OCAyNywgLTU1LjM2NzE4NzUgMjcgTTU1LjM2NzE4NzUgMjcgQzI3LjkyOTA3NDcyNTYwMTY5NCAyNywgMC40OTA5NjE5NTEyMDMzODg1IDI3LCAtNTUuMzY3MTg3NSAyNyBNLTU1LjM2NzE4NzUgMjcgQy01NS4zNjcxODc1IDI3LCAtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcgTS01NS4zNjcxODc1IDI3IEMtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcsIC01NS4zNjcxODc1IDI3IE0tNTUuMzY3MTg3NSAyNyBDLTU1LjUwNjM2NTY3MjAzNTM3IDI2Ljk5NDI0MzU0ODIyMzk5LCAtNTUuNjQ1NTQzODQ0MDcwNzQ0IDI2Ljk4ODQ4NzA5NjQ0Nzk4MywgLTU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBNLTU1LjM2NzE4NzUgMjcgQy01NS40NTg4OTU3Mzg3MDc1OSAyNi45OTYyMDY5MTkxODk1MzYsIC01NS41NTA2MDM5Nzc0MTUxOCAyNi45OTI0MTM4MzgzNzkwNzIsIC01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgTS01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgQy01NS45MDE2Nzc1Nzc2MzU5ODYgMjYuOTY3NzY1ODY5NzI3OTU1LCAtNTYuMDIzMjcwOTI3OTEwMzEgMjYuOTUyNjA5Mjc0NDIyNTY0LCAtNTYuMTkwMTYwNDUxNDAzNjcgMjYuOTMxODA2NTE3MDEzNjEyIE0tNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuOTAwOTczNjAzOTE5NTUgMjYuOTY3ODUzNjE5OTYwMiwgLTU2LjAyMTg2Mjk4MDQ3NzQ1IDI2Ljk1Mjc4NDc3NDg4NzA0NiwgLTU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBNLTU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBDLTU2LjMxNTY0MTA5MTE2OTA0IDI2LjkwNTQ5NTk5MzY0NjkwMiwgLTU2LjQ0MTEyMTczMDkzNDQyIDI2Ljg3OTE4NTQ3MDI4MDE5LCAtNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBNLTU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBDLTU2LjMxNDc1NTQ3Mzg3OTQ1IDI2LjkwNTY4MTY4ODA2NTA2MiwgLTU2LjQzOTM1MDQ5NjM1NTIzIDI2Ljg3OTU1Njg1OTExNjUxNiwgLTU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgTS01Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNjg3MDE1MzM3MzM2Mzc2IDI2LjgxOTQ5MjUzODI2MTI3LCAtNTYuNzc5NDE1NzM4OTY4NzUgMjYuNzkxOTgzNzQ2ODI1ODg1LCAtNTYuOTkwNjg0ODQ2MDIzNDIgMjYuNzI5MDg2MjA4NTAzMTczIE0tNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBDLTU2LjcyNTE2MDU2ODg2MDQ3IDI2LjgwODEzNjIxMDgwMjU5LCAtNTYuODU1NzA2MjAyMDE2OTQgMjYuNzY5MjcxMDkxOTA4NTMsIC01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgTS01Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgQy01Ny4wOTcxODQ2Mjk0Njc2NiAyNi42ODc1Mjk4NjQwNDkyMDcsIC01Ny4yMDM2ODQ0MTI5MTE5IDI2LjY0NTk3MzUxOTU5NTI0LCAtNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBNLTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDLTU3LjEwNTEzODQ2NjY2ODc5IDI2LjY4NDQyNjI2NzE0MjUxMywgLTU3LjIxOTU5MjA4NzMxNDE3IDI2LjYzOTc2NjMyNTc4MTg1NiwgLTU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgTS01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNDcxNTI4MzU1NDc4OTggMjYuNTMyMDAxNzEwMjgzNzg3LCAtNTcuNTY3MzkyMDg3NjkzMTE1IDI2LjQ4NTEzNjc4NzI5MjI4NywgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgTS01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNTE2ODAxODczNjk3NTk1IDI2LjUwOTg2ODgzNTg4MTA4NiwgLTU3LjY1NzkzOTEyNDEzMDM0NCAyNi40NDA4NzEwMzg0ODY4ODMsIC01Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBDLTU3Ljg1MzU5NjQ4MzIwMjM0NSAyNi4zMzM4MDYwMjk5NjQ2MDYsIC01Ny45NjAyNjg1MDEyMTkzMiAyNi4yNzAyNDMzMDM4OTY3NywgLTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgTS01Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IEMtNTcuODgwNTI4MDEwMTg2MjEgMjYuMzE3NzU4MzIzMTMzMDc3LCAtNTguMDE0MTMxNTU1MTg3MDUgMjYuMjM4MTQ3ODkwMjMzNzEyLCAtNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBNLTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgQy01OC4yMTQ4OTAxOTE0OTQ5NiAyNi4xMDUxNzkxNDQyOTc3NiwgLTU4LjMyNzg1MjA5MjM3Nzc4IDI2LjAyNDUyNTg5NzI4Mjg3NCwgLTU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IE0tNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBDLTU4LjE4OTIyNDkxNjM4MjUyIDI2LjEyMzUwMzc5ODUxNDU0NSwgLTU4LjI3NjUyMTU0MjE1MjkgMjYuMDYxMTc1MjA1NzE2NDQ1LCAtNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgTS01OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBDLTU4LjUwNzEyNzYxOTg1MTQ0IDI1Ljg4NzM2NzA3OTQxNzAxLCAtNTguNTc2MDA0MTc2MjU0NTM2IDI1LjgyOTAzMTYxMTg1MjA1MiwgLTU4Ljc1MzU5NTM1ODEyODcwNiAyNS42Nzg2MTk1NTMzNjU2NiBNLTU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IEMtNTguNTAyMzA5MTcwNTg4MDIgMjUuODkxNDQ4MDk3NzY5MjQyLCAtNTguNTY2MzY3Mjc3NzI3NjkgMjUuODM3MTkzNjQ4NTU2NTE0LCAtNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IE0tNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IEMtNTguODQ5MTc1MzE1MTMwNzEgMjUuNTgzMDM5NTk2MzYzNjU1LCAtNTguOTQ0NzU1MjcyMTMyNzIgMjUuNDg3NDU5NjM5MzYxNjUsIC01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgTS01OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjYgQy01OC44Mjc0MzgwNDE3MjIwOSAyNS42MDQ3NzY4Njk3NzIyNzcsIC01OC45MDEyODA3MjUzMTU0NyAyNS41MzA5MzQxODYxNzg4OTMsIC01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgTS01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQy01OS4xMjI2OTYyMDk4NzQwMSAyNS4yOTU2MjUwMDI1ODQ4MywgLTU5LjE5OTU4NTM2NjM4MjM2NSAyNS4yMDQ4NDIxNDcwNDA5NiwgLTU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IE0tNTkuMDQ1ODA3MDUzMzY1NjYgMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTkuMTQxNjE4Nzc4ODUzOTc0IDI1LjI3MzI4MzE2NzYwNzg4LCAtNTkuMjM3NDMwNTA0MzQyMjkgMjUuMTYwMTU4NDc3MDg3MDQ4LCAtNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgTS01OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBDLTU5LjM3NTI5MjQ2ODA4Nzc0IDI0Ljk4MzY2MzUzNDY5OTM3LCAtNTkuNDM3Njk0ODg5MTkzNTIgMjQuODk2MjYzNTA1OTUwMzk3LCAtNTkuNTUzMDE5ODkxMzEyNjQ0IDI0LjczNDc0MDc5MDYxMjEzMyBNLTU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IEMtNTkuMzk3MTgzNjMwNjgwNjcgMjQuOTUzMDAzMDUzODA2Njk3LCAtNTkuNDgxNDc3MjE0Mzc5Mzc1IDI0LjgzNDk0MjU0NDE2NTA1MiwgLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgTS01OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIEMtNTkuNjI4NTA1MjE3NzY2NzQgMjQuNjA4MDYwMDY3NDY0MTg3LCAtNTkuNzAzOTkwNTQ0MjIwODMgMjQuNDgxMzc5MzQ0MzE2MjQsIC01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBNLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQy01OS42MzQxNTIxMDU2ODMzMiAyNC41OTg1ODMzNjY3ODA2MDIsIC01OS43MTUyODQzMjAwNTM5OTYgMjQuNDYyNDI1OTQyOTQ5MDc1LCAtNTkuNzY0NTU2MjU2MDMyNDQgMjQuMzc5NzM2OTY1MTg1MzcgTS01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBDLTU5LjgxNTk2MzgwNzkyMTE2NSAyNC4yNzQ1ODExMzcwNDIwNCwgLTU5Ljg2NzM3MTM1OTgwOTg4IDI0LjE2OTQyNTMwODg5ODcxMywgLTU5Ljk0NjA1NDEzMzI3NTI4IDI0LjAwODQ3NzEyMzI2NDg1IE0tNTkuNzY0NTU2MjU2MDMyNDQgMjQuMzc5NzM2OTY1MTg1MzcgQy01OS44MTE1MjQyNDQyMzQzMDYgMjQuMjgzNjYyNDA5NzEzNzUyLCAtNTkuODU4NDkyMjMyNDM2MTYgMjQuMTg3NTg3ODU0MjQyMTM1LCAtNTkuOTQ2MDU0MTMzMjc1MjggMjQuMDA4NDc3MTIzMjY0ODUgTS01OS45NDYwNTQxMzMyNzUyOCAyNC4wMDg0NzcxMjMyNjQ4NSBDLTU5Ljk4NzUxMTgzNjA5NDk2IDIzLjkwMjIzMDEzNjY2Mzc1LCAtNjAuMDI4OTY5NTM4OTE0NjQgMjMuNzk1OTgzMTUwMDYyNjU0LCAtNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBNLTU5Ljk0NjA1NDEzMzI3NTI4IDI0LjAwODQ3NzEyMzI2NDg1IEMtNTkuOTgwMzc3NjQ0OTM4NDY0IDIzLjkyMDUxMzUwMTY1MTY5OCwgLTYwLjAxNDcwMTE1NjYwMTY0IDIzLjgzMjU0OTg4MDAzODU1LCAtNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBNLTYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQy02MC4xMzY4Mzk3NTE1MDkyIDIzLjQ4NzIzODQwOTQ4NjgwMywgLTYwLjE3NzQwNTc5NDUxNTIyIDIzLjM1MDk3OTQ3Mjk1MDE5LCAtNjAuMjE0MTg4ODI5Njk2NjUgMjMuMjI3NDI3NDM1NzAzOTk0IE0tNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBDLTYwLjEyOTQwMDYxMjk3NTg1IDIzLjUxMjIyNjAzNTM0Nzk2NSwgLTYwLjE2MjUyNzUxNzQ0ODUzIDIzLjQwMDk1NDcyNDY3MjUxMywgLTYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBNLTYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBDLTYwLjIzNDk5NDY3NTE0NjMzIDIzLjEyODE5OTgwODczOTAxLCAtNjAuMjU1ODAwNTIwNTk2MDEgMjMuMDI4OTcyMTgxNzc0MDIzLCAtNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgTS02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4yMzI0Mjg1MDMwNzQ3MSAyMy4xNDA0Mzg0NDQ1NDI5NCwgLTYwLjI1MDY2ODE3NjQ1Mjc1NiAyMy4wNTM0NDk0NTMzODE4ODQsIC02MC4yOTg5OTQwMTcwMTM2MSAyMi44MjI5NzI5NTE0MDM2NyBNLTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMzE3MTQyNzY3NDEwNDUgMjIuNjc3Mzc1MTIyODk1MzM4LCAtNjAuMzM1MjkxNTE3ODA3MyAyMi41MzE3NzcyOTQzODcwMSwgLTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNLTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMzE4MjcyODkwMjUxNDYgMjIuNjY4MzA4NzQ0ODgwMzYsIC02MC4zMzc1NTE3NjM0ODkyOTQgMjIuNTEzNjQ0NTM4MzU3MDUsIC02MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgTS02MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgQy02MC4zNTQ0NjI2NDExMjkwMjUgMjIuMzA3NjU4NzIyMDcxMDU3LCAtNjAuMzU4ODE1MzE3MjI0NyAyMi4yMDI0MjA3MTY3ODA0NTUsIC02MC4zNjcxODc1IDIyIE0tNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIEMtNjAuMzU1OTI0NTY4NjAxNjcgMjIuMjcyMzEyNTc0NjE2MjIsIC02MC4zNjE3MzkxNzIxNyAyMi4xMzE3Mjg0MjE4NzA3OCwgLTYwLjM2NzE4NzUgMjIgTS02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSAyMiwgLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyIE0tNjAuMzY3MTg3NSAyMiBDLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyLCAtNjAuMzY3MTg3NSAyMiBNLTYwLjM2NzE4NzUgMjIgQy02MC4zNjcxODc1IDUuMjA5NjMxOTkzOTAxNzg3LCAtNjAuMzY3MTg3NSAtMTEuNTgwNzM2MDEyMTk2NDI1LCAtNjAuMzY3MTg3NSAtMjIgTS02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSA1LjM3NDAwMTYwMTcxOTE0NCwgLTYwLjM2NzE4NzUgLTExLjI1MTk5Njc5NjU2MTcxMywgLTYwLjM2NzE4NzUgLTIyIE0tNjAuMzY3MTg3NSAtMjIgQy02MC4zNjcxODc1IC0yMiwgLTYwLjM2NzE4NzUgLTIyLCAtNjAuMzY3MTg3NSAtMjIgTS02MC4zNjcxODc1IC0yMiBDLTYwLjM2NzE4NzUgLTIyLCAtNjAuMzY3MTg3NSAtMjIsIC02MC4zNjcxODc1IC0yMiBNLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYzNjY5MjAwNjY1OTUgLTIyLjA4NTA2NDYzNTEzMjkxNiwgLTYwLjM2MDE1MDkwMTMzMTkgLTIyLjE3MDEyOTI3MDI2NTgzNiwgLTYwLjM1MDEwOTk2NTAzMzM1IC0yMi40MTI4OTY3MjczNjE2NiBNLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYzNTM4MjU1NzU1MDk1IC0yMi4wODgyMzA1OTExODM0NDYsIC02MC4zNTk4ODkwMTE1MTAxOSAtMjIuMTc2NDYxMTgyMzY2ODkzLCAtNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IE0tNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IEMtNjAuMzM5NDk4MzUwMzU0NDMgLTIyLjQ5ODAyODEwMTE3MTYsIC02MC4zMjg4ODY3MzU2NzU1MiAtMjIuNTgzMTU5NDc0OTgxNTQsIC02MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgTS02MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYgQy02MC4zMzIyMTI0MjYzNTk4NzQgLTIyLjU1NjQ3OTIxNzAzODQ1OCwgLTYwLjMxNDMxNDg4NzY4NjQgLTIyLjcwMDA2MTcwNjcxNTI1NywgLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBNLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDLTYwLjI4MDc4NTQ4MjM1NjQ4NSAtMjIuOTA5ODEzNDM1MTk0OTMsIC02MC4yNjI1NzY5NDc2OTkzNiAtMjIuOTk2NjUzOTE4OTg2MTk0LCAtNjAuMjE0MTg4ODI5Njk2NjUgLTIzLjIyNzQyNzQzNTcwMzk5NCBNLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDLTYwLjI3NzUwMDA0MTE5MzMzNSAtMjIuOTI1NDgyNDIyNTkyNzc2LCAtNjAuMjU2MDA2MDY1MzczMDYgLTIzLjAyNzk5MTg5Mzc4MTg3NywgLTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgTS02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEMtNjAuMTc3OTEwMTYyODQ1NTMgLTIzLjM0OTI4NTMyOTU5MjM5NCwgLTYwLjE0MTYzMTQ5NTk5NDQxIC0yMy40NzExNDMyMjM0ODA3OTQsIC02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBNLTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4xNzE0MTMxNjc3OTUwMSAtMjMuMzcxMTA4MzUxMjc5MTksIC02MC4xMjg2Mzc1MDU4OTMzNiAtMjMuNTE0Nzg5MjY2ODU0Mzg2LCAtNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgTS02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDLTYwLjA0MzQ4Nzg1NjM4Nzk1IC0yMy43NTg3NzU4OTExNjYzNjIsIC01OS45OTA3MDIwMDQyNzI3MiAtMjMuODk0MDU0NDM2MzA5MzA3LCAtNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBNLTYwLjA5NjI3MzcwODUwMzE3NiAtMjMuNjIzNDk3MzQ2MDIzNDE3IEMtNjAuMDQ2Nzc2ODI4OTExNCAtMjMuNzUwMzQ2OTc2OTc1MTgsIC01OS45OTcyNzk5NDkzMTk2MiAtMjMuODc3MTk2NjA3OTI2OTQ4LCAtNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBNLTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQy01OS44ODA5NTcwOTAwOTc4NDQgLTI0LjE0MTYzNTI1MzQxMzE5NSwgLTU5LjgxNTg2MDA0NjkyMDQgLTI0LjI3NDc5MzM4MzU2MTU0OCwgLTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNjYgTS01OS45NDYwNTQxMzMyNzUyOSAtMjQuMDA4NDc3MTIzMjY0ODQ2IEMtNTkuODgzOTMyMDkxMTM3NzIgLTI0LjEzNTU0OTc5MTUxOTMyOCwgLTU5LjgyMTgxMDA0OTAwMDE2IC0yNC4yNjI2MjI0NTk3NzM4MDUsIC01OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzY2IE0tNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM2NiBDLTU5LjcwMTIzNDY5OTYwMzM4IC0yNC40ODYwMDQyNDgzMjYzNiwgLTU5LjYzNzkxMzE0MzE3NDMxNiAtMjQuNTkyMjcxNTMxNDY3MzUsIC01OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzMyBNLTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNjYgQy01OS43MDg2OTYxNDMzNjY2NiAtMjQuNDczNDgyMzMwMDEzNzM4LCAtNTkuNjUyODM2MDMwNzAwODcgLTI0LjU2NzIyNzY5NDg0MjExLCAtNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzMgTS01OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzMyBDLTU5LjUwNDYwNzU5MTM5NDYyNCAtMjQuODAyNTQ2NDM1NDgzOTMzLCAtNTkuNDU2MTk1MjkxNDc2NjA0IC0yNC44NzAzNTIwODAzNTU3MywgLTU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBNLTU5LjU1MzAxOTg5MTMxMjY0NCAtMjQuNzM0NzQwNzkwNjEyMTMzIEMtNTkuNDYxNzM5MzQwODUyMjEgLTI0Ljg2MjU4NzE1NjA3NTExLCAtNTkuMzcwNDU4NzkwMzkxNzggLTI0Ljk5MDQzMzUyMTUzODA5LCAtNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IE0tNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEMtNTkuMjE4MTkwNTgwNzAyODc0IC0yNS4xODI4NzUwMTIwMDIzNzMsIC01OS4xMjM0OTExMTQ0MjM3ODYgLTI1LjI5NDY4NjQ2MDU1NjQwNiwgLTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgTS01OS4zMTI4OTAwNDY5ODE5NyAtMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4yMjI2MDEwMDQ0NjUyMiAtMjUuMTc3NjY3NjM0NTA2MTY4LCAtNTkuMTMyMzExOTYxOTQ4NDcgLTI1LjI4NDI3MTcwNTU2NCwgLTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgTS01OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTguOTMxNzg1Njk1MjEzNzU1IC0yNS41MDA0MjkyMTYyODA2MDgsIC01OC44MTc3NjQzMzcwNjE4NSAtMjUuNjE0NDUwNTc0NDMyNTE0LCAtNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgTS01OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTguOTcwMTE5MjM1NDczNjYgLTI1LjQ2MjA5NTY3NjAyMDcwNiwgLTU4Ljg5NDQzMTQxNzU4MTY2IC0yNS41Mzc3ODM0OTM5MTI3MDIsIC01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBNLTU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IEMtNTguNjgyMjg2MDcwNDgyNzYgLTI1LjczOTAxNTQzOTE5MDgsIC01OC42MTA5NzY3ODI4MzY4MTUgLTI1Ljc5OTQxMTMyNTAxNTk0NiwgLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgTS01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBDLTU4LjYzMjY3OTcwNTA1MjQ0NiAtMjUuNzgxMDI5ODg3NTQxNTUsIC01OC41MTE3NjQwNTE5NzYxOSAtMjUuODgzNDQwMjIxNzE3NDQ2LCAtNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk2NiBNLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgQy01OC4zMTQxNDQyNzEwMDEzNTQgLTI2LjAzNDMxMzA5MzYyODQ0NSwgLTU4LjE5MDAzNzQ3ODU1NDM2IC0yNi4xMjI5MjM2NDAyNzQ5MjMsIC01OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBNLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgQy01OC4zMjgzMTEyOTA1MjI5NSAtMjYuMDI0MTk4MDM2MTA4OTYsIC01OC4yMTgzNzE1MTc1OTc1NTQgLTI2LjEwMjY5MzUyNTIzNTk1MywgLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IE0tNTguMTAxOTI4MjkwNjEyMTM2IC0yNi4xODU4MzIzOTEzMTI2NDQgQy01Ny45NjYxNjgzOTcwNzc4NjUgLTI2LjI2NjcyNzcyOTA0MTY0OCwgLTU3LjgzMDQwODUwMzU0MzU5NSAtMjYuMzQ3NjIzMDY2NzcwNjUsIC01Ny43NDY5MjQ0NjUxODUzNjYgLTI2LjM5NzM2ODc1NjAzMjQ0NiBNLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTguMDA3NzU4MjQzMjkyODYgLTI2LjI0MTk0NTU1OTgxNTcyNiwgLTU3LjkxMzU4ODE5NTk3MzU4IC0yNi4yOTgwNTg3MjgzMTg4MSwgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MjExMjgwMDk0NTA2NCAtMjYuNDU4ODY2ODk1ODg2MjQsIC01Ny40OTUzMzE1NTM3MTU5MTQgLTI2LjUyMDM2NTAzNTc0MDAzNSwgLTU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MDY4ODkwODc4NTgyOCAtMjYuNDY1ODI3ODgwNDg5MDIzLCAtNTcuNDY2ODUzNzEwNTMxMTkgLTI2LjUzNDI4NzAwNDk0NTYsIC01Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBNLTU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuMjIzODIwNDkwODE4MzYgLTI2LjYzODExNjM5NzYxNDYwMywgLTU3LjA3MTk3NjM1ODM3MTg4IC0yNi42OTczNjYxNjE5NTM5MjMsIC01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIE0tNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQy01Ny4yNDQ5MzM4NDM5MDY1OCAtMjYuNjI5ODc3OTQxNjc0NzI0LCAtNTcuMTE0MjAzMDY0NTQ4MzEgLTI2LjY4MDg4OTI1MDA3NDE2MywgLTU2Ljk5MDY4NDg0NjAyMzQyIC0yNi43MjkwODYyMDg1MDMxNzMgTS01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEMtNTYuODc5MjE4NjQ4MDM0OTMgLTI2Ljc2MjI3MTEzMzQ0Mjc4OCwgLTU2Ljc2Nzc1MjQ1MDA0NjQ0IC0yNi43OTU0NTYwNTgzODI0LCAtNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgTS01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEMtNTYuODkzMDQzMzQ2MTkwNTk1IC0yNi43NTgxNTUzNDI0Nzc1NjUsIC01Ni43OTU0MDE4NDYzNTc3NiAtMjYuNzg3MjI0NDc2NDUxOTU3LCAtNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgTS01Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBDLTU2LjQ0NjYxODI5MjY5MDU3IC0yNi44NzgwMzI5NjI0NzY2NiwgLTU2LjI5ODYyMTY0OTY3NzE0NSAtMjYuOTA5MDY0NTk1MjU2NjY2LCAtNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBNLTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNDQ2NjE3NzEzOTc0NjkgLTI2Ljg3ODAzMzA4MzgyMDYyLCAtNTYuMjk4NjIwNDkyMjQ1MzkgLTI2LjkwOTA2NDgzNzk0NDU4NCwgLTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgTS01Ni4xOTAxNjA0NTE0MDM2NyAtMjYuOTMxODA2NTE3MDEzNjEyIEMtNTYuMDgxMzQxNzUzNTI1MjIgLTI2Ljk0NTM3MDc1MzU1ODQ3NSwgLTU1Ljk3MjUyMzA1NTY0Njc4IC0yNi45NTg5MzQ5OTAxMDMzMzcsIC01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IE0tNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBDLTU2LjA4NzE2NTYxNzkwMDI3NiAtMjYuOTQ0NjQ0ODA5NjIxMTg4LCAtNTUuOTg0MTcwNzg0Mzk2ODggLTI2Ljk1NzQ4MzEwMjIyODc2NCwgLTU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgTS01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuNjYwMjczMzEzNzI0MjQgLTI2Ljk4Nzg3Nzg4MTA3NjczNCwgLTU1LjU0MDQ2MjQwMDA4NjgyNCAtMjYuOTkyODMzMjk3MTIwMTE4LCAtNTUuMzY3MTg3NSAtMjcgTS01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuNjg0ODA4ODg5NjgwMDg1IC0yNi45ODY4NjMwODIxNDg3MDUsIC01NS41ODk1MzM1NTE5OTg1MTQgLTI2Ljk5MDgwMzY5OTI2NDA2MywgLTU1LjM2NzE4NzUgLTI3IE0tNTUuMzY3MTg3NSAtMjcgQy01NS4zNjcxODc1IC0yNywgLTU1LjM2NzE4NzUgLTI3LCAtNTUuMzY3MTg3NSAtMjcgTS01NS4zNjcxODc1IC0yNyBDLTU1LjM2NzE4NzUgLTI3LCAtNTUuMzY3MTg3NSAtMjcsIC01NS4zNjcxODc1IC0yNyIgc3Ryb2tlPSIjOTM3MERCIiBzdHJva2Utd2lkdGg9IjEuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLWRhc2hhcnJheT0iMCAwIiBzdHlsZT0iIi8+PC9nPjxnIGNsYXNzPSJsYWJlbCIgc3R5bGU9IiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQ1LjM2NzE4NzUsIC0xMikiPjxyZWN0Lz48Zm9yZWlnbk9iamVjdCB3aWR0aD0iOTAuNzM0Mzc1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9Im5vZGVMYWJlbCAiPjxwPkdvIHNob3BwaW5nPC9wPjwvc3Bhbj48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9nPjwvZz48ZyBjbGFzcz0ibm9kZSBkZWZhdWx0ICAiIGlkPSJmbG93Y2hhcnQtQy0zIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMjEuODUxNTYyNSwgMzEwLjU3ODEyNSkiPjxwb2x5Z29uIHBvaW50cz0iNzAuNTc4MTI1LDAgMTQxLjE1NjI1LC03MC41NzgxMjUgNzAuNTc4MTI1LC0xNDEuMTU2MjUgMCwtNzAuNTc4MTI1IiBjbGFzcz0ibGFiZWwtY29udGFpbmVyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNzAuMDc4MTI1LCA3MC41NzgxMjUpIi8+PGcgY2xhc3M9ImxhYmVsIiBzdHlsZT0iIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDMuNTc4MTI1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9Ijg3LjE1NjI1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9Im5vZGVMYWJlbCAiPjxwPkxldCBtZSB0aGluazwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCAgIiBpZD0iZmxvd2NoYXJ0LUQtNSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjIuNDY4NzUsIDQ4Mi4xNTYyNSkiPjxyZWN0IGNsYXNzPSJiYXNpYyBsYWJlbC1jb250YWluZXIiIHN0eWxlPSIiIHg9Ii01NC40Njg3NSIgeT0iLTI3IiB3aWR0aD0iMTA4LjkzNzUiIGhlaWdodD0iNTQiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNC40Njg3NSwgLTEyKSI+PHJlY3QvPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI0OC45Mzc1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9Im5vZGVMYWJlbCAiPjxwPkxhcHRvcDwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCAgIiBpZD0iZmxvd2NoYXJ0LUUtNyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIxLjg1MTU2MjUsIDQ4Mi4xNTYyNSkiPjxyZWN0IGNsYXNzPSJiYXNpYyBsYWJlbC1jb250YWluZXIiIHN0eWxlPSIiIHg9Ii01NC45MTQwNjI1IiB5PSItMjciIHdpZHRoPSIxMDkuODI4MTI1IiBoZWlnaHQ9IjU0Ii8+PGcgY2xhc3M9ImxhYmVsIiBzdHlsZT0iIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjQuOTE0MDYyNSwgLTEyKSI+PHJlY3QvPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI0OS44MjgxMjUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0ibm9kZUxhYmVsICI+PHA+aVBob25lPC9wPjwvc3Bhbj48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9nPjwvZz48ZyBjbGFzcz0ibm9kZSBkZWZhdWx0ICAiIGlkPSJmbG93Y2hhcnQtRi05IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNzkuODgyODEyNSwgNDgyLjE1NjI1KSI+PHJlY3QgY2xhc3M9ImJhc2ljIGxhYmVsLWNvbnRhaW5lciIgc3R5bGU9IiIgeD0iLTUzLjExNzE4NzUiIHk9Ii0yNyIgd2lkdGg9IjEwNi4yMzQzNzUiIGhlaWdodD0iNTQiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMy4xMTcxODc1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjQ2LjIzNDM3NSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJub2RlTGFiZWwgIj48cD48aSBjbGFzcz0iZmEgZmEtY2FyIj48L2k+IENhcjwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PC9nPjwvZz48L2c+PC9zdmc+", view.getContent()); + assertEquals("data:image/svg+xml;base64,PHN2ZyBpZD0ibWVybWFpZElua1N2ZyIgd2lkdGg9IjEwMCUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgY2xhc3M9ImZsb3djaGFydCIgc3R5bGU9Im1heC13aWR0aDogNDQxcHg7IiB2aWV3Qm94PSIwIDAgNDQxIDUxNy4xNTYyNSIgcm9sZT0iZ3JhcGhpY3MtZG9jdW1lbnQgZG9jdW1lbnQiIGFyaWEtcm9sZWRlc2NyaXB0aW9uPSJmbG93Y2hhcnQtdjIiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48c3R5bGUgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiPkBpbXBvcnQgdXJsKCJodHRwczovL2NkbmpzLmNsb3VkZmxhcmUuY29tL2FqYXgvbGlicy9mb250LWF3ZXNvbWUvNi43LjIvY3NzL2FsbC5taW4uY3NzIik7PC9zdHlsZT48c3R5bGU+I21lcm1haWRJbmtTdmd7Zm9udC1mYW1pbHk6InRyZWJ1Y2hldCBtcyIsdmVyZGFuYSxhcmlhbCxzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZpbGw6IzMzMzt9QGtleWZyYW1lcyBlZGdlLWFuaW1hdGlvbi1mcmFtZXtmcm9te3N0cm9rZS1kYXNob2Zmc2V0OjA7fX1Aa2V5ZnJhbWVzIGRhc2h7dG97c3Ryb2tlLWRhc2hvZmZzZXQ6MDt9fSNtZXJtYWlkSW5rU3ZnIC5lZGdlLWFuaW1hdGlvbi1zbG93e3N0cm9rZS1kYXNoYXJyYXk6OSw1IWltcG9ydGFudDtzdHJva2UtZGFzaG9mZnNldDo5MDA7YW5pbWF0aW9uOmRhc2ggNTBzIGxpbmVhciBpbmZpbml0ZTtzdHJva2UtbGluZWNhcDpyb3VuZDt9I21lcm1haWRJbmtTdmcgLmVkZ2UtYW5pbWF0aW9uLWZhc3R7c3Ryb2tlLWRhc2hhcnJheTo5LDUhaW1wb3J0YW50O3N0cm9rZS1kYXNob2Zmc2V0OjkwMDthbmltYXRpb246ZGFzaCAyMHMgbGluZWFyIGluZmluaXRlO3N0cm9rZS1saW5lY2FwOnJvdW5kO30jbWVybWFpZElua1N2ZyAuZXJyb3ItaWNvbntmaWxsOiM1NTIyMjI7fSNtZXJtYWlkSW5rU3ZnIC5lcnJvci10ZXh0e2ZpbGw6IzU1MjIyMjtzdHJva2U6IzU1MjIyMjt9I21lcm1haWRJbmtTdmcgLmVkZ2UtdGhpY2tuZXNzLW5vcm1hbHtzdHJva2Utd2lkdGg6MXB4O30jbWVybWFpZElua1N2ZyAuZWRnZS10aGlja25lc3MtdGhpY2t7c3Ryb2tlLXdpZHRoOjMuNXB4O30jbWVybWFpZElua1N2ZyAuZWRnZS1wYXR0ZXJuLXNvbGlke3N0cm9rZS1kYXNoYXJyYXk6MDt9I21lcm1haWRJbmtTdmcgLmVkZ2UtdGhpY2tuZXNzLWludmlzaWJsZXtzdHJva2Utd2lkdGg6MDtmaWxsOm5vbmU7fSNtZXJtYWlkSW5rU3ZnIC5lZGdlLXBhdHRlcm4tZGFzaGVke3N0cm9rZS1kYXNoYXJyYXk6Mzt9I21lcm1haWRJbmtTdmcgLmVkZ2UtcGF0dGVybi1kb3R0ZWR7c3Ryb2tlLWRhc2hhcnJheToyO30jbWVybWFpZElua1N2ZyAubWFya2Vye2ZpbGw6IzMzMzMzMztzdHJva2U6IzMzMzMzMzt9I21lcm1haWRJbmtTdmcgLm1hcmtlci5jcm9zc3tzdHJva2U6IzMzMzMzMzt9I21lcm1haWRJbmtTdmcgc3Zne2ZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDt9I21lcm1haWRJbmtTdmcgcHttYXJnaW46MDt9I21lcm1haWRJbmtTdmcgLmxhYmVse2ZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjtjb2xvcjojMzMzO30jbWVybWFpZElua1N2ZyAuY2x1c3Rlci1sYWJlbCB0ZXh0e2ZpbGw6IzMzMzt9I21lcm1haWRJbmtTdmcgLmNsdXN0ZXItbGFiZWwgc3Bhbntjb2xvcjojMzMzO30jbWVybWFpZElua1N2ZyAuY2x1c3Rlci1sYWJlbCBzcGFuIHB7YmFja2dyb3VuZC1jb2xvcjp0cmFuc3BhcmVudDt9I21lcm1haWRJbmtTdmcgLmxhYmVsIHRleHQsI21lcm1haWRJbmtTdmcgc3BhbntmaWxsOiMzMzM7Y29sb3I6IzMzMzt9I21lcm1haWRJbmtTdmcgLm5vZGUgcmVjdCwjbWVybWFpZElua1N2ZyAubm9kZSBjaXJjbGUsI21lcm1haWRJbmtTdmcgLm5vZGUgZWxsaXBzZSwjbWVybWFpZElua1N2ZyAubm9kZSBwb2x5Z29uLCNtZXJtYWlkSW5rU3ZnIC5ub2RlIHBhdGh7ZmlsbDojRUNFQ0ZGO3N0cm9rZTojOTM3MERCO3N0cm9rZS13aWR0aDoxcHg7fSNtZXJtYWlkSW5rU3ZnIC5yb3VnaC1ub2RlIC5sYWJlbCB0ZXh0LCNtZXJtYWlkSW5rU3ZnIC5ub2RlIC5sYWJlbCB0ZXh0LCNtZXJtYWlkSW5rU3ZnIC5pbWFnZS1zaGFwZSAubGFiZWwsI21lcm1haWRJbmtTdmcgLmljb24tc2hhcGUgLmxhYmVse3RleHQtYW5jaG9yOm1pZGRsZTt9I21lcm1haWRJbmtTdmcgLm5vZGUgLmthdGV4IHBhdGh7ZmlsbDojMDAwO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoxcHg7fSNtZXJtYWlkSW5rU3ZnIC5yb3VnaC1ub2RlIC5sYWJlbCwjbWVybWFpZElua1N2ZyAubm9kZSAubGFiZWwsI21lcm1haWRJbmtTdmcgLmltYWdlLXNoYXBlIC5sYWJlbCwjbWVybWFpZElua1N2ZyAuaWNvbi1zaGFwZSAubGFiZWx7dGV4dC1hbGlnbjpjZW50ZXI7fSNtZXJtYWlkSW5rU3ZnIC5ub2RlLmNsaWNrYWJsZXtjdXJzb3I6cG9pbnRlcjt9I21lcm1haWRJbmtTdmcgLnJvb3QgLmFuY2hvciBwYXRoe2ZpbGw6IzMzMzMzMyFpbXBvcnRhbnQ7c3Ryb2tlLXdpZHRoOjA7c3Ryb2tlOiMzMzMzMzM7fSNtZXJtYWlkSW5rU3ZnIC5hcnJvd2hlYWRQYXRoe2ZpbGw6IzMzMzMzMzt9I21lcm1haWRJbmtTdmcgLmVkZ2VQYXRoIC5wYXRoe3N0cm9rZTojMzMzMzMzO3N0cm9rZS13aWR0aDoyLjBweDt9I21lcm1haWRJbmtTdmcgLmZsb3djaGFydC1saW5re3N0cm9rZTojMzMzMzMzO2ZpbGw6bm9uZTt9I21lcm1haWRJbmtTdmcgLmVkZ2VMYWJlbHtiYWNrZ3JvdW5kLWNvbG9yOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7dGV4dC1hbGlnbjpjZW50ZXI7fSNtZXJtYWlkSW5rU3ZnIC5lZGdlTGFiZWwgcHtiYWNrZ3JvdW5kLWNvbG9yOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7fSNtZXJtYWlkSW5rU3ZnIC5lZGdlTGFiZWwgcmVjdHtvcGFjaXR5OjAuNTtiYWNrZ3JvdW5kLWNvbG9yOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7ZmlsbDpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO30jbWVybWFpZElua1N2ZyAubGFiZWxCa2d7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwgMjMyLCAyMzIsIDAuNSk7fSNtZXJtYWlkSW5rU3ZnIC5jbHVzdGVyIHJlY3R7ZmlsbDojZmZmZmRlO3N0cm9rZTojYWFhYTMzO3N0cm9rZS13aWR0aDoxcHg7fSNtZXJtYWlkSW5rU3ZnIC5jbHVzdGVyIHRleHR7ZmlsbDojMzMzO30jbWVybWFpZElua1N2ZyAuY2x1c3RlciBzcGFue2NvbG9yOiMzMzM7fSNtZXJtYWlkSW5rU3ZnIGRpdi5tZXJtYWlkVG9vbHRpcHtwb3NpdGlvbjphYnNvbHV0ZTt0ZXh0LWFsaWduOmNlbnRlcjttYXgtd2lkdGg6MjAwcHg7cGFkZGluZzoycHg7Zm9udC1mYW1pbHk6InRyZWJ1Y2hldCBtcyIsdmVyZGFuYSxhcmlhbCxzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxMnB4O2JhY2tncm91bmQ6aHNsKDgwLCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSk7Ym9yZGVyOjFweCBzb2xpZCAjYWFhYTMzO2JvcmRlci1yYWRpdXM6MnB4O3BvaW50ZXItZXZlbnRzOm5vbmU7ei1pbmRleDoxMDA7fSNtZXJtYWlkSW5rU3ZnIC5mbG93Y2hhcnRUaXRsZVRleHR7dGV4dC1hbmNob3I6bWlkZGxlO2ZvbnQtc2l6ZToxOHB4O2ZpbGw6IzMzMzt9I21lcm1haWRJbmtTdmcgcmVjdC50ZXh0e2ZpbGw6bm9uZTtzdHJva2Utd2lkdGg6MDt9I21lcm1haWRJbmtTdmcgLmljb24tc2hhcGUsI21lcm1haWRJbmtTdmcgLmltYWdlLXNoYXBle2JhY2tncm91bmQtY29sb3I6cmdiYSgyMzIsMjMyLDIzMiwgMC44KTt0ZXh0LWFsaWduOmNlbnRlcjt9I21lcm1haWRJbmtTdmcgLmljb24tc2hhcGUgcCwjbWVybWFpZElua1N2ZyAuaW1hZ2Utc2hhcGUgcHtiYWNrZ3JvdW5kLWNvbG9yOnJnYmEoMjMyLDIzMiwyMzIsIDAuOCk7cGFkZGluZzoycHg7fSNtZXJtYWlkSW5rU3ZnIC5pY29uLXNoYXBlIHJlY3QsI21lcm1haWRJbmtTdmcgLmltYWdlLXNoYXBlIHJlY3R7b3BhY2l0eTowLjU7YmFja2dyb3VuZC1jb2xvcjpyZ2JhKDIzMiwyMzIsMjMyLCAwLjgpO2ZpbGw6cmdiYSgyMzIsMjMyLDIzMiwgMC44KTt9I21lcm1haWRJbmtTdmcgLmxhYmVsLWljb257ZGlzcGxheTppbmxpbmUtYmxvY2s7aGVpZ2h0OjFlbTtvdmVyZmxvdzp2aXNpYmxlO3ZlcnRpY2FsLWFsaWduOi0wLjEyNWVtO30jbWVybWFpZElua1N2ZyAubm9kZSAubGFiZWwtaWNvbiBwYXRoe2ZpbGw6Y3VycmVudENvbG9yO3N0cm9rZTpyZXZlcnQ7c3Ryb2tlLXdpZHRoOnJldmVydDt9I21lcm1haWRJbmtTdmcgOnJvb3R7LS1tZXJtYWlkLWZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT48Zz48bWFya2VyIGlkPSJtZXJtYWlkSW5rU3ZnX2Zsb3djaGFydC12Mi1wb2ludEVuZCIgY2xhc3M9Im1hcmtlciBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iNSIgcmVmWT0iNSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iOCIgbWFya2VySGVpZ2h0PSI4IiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0gMCAwIEwgMTAgNSBMIDAgMTAgeiIgY2xhc3M9ImFycm93TWFya2VyUGF0aCIgc3R5bGU9InN0cm9rZS13aWR0aDogMTsgc3Ryb2tlLWRhc2hhcnJheTogMSwgMDsiLz48L21hcmtlcj48bWFya2VyIGlkPSJtZXJtYWlkSW5rU3ZnX2Zsb3djaGFydC12Mi1wb2ludFN0YXJ0IiBjbGFzcz0ibWFya2VyIGZsb3djaGFydC12MiIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI0LjUiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjgiIG1hcmtlckhlaWdodD0iOCIgb3JpZW50PSJhdXRvIj48cGF0aCBkPSJNIDAgNSBMIDEwIDEwIEwgMTAgMCB6IiBjbGFzcz0iYXJyb3dNYXJrZXJQYXRoIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyBzdHJva2UtZGFzaGFycmF5OiAxLCAwOyIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9Im1lcm1haWRJbmtTdmdfZmxvd2NoYXJ0LXYyLWNpcmNsZUVuZCIgY2xhc3M9Im1hcmtlciBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iMTEiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjExIiBtYXJrZXJIZWlnaHQ9IjExIiBvcmllbnQ9ImF1dG8iPjxjaXJjbGUgY3g9IjUiIGN5PSI1IiByPSI1IiBjbGFzcz0iYXJyb3dNYXJrZXJQYXRoIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyBzdHJva2UtZGFzaGFycmF5OiAxLCAwOyIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9Im1lcm1haWRJbmtTdmdfZmxvd2NoYXJ0LXYyLWNpcmNsZVN0YXJ0IiBjbGFzcz0ibWFya2VyIGZsb3djaGFydC12MiIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSItMSIgcmVmWT0iNSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTEiIG1hcmtlckhlaWdodD0iMTEiIG9yaWVudD0iYXV0byI+PGNpcmNsZSBjeD0iNSIgY3k9IjUiIHI9IjUiIGNsYXNzPSJhcnJvd01hcmtlclBhdGgiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7IHN0cm9rZS1kYXNoYXJyYXk6IDEsIDA7Ii8+PC9tYXJrZXI+PG1hcmtlciBpZD0ibWVybWFpZElua1N2Z19mbG93Y2hhcnQtdjItY3Jvc3NFbmQiIGNsYXNzPSJtYXJrZXIgY3Jvc3MgZmxvd2NoYXJ0LXYyIiB2aWV3Qm94PSIwIDAgMTEgMTEiIHJlZlg9IjEyIiByZWZZPSI1LjIiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjExIiBtYXJrZXJIZWlnaHQ9IjExIiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0gMSwxIGwgOSw5IE0gMTAsMSBsIC05LDkiIGNsYXNzPSJhcnJvd01hcmtlclBhdGgiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDI7IHN0cm9rZS1kYXNoYXJyYXk6IDEsIDA7Ii8+PC9tYXJrZXI+PG1hcmtlciBpZD0ibWVybWFpZElua1N2Z19mbG93Y2hhcnQtdjItY3Jvc3NTdGFydCIgY2xhc3M9Im1hcmtlciBjcm9zcyBmbG93Y2hhcnQtdjIiIHZpZXdCb3g9IjAgMCAxMSAxMSIgcmVmWD0iLTEiIHJlZlk9IjUuMiIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTEiIG1hcmtlckhlaWdodD0iMTEiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAxLDEgbCA5LDkgTSAxMCwxIGwgLTksOSIgY2xhc3M9ImFycm93TWFya2VyUGF0aCIgc3R5bGU9InN0cm9rZS13aWR0aDogMjsgc3Ryb2tlLWRhc2hhcnJheTogMSwgMDsiLz48L21hcmtlcj48ZyBjbGFzcz0icm9vdCI+PGcgY2xhc3M9ImNsdXN0ZXJzIi8+PGcgY2xhc3M9ImVkZ2VQYXRocyI+PHBhdGggZD0iTTIyMS44NTIsNjJMMjIxLjg1Miw2OC4xNjdDMjIxLjg1Miw3NC4zMzMsMjIxLjg1Miw4Ni42NjcsMjIxLjkyNiw5OC40MTdDMjIyLDExMC4xNjcsMjIyLjE0OSwxMjEuMzM0LDIyMi4yMjQsMTI2LjkxN0wyMjIuMjk4LDEzMi41IiBpZD0iTF9BX0JfMCIgY2xhc3M9ImVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZWRnZS10aGlja25lc3Mtbm9ybWFsIGVkZ2UtcGF0dGVybi1zb2xpZCBmbG93Y2hhcnQtbGluayIgc3R5bGU9IjsiIGRhdGEtZWRnZT0idHJ1ZSIgZGF0YS1ldD0iZWRnZSIgZGF0YS1pZD0iTF9BX0JfMCIgZGF0YS1wb2ludHM9Ilczc2llQ0k2TWpJeExqZzFNVFUyTWpVc0lua2lPall5ZlN4N0luZ2lPakl5TVM0NE5URTFOakkxTENKNUlqbzVPWDBzZXlKNElqb3lNakl1TXpVeE5UWXlOU3dpZVNJNk1UTTJMalY5WFE9PSIgbWFya2VyLWVuZD0idXJsKCNtZXJtYWlkSW5rU3ZnX2Zsb3djaGFydC12Mi1wb2ludEVuZCkiLz48cGF0aCBkPSJNMjIyLjM1MiwxOTAuNUwyMjIuMjY4LDE5NC41ODNDMjIyLjE4NSwxOTguNjY3LDIyMi4wMTgsMjA2LjgzMywyMjEuOTM1LDIxNC40MTdDMjIxLjg1MiwyMjIsMjIxLjg1MiwyMjksMjIxLjg1MiwyMzIuNUwyMjEuODUyLDIzNiIgaWQ9IkxfQl9DXzAiIGNsYXNzPSJlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZmxvd2NoYXJ0LWxpbmsiIHN0eWxlPSI7IiBkYXRhLWVkZ2U9InRydWUiIGRhdGEtZXQ9ImVkZ2UiIGRhdGEtaWQ9IkxfQl9DXzAiIGRhdGEtcG9pbnRzPSJXM3NpZUNJNk1qSXlMak0xTVRVMk1qVXNJbmtpT2pFNU1DNDFmU3g3SW5naU9qSXlNUzQ0TlRFMU5qSTFMQ0o1SWpveU1UVjlMSHNpZUNJNk1qSXhMamcxTVRVMk1qVXNJbmtpT2pJME1IMWQiIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZElua1N2Z19mbG93Y2hhcnQtdjItcG9pbnRFbmQpIi8+PHBhdGggZD0iTTE3OS43MTUsMzM5LjAxOUwxNjAuMTc0LDM1Mi4yMDlDMTQwLjYzMywzNjUuMzk4LDEwMS41NTEsMzkxLjc3Nyw4Mi4wMSw0MTAuNDY3QzYyLjQ2OSw0MjkuMTU2LDYyLjQ2OSw0NDAuMTU2LDYyLjQ2OSw0NDUuNjU2TDYyLjQ2OSw0NTEuMTU2IiBpZD0iTF9DX0RfMCIgY2xhc3M9ImVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZWRnZS10aGlja25lc3Mtbm9ybWFsIGVkZ2UtcGF0dGVybi1zb2xpZCBmbG93Y2hhcnQtbGluayIgc3R5bGU9IjsiIGRhdGEtZWRnZT0idHJ1ZSIgZGF0YS1ldD0iZWRnZSIgZGF0YS1pZD0iTF9DX0RfMCIgZGF0YS1wb2ludHM9Ilczc2llQ0k2TVRjNUxqY3hORFV6TmpFeU9EazFORE0yTENKNUlqb3pNemt1TURFNU1qSXpOakk0T1RVME16WjlMSHNpZUNJNk5qSXVORFk0TnpVc0lua2lPalF4T0M0eE5UWXlOWDBzZXlKNElqbzJNaTQwTmpnM05Td2llU0k2TkRVMUxqRTFOakkxZlYwPSIgbWFya2VyLWVuZD0idXJsKCNtZXJtYWlkSW5rU3ZnX2Zsb3djaGFydC12Mi1wb2ludEVuZCkiLz48cGF0aCBkPSJNMjIxLjg1MiwzODEuMTU2TDIyMS44NTIsMzg3LjMyM0MyMjEuODUyLDM5My40OSwyMjEuODUyLDQwNS44MjMsMjIxLjg1Miw0MTcuNDlDMjIxLjg1Miw0MjkuMTU2LDIyMS44NTIsNDQwLjE1NiwyMjEuODUyLDQ0NS42NTZMMjIxLjg1Miw0NTEuMTU2IiBpZD0iTF9DX0VfMCIgY2xhc3M9ImVkZ2UtdGhpY2tuZXNzLW5vcm1hbCBlZGdlLXBhdHRlcm4tc29saWQgZWRnZS10aGlja25lc3Mtbm9ybWFsIGVkZ2UtcGF0dGVybi1zb2xpZCBmbG93Y2hhcnQtbGluayIgc3R5bGU9IjsiIGRhdGEtZWRnZT0idHJ1ZSIgZGF0YS1ldD0iZWRnZSIgZGF0YS1pZD0iTF9DX0VfMCIgZGF0YS1wb2ludHM9Ilczc2llQ0k2TWpJeExqZzFNVFUyTWpVc0lua2lPak00TVM0eE5UWXlOWDBzZXlKNElqb3lNakV1T0RVeE5UWXlOU3dpZVNJNk5ERTRMakUxTmpJMWZTeDdJbmdpT2pJeU1TNDROVEUxTmpJMUxDSjVJam8wTlRVdU1UVTJNalY5WFE9PSIgbWFya2VyLWVuZD0idXJsKCNtZXJtYWlkSW5rU3ZnX2Zsb3djaGFydC12Mi1wb2ludEVuZCkiLz48cGF0aCBkPSJNMjYzLjg0NCwzMzkuMTY0TDI4My4xODQsMzUyLjMyOUMzMDIuNTI0LDM2NS40OTUsMzQxLjIwMywzOTEuODI1LDM2MC41NDMsNDEwLjQ5MUMzNzkuODgzLDQyOS4xNTYsMzc5Ljg4Myw0NDAuMTU2LDM3OS44ODMsNDQ1LjY1NkwzNzkuODgzLDQ1MS4xNTYiIGlkPSJMX0NfRl8wIiBjbGFzcz0iZWRnZS10aGlja25lc3Mtbm9ybWFsIGVkZ2UtcGF0dGVybi1zb2xpZCBlZGdlLXRoaWNrbmVzcy1ub3JtYWwgZWRnZS1wYXR0ZXJuLXNvbGlkIGZsb3djaGFydC1saW5rIiBzdHlsZT0iOyIgZGF0YS1lZGdlPSJ0cnVlIiBkYXRhLWV0PSJlZGdlIiBkYXRhLWlkPSJMX0NfRl8wIiBkYXRhLXBvaW50cz0iVzNzaWVDSTZNall6TGpnME16ZzJOVE0xTmpBMU1ETTBMQ0o1SWpvek16a3VNVFl6T1RRM01UUXpPVFE1TmpaOUxIc2llQ0k2TXpjNUxqZzRNamd4TWpVc0lua2lPalF4T0M0eE5UWXlOWDBzZXlKNElqb3pOemt1T0RneU9ERXlOU3dpZVNJNk5EVTFMakUxTmpJMWZWMD0iIG1hcmtlci1lbmQ9InVybCgjbWVybWFpZElua1N2Z19mbG93Y2hhcnQtdjItcG9pbnRFbmQpIi8+PC9nPjxnIGNsYXNzPSJlZGdlTGFiZWxzIj48ZyBjbGFzcz0iZWRnZUxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMjEuODUxNTYyNSwgOTkpIj48ZyBjbGFzcz0ibGFiZWwiIGRhdGEtaWQ9IkxfQV9CXzAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zOS4xMzI4MTI1LCAtMTIpIj48Zm9yZWlnbk9iamVjdCB3aWR0aD0iNzguMjY1NjI1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBjbGFzcz0ibGFiZWxCa2ciIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0iZWRnZUxhYmVsIj48cD5HZXQgbW9uZXk8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJlZGdlTGFiZWwiPjxnIGNsYXNzPSJsYWJlbCIgZGF0YS1pZD0iTF9CX0NfMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwgMCkiPjxmb3JlaWduT2JqZWN0IHdpZHRoPSIwIiBoZWlnaHQ9IjAiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIGNsYXNzPSJsYWJlbEJrZyIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJlZGdlTGFiZWwiPjwvc3Bhbj48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9nPjwvZz48ZyBjbGFzcz0iZWRnZUxhYmVsIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Mi40Njg3NSwgNDE4LjE1NjI1KSI+PGcgY2xhc3M9ImxhYmVsIiBkYXRhLWlkPSJMX0NfRF8wIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTUuMTI1LCAtMTIpIj48Zm9yZWlnbk9iamVjdCB3aWR0aD0iMzAuMjUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIGNsYXNzPSJsYWJlbEJrZyIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJlZGdlTGFiZWwiPjxwPk9uZTwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9ImVkZ2VMYWJlbCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIxLjg1MTU2MjUsIDQxOC4xNTYyNSkiPjxnIGNsYXNzPSJsYWJlbCIgZGF0YS1pZD0iTF9DX0VfMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE0LjY3MTg3NSwgLTEyKSI+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjI5LjM0Mzc1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBjbGFzcz0ibGFiZWxCa2ciIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0iZWRnZUxhYmVsIj48cD5Ud288L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJlZGdlTGFiZWwiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM3OS44ODI4MTI1LCA0MTguMTU2MjUpIj48ZyBjbGFzcz0ibGFiZWwiIGRhdGEtaWQ9IkxfQ19GXzAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMC44OTg0Mzc1LCAtMTIpIj48Zm9yZWlnbk9iamVjdCB3aWR0aD0iNDEuNzk2ODc1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBjbGFzcz0ibGFiZWxCa2ciIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0iZWRnZUxhYmVsIj48cD5UaHJlZTwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PC9nPjxnIGNsYXNzPSJub2RlcyI+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCIgaWQ9ImZsb3djaGFydC1BLTAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMS44NTE1NjI1LCAzNSkiPjxyZWN0IGNsYXNzPSJiYXNpYyBsYWJlbC1jb250YWluZXIiIHN0eWxlPSIiIHg9Ii02Ni4wMDc4MTI1IiB5PSItMjciIHdpZHRoPSIxMzIuMDE1NjI1IiBoZWlnaHQ9IjU0Ii8+PGcgY2xhc3M9ImxhYmVsIiBzdHlsZT0iIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzYuMDA3ODEyNSwgLTEyKSI+PHJlY3QvPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI3Mi4wMTU2MjUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0ibm9kZUxhYmVsIj48cD5DaHJpc3RtYXM8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJub2RlIGRlZmF1bHQiIGlkPSJmbG93Y2hhcnQtQi0xIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMjEuODUxNTYyNSwgMTYzKSI+PGcgY2xhc3M9ImJhc2ljIGxhYmVsLWNvbnRhaW5lciBvdXRlci1wYXRoIj48cGF0aCBkPSJNLTU1LjM2NzE4NzUgLTI3IEMtMjYuMDY1Mjk0MzIwMjQwNjMyIC0yNywgMy4yMzY1OTg4NTk1MTg3MzYgLTI3LCA1NS4zNjcxODc1IC0yNyBDNTUuMzY3MTg3NSAtMjcsIDU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNyBDNTUuNTI3NDU4NTE3MTQzNjYgLTI2Ljk5MzM3MTE0MTY5NzEsIDU1LjY4NzcyOTUzNDI4NzMxNCAtMjYuOTg2NzQyMjgzMzk0MiwgNTUuNzgwMDg0MjI3MzYxNjYgLTI2Ljk4MjkyMjQ2NTAzMzM0NyBDNTUuODkxMzQ1NjE0NTE2ODIgLTI2Ljk2OTA1Mzc0NzU4NzY3NiwgNTYuMDAyNjA3MDAxNjcxOTggLTI2Ljk1NTE4NTAzMDE0MjAwNSwgNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBDNTYuMzE5NTkzOTgwMDgxMjE2IC0yNi45MDQ2NjcxNjAwMDE0NTYsIDU2LjQ0OTAyNzUwODc1ODc2IC0yNi44Nzc1Mjc4MDI5ODkzLCA1Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNzQwOTk1NDAxMzg2MzE0IC0yNi44MDM0MjE5NzY5MDY3MTYsIDU2Ljg4NzM3NTg2NzA2ODY0IC0yNi43NTk4NDI2MjQxMTY3OCwgNTYuOTkwNjg0ODQ2MDIzNDIgLTI2LjcyOTA4NjIwODUwMzE3MyBDNTcuMTEyMjk4NDc0NzQ4ODA1IC0yNi42ODE2MzI0MjMzMjAwOCwgNTcuMjMzOTEyMTAzNDc0MTkgLTI2LjYzNDE3ODYzODEzNjk4NywgNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQzU3LjQ2MTAwMTkxNDY0NzY5NiAtMjYuNTM3MTQ3NzczNjMzNjk1LCA1Ny41NDYzMzkyMDYwMzA1NSAtMjYuNDk1NDI4OTEzOTkyMTA0LCA1Ny43NDY5MjQ0NjUxODUzNjYgLTI2LjM5NzM2ODc1NjAzMjQ0NiBDNTcuODI3MDU1Nzc2NTMzNzMgLTI2LjM0OTYyMDg1ODQzNTg4OCwgNTcuOTA3MTg3MDg3ODgyMSAtMjYuMzAxODcyOTYwODM5MzI2LCA1OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBDNTguMTgwMzI0ODY5NzgwOTIgLTI2LjEyOTg1ODMwOTY0OTY1MiwgNTguMjU4NzIxNDQ4OTQ5NyAtMjYuMDczODg0MjI3OTg2NjYsIDU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NyBDNTguNTM1NzA1MzU2OTYyNyAtMjUuODYzMTYyOTcwNzQyMDk2LCA1OC42MzMxNTk2NTA0NzcwNyAtMjUuNzgwNjIzMzk0NTAyMjI1LCA1OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBDNTguODYzMzIyODE4MzY4NTk0IC0yNS41Njg4OTIwOTMxMjU3NywgNTguOTczMDUwMjc4NjA4NDggLTI1LjQ1OTE2NDYzMjg4NTg3NywgNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBDNTkuMTA2Njk3ODUwNDczMzUgLTI1LjMxNDUxNDIyOTgzNTYzOCwgNTkuMTY3NTg4NjQ3NTgxMDUgLTI1LjI0MjYyMDYwMTU0MjU2NiwgNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4zNjUxNDAxOTEyNTc5MTUgLTI0Ljk5Nzg4MjY4MzEwODU0NiwgNTkuNDE3MzkwMzM1NTMzODUgLTI0LjkyNDcwMTgwMjc2ODc1NSwgNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzYgQzU5LjYxOTI0MDc1ODU2Mzk0IC0yNC42MjM2MDc4MzYxMDM2OSwgNTkuNjg1NDYxNjI1ODE1MjM2IC0yNC41MTI0NzQ4ODE1OTUyNSwgNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM3IEM1OS44MTU3NjcyMzk5NzU5NSAtMjQuMjc0OTgzMjIzMjAwNzI4LCA1OS44NjY5NzgyMjM5MTk0NCAtMjQuMTcwMjI5NDgxMjE2MDg2LCA1OS45NDYwNTQxMzMyNzUyOSAtMjQuMDA4NDc3MTIzMjY0ODQ2IEM1OS45OTg4NTkxODcyNjUzNDYgLTIzLjg3MzE0OTM2NzkzNDE5NywgNjAuMDUxNjY0MjQxMjU1NCAtMjMuNzM3ODIxNjEyNjAzNTQ3LCA2MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMTM0OTI0MjY5NzY5MjM2IC0yMy40OTM2NzIzOTkyMzU0MiwgNjAuMTczNTc0ODMxMDM1MjkgLTIzLjM2Mzg0NzQ1MjQ0NzQzLCA2MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4yMzQ1MzcxMTQ3NDM5NiAtMjMuMTMwMzgyMDE0MzYxNDg0LCA2MC4yNTQ4ODUzOTk3OTEyNyAtMjMuMDMzMzM2NTkzMDE4OTcsIDYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDNjAuMzEzNTQyMjIzMTI3MDk0IC0yMi43MDYyNjAzODU5Mzc4NDYsIDYwLjMyODA5MDQyOTI0MDU3NSAtMjIuNTg5NTQ3ODIwNDcyMDIsIDYwLjM1MDEwOTk2NTAzMzM1IC0yMi40MTI4OTY3MjczNjE2NjIgQzYwLjM1NTgwNzMzMzM0MzE3IC0yMi4yNzUxNDcwNjE4MzM1MzYsIDYwLjM2MTUwNDcwMTY1MyAtMjIuMTM3Mzk3Mzk2MzA1NDA2LCA2MC4zNjcxODc1IC0yMiBDNjAuMzY3MTg3NSAtMjIsIDYwLjM2NzE4NzUgLTIyLCA2MC4zNjcxODc1IC0yMiBDNjAuMzY3MTg3NSAtNi42NDk0ODg2MDMwNTIwNjcsIDYwLjM2NzE4NzUgOC43MDEwMjI3OTM4OTU4NjYsIDYwLjM2NzE4NzUgMjIgQzYwLjM2NzE4NzUgMjIsIDYwLjM2NzE4NzUgMjIsIDYwLjM2NzE4NzUgMjIgQzYwLjM2MjUyMjUzMzM0NzkzIDIyLjExMjc4ODQ5NDgwNjM2NywgNjAuMzU3ODU3NTY2Njk1ODU2IDIyLjIyNTU3Njk4OTYxMjczMywgNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIEM2MC4zMzY1MzQ4NDg4NDM0MTQgMjIuNTIxODAyNzA2ODgwMDI0LCA2MC4zMjI5NTk3MzI2NTM0OCAyMi42MzA3MDg2ODYzOTgzODYsIDYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEM2MC4yNzQyOTgzNzYzMjY0MSAyMi45NDA3NTE4NjI1MTI5MDQsIDYwLjI0OTYwMjczNTYzOTIyIDIzLjA1ODUzMDc3MzYyMjE0LCA2MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQzYwLjE3OTUwMDUyMzQ4NjE0IDIzLjM0Mzk0MzQwMjM2NjAzNywgNjAuMTQ0ODEyMjE3Mjc1NjMgMjMuNDYwNDU5MzY5MDI4MDc2LCA2MC4wOTYyNzM3MDg1MDMxNzYgMjMuNjIzNDk3MzQ2MDIzNDE3IEM2MC4wNjQ2MTI5OTA1NTA2NCAyMy43MDQ2MzY4MTIyMDkzNTUsIDYwLjAzMjk1MjI3MjU5ODExNiAyMy43ODU3NzYyNzgzOTUyOTMsIDU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuODkzOTA4ODA3NDAzMDQgMjQuMTE1MTQyMDkyMjEyODAzLCA1OS44NDE3NjM0ODE1MzA3NzYgMjQuMjIxODA3MDYxMTYwNzY0LCA1OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgQzU5LjY5NDUxMjQ5Njg0OTQ5IDI0LjQ5NzI4NTU2MDE1MjI3OCwgNTkuNjI0NDY4NzM3NjY2NTMgMjQuNjE0ODM0MTU1MTE5MTksIDU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQzU5LjQ4MjUwNTgwODI3MjU1NiAyNC44MzM1MDE5MDg3ODE5NjUsIDU5LjQxMTk5MTcyNTIzMjQ2IDI0LjkzMjI2MzAyNjk1MTgsIDU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4yMjQwNjg5NTQxNCAyNS4xNzU5MzQ0Mjk2MDAyNiwgNTkuMTM1MjQ3ODYxMjk4MDI2IDI1LjI4MDgwNTI5NTc1MjE4LCA1OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQzU4Ljk0MTgzMTU4ODIzMjAxIDI1LjQ5MDM4MzMyMzI2MjM1LCA1OC44Mzc4NTYxMjMwOTgzNyAyNS41OTQzNTg3ODgzOTU5OTQsIDU4Ljc1MzU5NTM1ODEyODcwNiAyNS42Nzg2MTk1NTMzNjU2NTcgQzU4LjY3MjY5NTcxMDc4NTMgMjUuNzQ3MTM4MDU5NTE3NDI1LCA1OC41OTE3OTYwNjM0NDE4OSAyNS44MTU2NTY1NjU2NjkxOSwgNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjMzNDg0MzY4NjgyNjQ1IDI2LjAxOTUzMzk5NDc5NDYwMywgNTguMjMxNDM2MzEwMjA0NTY1IDI2LjA5MzM2NTQ0MjYwNzI0LCA1OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IEM1OC4wMTQwNDc1MzI0OTY5NzQgMjYuMjM4MTk3OTU2ODg5NzMsIDU3LjkyNjE2Njc3NDM4MTgxIDI2LjI5MDU2MzUyMjQ2NjgxNSwgNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBDNTcuNjUzMTQ5MzQwNzQ3NDA0IDI2LjQ0MzIxMjYyMDg5OTc4MiwgNTcuNTU5Mzc0MjE2MzA5NDUgMjYuNDg5MDU2NDg1NzY3MTE1LCA1Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEM1Ny4yODc4NjgyMjQ3Mzg3MSAyNi42MTMxMjQ4OTQyODc0MywgNTcuMjAwMDcxODI2MjEyNTcgMjYuNjQ3MzgzMTU1Mjk5NTgsIDU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDNTYuODYxMTUxMTkyMjAxMDE0IDI2Ljc2NzY1MDA0ODA5NTk5OCwgNTYuNzMxNjE3NTM4Mzc4NjE1IDI2LjgwNjIxMzg4NzY4ODgyMywgNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNTA1NzcwMjgzMjAxMzY1IDI2Ljg2NTYzMDA5NDM0MTUzLCA1Ni40MTY5MjU2MzA2OTg3MzUgMjYuODg0MjU4ODU4OTg2NDA2LCA1Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQzU2LjA3NzExMTk1NjE5OTggMjYuOTQ1ODk3OTk3MjQ1MjQzLCA1NS45NjQwNjM0NjA5OTU5MiAyNi45NTk5ODk0Nzc0NzY4NzgsIDU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBDNTUuNjgzOTkwODYyNzI0NzA2IDI2Ljk4Njg5NjkxNTk5Mzg1LCA1NS41ODc4OTc0OTgwODc3NSAyNi45OTA4NzEzNjY5NTQzNSwgNTUuMzY3MTg3NSAyNyBDNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNywgNTUuMzY3MTg3NSAyNyBDMjUuNjQ2MTg2Mzc2MDY1NzkgMjcsIC00LjA3NDgxNDc0Nzg2ODQyMyAyNywgLTU1LjM2NzE4NzUgMjcgQy01NS4zNjcxODc1IDI3LCAtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcgQy01NS40ODI5NTI2OTQwMzAwMiAyNi45OTUyMTE5MTYxNTczMzgsIC01NS41OTg3MTc4ODgwNjAwNDQgMjYuOTkwNDIzODMyMzE0NjcyLCAtNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuOTExODU3MzEyMjEzNzYgMjYuOTY2NDk2OTY3MTM2MTMsIC01Ni4wNDM2MzAzOTcwNjU4NiAyNi45NTAwNzE0NjkyMzg5MTcsIC01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQy01Ni4zNDY1MzAzMDI5MTMzNDQgMjYuODk5MDE5MjA3MDM3NDUsIC01Ni41MDI5MDAxNTQ0MjMwMSAyNi44NjYyMzE4OTcwNjEyOTQsIC01Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNjc3NTUyNjk3NDYzNDkgMjYuODIyMzA5Njg4MjA5Njc1LCAtNTYuNzYwNDkwNDU5MjIyOTkgMjYuNzk3NjE4MDQ2NzIyNjkzLCAtNTYuOTkwNjg0ODQ2MDIzNDIgMjYuNzI5MDg2MjA4NTAzMTczIEMtNTcuMTA1MDIwMjYwNDY5MSAyNi42ODQ0NzIzOTEzNDQ3NjIsIC01Ny4yMTkzNTU2NzQ5MTQ3NyAyNi42Mzk4NTg1NzQxODYzNTYsIC01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNDg3NTAzNDYyNjE4NTQgMjYuNTI0MTkxOTU2Mjg2MjcsIC01Ny41OTkzNDIzMDE5NzIyMyAyNi40Njk1MTcyNzkyOTcyNSwgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgQy01Ny44ODg1MTU4NzU5MTI1MyAyNi4zMTI5OTg1ODgyODkxMzcsIC01OC4wMzAxMDcyODY2Mzk3IDI2LjIyODYyODQyMDU0NTgyNCwgLTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgQy01OC4xODcwOTIyODg5MTA0MiAyNi4xMjUwMjY0NjUyNjI4MzUsIC01OC4yNzIyNTYyODcyMDg3MSAyNi4wNjQyMjA1MzkyMTMwMiwgLTU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IEMtNTguNTMyNzg4OTAyMzM1NzMgMjUuODY1NjMzMDgxODEwMDkyLCAtNTguNjI3MzI2NzQxMjIzMTE1IDI1Ljc4NTU2MzYxNjYzODIxNCwgLTU4Ljc1MzU5NTM1ODEyODcwNiAyNS42Nzg2MTk1NTMzNjU2NiBDLTU4LjgzOTAxNTE3ODQxNzEyIDI1LjU5MzE5OTczMzA3NzI0MiwgLTU4LjkyNDQzNDk5ODcwNTUzNiAyNS41MDc3Nzk5MTI3ODg4MjcsIC01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQy01OS4xMTIzNjYyNzI3NjY0OCAyNS4zMDc4MjE1MzYyODA4MiwgLTU5LjE3ODkyNTQ5MjE2NzMxIDI1LjIyOTIzNTIxNDQzMjkzLCAtNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4zOTQwNjMxNzM1MjIwOSAyNC45NTczNzM1MjU5ODYwOTIsIC01OS40NzUyMzYzMDAwNjIyMTQgMjQuODQzNjgzNDg4NTIzODQ0LCAtNTkuNTUzMDE5ODkxMzEyNjQ0IDI0LjczNDc0MDc5MDYxMjEzMyBDLTU5LjYwNjgwNjk3NTc1MDA3IDI0LjY0NDQ3NDQxNjAzODA1LCAtNTkuNjYwNTk0MDYwMTg3NDg1IDI0LjU1NDIwODA0MTQ2Mzk3LCAtNTkuNzY0NTU2MjU2MDMyNDQgMjQuMzc5NzM2OTY1MTg1MzcgQy01OS44MjA2MDkxNTU3NjY5NSAyNC4yNjUwNzg5MjYxNDUxNzYsIC01OS44NzY2NjIwNTU1MDE0NyAyNC4xNTA0MjA4ODcxMDQ5ODIsIC01OS45NDYwNTQxMzMyNzUyOCAyNC4wMDg0NzcxMjMyNjQ4NSBDLTU5Ljk3NzQ3MjU1ODYzOCAyMy45Mjc5NTg1OTk3Njk3MTcsIC02MC4wMDg4OTA5ODQwMDA3MiAyMy44NDc0NDAwNzYyNzQ1ODQsIC02MC4wOTYyNzM3MDg1MDMxNzYgMjMuNjIzNDk3MzQ2MDIzNDE3IEMtNjAuMTMwMjQ0OTA1NjIxOTUgMjMuNTA5MzkwMTA2MzQwNCwgLTYwLjE2NDIxNjEwMjc0MDcyNCAyMy4zOTUyODI4NjY2NTczODIsIC02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4yMzc0MTA1NTU4MDk5MyAyMy4xMTY2Nzc5NDU1MjcxLCAtNjAuMjYwNjMyMjgxOTIzMjEgMjMuMDA1OTI4NDU1MzUwMjA4LCAtNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQy02MC4zMTEyODU5OTM2NDg4NSAyMi43MjQzNjA5MjIwODg1NTcsIC02MC4zMjM1Nzc5NzAyODQwOSAyMi42MjU3NDg4OTI3NzM0NDQsIC02MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgQy02MC4zNTY0NzgyMjYxMDYwNiAyMi4yNTg5MjYzNzAzMzc1MTIsIC02MC4zNjI4NDY0ODcxNzg3NyAyMi4xMDQ5NTYwMTMzMTMzNjYsIC02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSAyMiwgLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSAxMi43MTU1Mjg2MTg3NDA0MzIsIC02MC4zNjcxODc1IDMuNDMxMDU3MjM3NDgwODY0MywgLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzY3MTg3NSAtMjIsIC02MC4zNjcxODc1IC0yMiwgLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYzNjIyODk5MDY0MDMgLTIyLjA4NjE4NDEwNDY1NDY3NywgLTYwLjM2MDA1ODI5ODEyODA3NCAtMjIuMTcyMzY4MjA5MzA5MzU0LCAtNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IEMtNjAuMzM4MDM3MDc1MTY5NTk1IC0yMi41MDk3NTExMzkzNDg1MTMsIC02MC4zMjU5NjQxODUzMDU4NCAtMjIuNjA2NjA1NTUxMzM1MzcsIC02MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgQy02MC4yNzkwMTM0MTUxOTg1MzQgLTIyLjkxODI2NDgxMTAwMzI4LCAtNjAuMjU5MDMyODEzMzgzNDYgLTIzLjAxMzU1NjY3MDYwMjg4NSwgLTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4xNzIyOTkzNDc4ODA4IC0yMy4zNjgxMzE3MjQ4NDE4NDUsIC02MC4xMzA0MDk4NjYwNjQ5NDQgLTIzLjUwODgzNjAxMzk3OTY5NiwgLTYwLjA5NjI3MzcwODUwMzE3NiAtMjMuNjIzNDk3MzQ2MDIzNDE3IEMtNjAuMDYwMjYyODU1MjM3NDI2IC0yMy43MTU3ODUyNTM1NTgwNTMsIC02MC4wMjQyNTIwMDE5NzE2NyAtMjMuODA4MDczMTYxMDkyNjksIC01OS45NDYwNTQxMzMyNzUyOSAtMjQuMDA4NDc3MTIzMjY0ODQ2IEMtNTkuODgxNTIwNjg4MTU0NjUgLTI0LjE0MDQ4MjM5NTE2MTMyLCAtNTkuODE2OTg3MjQzMDM0MDIgLTI0LjI3MjQ4NzY2NzA1Nzc5MiwgLTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNjYgQy01OS43MTIwNTcxMzA2Mjc3MSAtMjQuNDY3ODQxODY1NjExOTU1LCAtNTkuNjU5NTU4MDA1MjIyOTc2IC0yNC41NTU5NDY3NjYwMzg1NDUsIC01OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzMyBDLTU5LjQ5MTI1MjA4NTgxNTI5IC0yNC44MjEyNTE5ODQ4OTg2MjcsIC01OS40Mjk0ODQyODAzMTc5NCAtMjQuOTA3NzYzMTc5MTg1MTI1LCAtNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEMtNTkuMjE1MjA2NjAzMDE1NTggLTI1LjE4NjM5ODE4NzgwMDI5LCAtNTkuMTE3NTIzMTU5MDQ5MTg1IC0yNS4zMDE3MzI4MTIxNTIyMzUsIC01OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTguOTYxNTM5OTY5ODEwMzk2IC0yNS40NzA2NzQ5NDE2ODM5NjcsIC01OC44NzcyNzI4ODYyNTUxMzUgLTI1LjU1NDk0MjAyNTIzOTIyOCwgLTU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IEMtNTguNjg1Mjg2MTI1MTM0MDQgLTI1LjczNjQ3NDUyMjUxNDAzNywgLTU4LjYxNjk3Njg5MjEzOTM3IC0yNS43OTQzMjk0OTE2NjI0MTMsIC01OC40MzgyNTEwNjM0NDgzNCAtMjUuOTQ1NzAyNTQ2OTgxOTY2IEMtNTguMzEwMjQzNTEyOTYyMTMgLTI2LjAzNzA5ODE4MTMzNjk4NywgLTU4LjE4MjIzNTk2MjQ3NTkzIC0yNi4xMjg0OTM4MTU2OTIwMDcsIC01OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBDLTU3Ljk2ODExOTg5MzcxODAwNCAtMjYuMjY1NTY0ODg5NDQ1NDIsIC01Ny44MzQzMTE0OTY4MjM4NyAtMjYuMzQ1Mjk3Mzg3NTc4MjAzLCAtNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42NTU3ODAwNjUxODA2NCAtMjYuNDQxOTI2NTM4MDkxMTEsIC01Ny41NjQ2MzU2NjUxNzU5MyAtMjYuNDg2NDg0MzIwMTQ5Nzc0LCAtNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQy01Ny4yNjc1ODQ5MzM5MTY4NCAtMjYuNjIxMDM5NDU4OTI1OTE0LCAtNTcuMTU5NTA1MjQ0NTY4ODM0IC0yNi42NjMyMTIyODQ1NzY1MzgsIC01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEMtNTYuODY3NTk2ODExMDUzNjY2IC0yNi43NjU3MzExMDQyMjE3NiwgLTU2Ljc0NDUwODc3NjA4MzkxIC0yNi44MDIzNzU5OTk5NDAzNSwgLTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNDYzMzIzMTkzNzExMzYgLTI2Ljg3NDUzMDMxMzA2ODIxLCAtNTYuMzMyMDMxNDUxNzE4NzIgLTI2LjkwMjA1OTI5NjQzOTc2NiwgLTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQy01Ni4wMjY1OTMxMTY1OTA5MjQgLTI2Ljk1MjE5NTE2NDAzNTM4OCwgLTU1Ljg2MzAyNTc4MTc3ODE4IC0yNi45NzI1ODM4MTEwNTcxNjMsIC01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEMtNTUuNjc3MzkzMTUzNDkxMDcgLTI2Ljk4NzE2OTc5OTI2NjI4LCAtNTUuNTc0NzAyMDc5NjIwNDY1IC0yNi45OTE0MTcxMzM0OTkyMSwgLTU1LjM2NzE4NzUgLTI3IEMtNTUuMzY3MTg3NSAtMjcsIC01NS4zNjcxODc1IC0yNywgLTU1LjM2NzE4NzUgLTI3IiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMCIgZmlsbD0iI0VDRUNGRiIgc3R5bGU9IiIvPjxwYXRoIGQ9Ik0tNTUuMzY3MTg3NSAtMjcgQy0yMC44NzY5OTY4ODI1OTMzNCAtMjcsIDEzLjYxMzE5MzczNDgxMzMyMiAtMjcsIDU1LjM2NzE4NzUgLTI3IE0tNTUuMzY3MTg3NSAtMjcgQy0xOC41MzAwMTUxNTE2ODU0MiAtMjcsIDE4LjMwNzE1NzE5NjYyOTE2MyAtMjcsIDU1LjM2NzE4NzUgLTI3IE01NS4zNjcxODc1IC0yNyBDNTUuMzY3MTg3NSAtMjcsIDU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNyBNNTUuMzY3MTg3NSAtMjcgQzU1LjM2NzE4NzUgLTI3LCA1NS4zNjcxODc1IC0yNywgNTUuMzY3MTg3NSAtMjcgTTU1LjM2NzE4NzUgLTI3IEM1NS40ODQzMzQ1NDgwOTcxNDUgLTI2Ljk5NTE1NDc2MjI1MDM1LCA1NS42MDE0ODE1OTYxOTQyOCAtMjYuOTkwMzA5NTI0NTAwNzAyLCA1NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IE01NS4zNjcxODc1IC0yNyBDNTUuNDg4NDQ1MjU0NjU4NjY0IC0yNi45OTQ5ODQ3NDIxNjkzMjYsIDU1LjYwOTcwMzAwOTMxNzMzIC0yNi45ODk5Njk0ODQzMzg2NDgsIDU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgTTU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgQzU1Ljk0MjU2MTM4MDkwODAyNSAtMjYuOTYyNjY5NzA4OTY0NzEsIDU2LjEwNTAzODUzNDQ1NDM5NCAtMjYuOTQyNDE2OTUyODk2MDc1LCA1Ni4xOTAxNjA0NTE0MDM2NyAtMjYuOTMxODA2NTE3MDEzNjEyIE01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IEM1NS45MzQ5NDA0NTU5NzQ0NCAtMjYuOTYzNjE5NjU2MjY3MDE0LCA1Ni4wODk3OTY2ODQ1ODcyMiAtMjYuOTQ0MzE2ODQ3NTAwNjgsIDU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgTTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQzU2LjMyMDYyNzAxMjcwMjIxIC0yNi45MDQ0NTA1NTU4Mzg3NCwgNTYuNDUxMDkzNTc0MDAwNzUgLTI2Ljg3NzA5NDU5NDY2Mzg3LCA1Ni41OTQ2MTQ5MzU3MDM5OTQgLTI2Ljg0NzAwMTMyOTY5NjY1MyBNNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBDNTYuMjg3MjM2NzYwNjYwNjcgLTI2LjkxMTQ1MTc1NTQ1MTcsIDU2LjM4NDMxMzA2OTkxNzY3IC0yNi44OTEwOTY5OTM4ODk3OSwgNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgTTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIEM1Ni43MzIyOTk3Mjk5NjM3NSAtMjYuODA2MDEwNzkwNDU0MzYyLCA1Ni44Njk5ODQ1MjQyMjM1MDQgLTI2Ljc2NTAyMDI1MTIxMjA3NSwgNTYuOTkwNjg0ODQ2MDIzNDIgLTI2LjcyOTA4NjIwODUwMzE3MyBNNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgQzU2Ljc0Mzk0OTkyMjYwMzcwNCAtMjYuODAyNTQyMzc3ODMwMDM2LCA1Ni44OTMyODQ5MDk1MDM0MTQgLTI2Ljc1ODA4MzQyNTk2MzQyNCwgNTYuOTkwNjg0ODQ2MDIzNDIgLTI2LjcyOTA4NjIwODUwMzE3MyBNNTYuOTkwNjg0ODQ2MDIzNDIgLTI2LjcyOTA4NjIwODUwMzE3MyBDNTcuMDkwOTU4Mjk1NTkwNTcgLTI2LjY4OTk1OTM4NzA2NzEzNCwgNTcuMTkxMjMxNzQ1MTU3NzE1IC0yNi42NTA4MzI1NjU2MzEwOTUsIDU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEM1Ny4wNzkzNzExMjc2Mzk4NDUgLTI2LjY5NDQ4MDcxNDAzMTY5LCA1Ny4xNjgwNTc0MDkyNTYyNjQgLTI2LjY1OTg3NTIxOTU2MDIxMywgNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgTTU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IEM1Ny40NTQ1MDYxMDIwNDY5MiAtMjYuNTQwMzIzMzgyOTEwMjMsIDU3LjUzMzM0NzU4MDgyOSAtMjYuNTAxNzgwMTMyNTQ1MTc1LCA1Ny43NDY5MjQ0NjUxODUzNjYgLTI2LjM5NzM2ODc1NjAzMjQ0NiBNNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQzU3LjQ4MDMwODU4MDY2MzY2IC0yNi41Mjc3MDkzMTk3NDU0MjYsIDU3LjU4NDk1MjUzODA2MjQ3NCAtMjYuNDc2NTUyMDA2MjE1NTYzLCA1Ny43NDY5MjQ0NjUxODUzNjYgLTI2LjM5NzM2ODc1NjAzMjQ0NiBNNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQzU3Ljg0ODAwMDYwMDI0NTY3IC0yNi4zMzcxNDA0NTI0NTA0MiwgNTcuOTQ5MDc2NzM1MzA1OTcgLTI2LjI3NjkxMjE0ODg2ODM5NCwgNTguMTAxOTI4MjkwNjEyMTM2IC0yNi4xODU4MzIzOTEzMTI2NDQgTTU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IEM1Ny44NDkwOTUyNzkyMTUzMSAtMjYuMzM2NDg4MTY1MzY3MzI4LCA1Ny45NTEyNjYwOTMyNDUyNjYgLTI2LjI3NTYwNzU3NDcwMjIwNywgNTguMTAxOTI4MjkwNjEyMTM2IC0yNi4xODU4MzIzOTEzMTI2NDQgTTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEM1OC4xNzcxNTk3NTk1OTQzOSAtMjYuMTMyMTE4MTU0ODg2OTMsIDU4LjI1MjM5MTIyODU3NjYzNCAtMjYuMDc4NDAzOTE4NDYxMjE2LCA1OC40MzgyNTEwNjM0NDgzNCAtMjUuOTQ1NzAyNTQ2OTgxOTcgTTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEM1OC4yMTU0MzkxMTM4NTI5NjYgLTI2LjEwNDc4NzIyMTI2NzIsIDU4LjMyODk0OTkzNzA5Mzc5IC0yNi4wMjM3NDIwNTEyMjE3NSwgNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk3IE01OC40MzgyNTEwNjM0NDgzNCAtMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjUzMjI0NTkxMTIyMjU5IC0yNS44NjYwOTI5NzE4MjM4MDQsIDU4LjYyNjI0MDc1ODk5NjgzIC0yNS43ODY0ODMzOTY2NjU2NCwgNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgTTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NyBDNTguNTM5MzI2OTE2MTk1MDE2IC0yNS44NjAwOTU2NjY1MzYzNywgNTguNjQwNDAyNzY4OTQxNyAtMjUuNzc0NDg4Nzg2MDkwNzY3LCA1OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBNNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgQzU4Ljg2NDk4MTMyODAwMTQ2IC0yNS41NjcyMzM1ODM0OTI5MDQsIDU4Ljk3NjM2NzI5Nzg3NDIxIC0yNS40NTU4NDc2MTM2MjAxNSwgNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBNNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgQzU4LjgzMzk2NTc4MzEwNTQwNSAtMjUuNTk4MjQ5MTI4Mzg4OTU3LCA1OC45MTQzMzYyMDgwODIxMDUgLTI1LjUxNzg3ODcwMzQxMjI1OCwgNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBNNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBDNTkuMTAzOTQ4MjQzNDczODYgLTI1LjMxNzc2MDY4NDY4NTM1NywgNTkuMTYyMDg5NDMzNTgyMDYgLTI1LjI0OTExMzUxMTI0MjAwNSwgNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IE01OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IEM1OS4xNTE3NjI2NzY1MjgzMzUgLTI1LjI2MTMwNjI5MDI1NTU2LCA1OS4yNTc3MTgyOTk2OTEwMiAtMjUuMTM2MjA0NzIyMzgyNDEzLCA1OS4zMTI4OTAwNDY5ODE5NyAtMjUuMDcxMDYzNTYzNDQ4MzQgTTU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBDNTkuMzYzNjg2OTQ0MzcxNzcgLTI0Ljk5OTkxODA4MjAxNDE4NywgNTkuNDE0NDgzODQxNzYxNTcgLTI0LjkyODc3MjYwMDU4MDAzMiwgNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzYgTTU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBDNTkuMzgyMDIxNzk5NzM0MzQgLTI0Ljk3NDIzODUxOTMwODExLCA1OS40NTExNTM1NTI0ODY3MTYgLTI0Ljg3NzQxMzQ3NTE2Nzg3NCwgNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzYgTTU5LjU1MzAxOTg5MTMxMjY0NCAtMjQuNzM0NzQwNzkwNjEyMTM2IEM1OS42MjUzMjQ3ODUwNzY2MiAtMjQuNjEzMzk3NTIyMTk5MjI1LCA1OS42OTc2Mjk2Nzg4NDA1OSAtMjQuNDkyMDU0MjUzNzg2MzEsIDU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNyBNNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzYgQzU5LjYyMDMwNDY4Mzk3OTUyIC0yNC42MjE4MjIzMzg4NzcyNDgsIDU5LjY4NzU4OTQ3NjY0NjM5IC0yNC41MDg5MDM4ODcxNDIzNjMsIDU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNyBNNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM3IEM1OS44MjA5NTkyOTA1MzY4MzQgLTI0LjI2NDM2MjcxNDAyNTg0NywgNTkuODc3MzYyMzI1MDQxMjIgLTI0LjE0ODk4ODQ2Mjg2NjMyNCwgNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBNNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM3IEM1OS44MzY4MzI2MzM5Mjg2MTYgLTI0LjIzMTg5MzI3MTE0MzYxLCA1OS45MDkxMDkwMTE4MjQ3OCAtMjQuMDg0MDQ5NTc3MTAxODUsIDU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgTTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgQzYwLjAwMTgzMTQ4OTAwNzEyIC0yMy44NjU1MzIwMTE0MDMyMjUsIDYwLjA1NzYwODg0NDczODk0NCAtMjMuNzIyNTg2ODk5NTQxNjA3LCA2MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBNNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuOTc4OTU0MTU4MDg3NDkgLTIzLjkyNDE2MTU4NTc5NzYyNCwgNjAuMDExODU0MTgyODk5NjggLTIzLjgzOTg0NjA0ODMzMDM5NywgNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgTTYwLjA5NjI3MzcwODUwMzE3NiAtMjMuNjIzNDk3MzQ2MDIzNDE3IEM2MC4xMjIzOTgxNTI1OTYyMDUgLTIzLjUzNTc0Njg4NTE0NjYyNiwgNjAuMTQ4NTIyNTk2Njg5MjMgLTIzLjQ0Nzk5NjQyNDI2OTg0LCA2MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IE02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBDNjAuMTMwODQzOTQwOTY3Njk0IC0yMy41MDczNzc5ODIwODMwMDcsIDYwLjE2NTQxNDE3MzQzMjIxIC0yMy4zOTEyNTg2MTgxNDI2LCA2MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IE02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4yMzI0NzQ3ODY0MDIxMiAtMjMuMTQwMjE3NzA5MjMyODA0LCA2MC4yNTA3NjA3NDMxMDc2IC0yMy4wNTMwMDc5ODI3NjE2MTcsIDYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBNNjAuMjE0MTg4ODI5Njk2NjUgLTIzLjIyNzQyNzQzNTcwMzk5NCBDNjAuMjQ1MTI2MDA0MzUzMDcgLTIzLjA3OTg4MTI4NDEzODk1LCA2MC4yNzYwNjMxNzkwMDk0OSAtMjIuOTMyMzM1MTMyNTczOSwgNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IE02MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgQzYwLjMxNjQ1MjIwMTc4MzAzIC0yMi42ODI5MTUxNjU3OTYyLCA2MC4zMzM5MTAzODY1NTI0NDYgLTIyLjU0Mjg1NzM4MDE4ODcyOCwgNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2MiBNNjAuMjk4OTk0MDE3MDEzNjEgLTIyLjgyMjk3Mjk1MTQwMzY3IEM2MC4zMTI4ODE3NDgzNjYzOTQgLTIyLjcxMTU1OTAyNTcyNTE0LCA2MC4zMjY3Njk0Nzk3MTkxNzUgLTIyLjYwMDE0NTEwMDA0NjYxLCA2MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYyIE02MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYyIEM2MC4zNTUzNzIxMTAwNjkzNTQgLTIyLjI4NTY2OTc5MDQyMjk3LCA2MC4zNjA2MzQyNTUxMDUzNyAtMjIuMTU4NDQyODUzNDg0Mjc0LCA2MC4zNjcxODc1IC0yMiBNNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzU1NDQ0OTUwNDc2NjQgLTIyLjI4MzkwODY3MTc0NTk1LCA2MC4zNjA3Nzk5MzU5MTk5MzYgLTIyLjE1NDkyMDYxNjEzMDIzNywgNjAuMzY3MTg3NSAtMjIgTTYwLjM2NzE4NzUgLTIyIEM2MC4zNjcxODc1IC0yMiwgNjAuMzY3MTg3NSAtMjIsIDYwLjM2NzE4NzUgLTIyIE02MC4zNjcxODc1IC0yMiBDNjAuMzY3MTg3NSAtMjIsIDYwLjM2NzE4NzUgLTIyLCA2MC4zNjcxODc1IC0yMiBNNjAuMzY3MTg3NSAtMjIgQzYwLjM2NzE4NzUgLTExLjE3MjE4NTYxMjY0NDIwOCwgNjAuMzY3MTg3NSAtMC4zNDQzNzEyMjUyODg0MTYxLCA2MC4zNjcxODc1IDIyIE02MC4zNjcxODc1IC0yMiBDNjAuMzY3MTg3NSAtNi42MTY3ODAwMDU2MTI3NTMsIDYwLjM2NzE4NzUgOC43NjY0Mzk5ODg3NzQ0OTMsIDYwLjM2NzE4NzUgMjIgTTYwLjM2NzE4NzUgMjIgQzYwLjM2NzE4NzUgMjIsIDYwLjM2NzE4NzUgMjIsIDYwLjM2NzE4NzUgMjIgTTYwLjM2NzE4NzUgMjIgQzYwLjM2NzE4NzUgMjIsIDYwLjM2NzE4NzUgMjIsIDYwLjM2NzE4NzUgMjIgTTYwLjM2NzE4NzUgMjIgQzYwLjM2MDUzNTEzOTM4MDYgMjIuMTYwODM5MjUwNzY2NzE3LCA2MC4zNTM4ODI3Nzg3NjEyMDQgMjIuMzIxNjc4NTAxNTMzNDMsIDYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNNjAuMzY3MTg3NSAyMiBDNjAuMzYzMzAwMTU2NDk0NjI0IDIyLjA5Mzk4NzMwMDU0NjM1MywgNjAuMzU5NDEyODEyOTg5MjQgMjIuMTg3OTc0NjAxMDkyNzA1LCA2MC4zNTAxMDk5NjUwMzMzNSAyMi40MTI4OTY3MjczNjE2NjIgTTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzM3NjEwNjU1NDYyNDkgMjIuNTEzMTcyMDc5MTk0NTQzLCA2MC4zMjUxMTEzNDU4OTE2MyAyMi42MTM0NDc0MzEwMjc0MiwgNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgTTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBDNjAuMzMxNzcwMTAxNTgwODk0IDIyLjU2MDAyNzc1NDg2NjUzNSwgNjAuMzEzNDMwMjM4MTI4NDMgMjIuNzA3MTU4NzgyMzcxNDEyLCA2MC4yOTg5OTQwMTcwMTM2MSAyMi44MjI5NzI5NTE0MDM2NyBNNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgQzYwLjI3MTg3NzQ0NzMzMTM4IDIyLjk1MjI5NzgwMjMyMTQ2NCwgNjAuMjQ0NzYwODc3NjQ5MTUgMjMuMDgxNjIyNjUzMjM5MjU4LCA2MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgTTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEM2MC4yNjkxNTcyNzQyMDkyOCAyMi45NjUyNzA5MDI4MTM0OTcsIDYwLjIzOTMyMDUzMTQwNDkzNSAyMy4xMDc1Njg4NTQyMjMzMjcsIDYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBNNjAuMjE0MTg4ODI5Njk2NjUgMjMuMjI3NDI3NDM1NzAzOTk0IEM2MC4xNzU4ODc5MDM2MTY2MiAyMy4zNTYwNzc5Nzg2MDQwNzMsIDYwLjEzNzU4Njk3NzUzNjU4NiAyMy40ODQ3Mjg1MjE1MDQxNTYsIDYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgTTYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBDNjAuMTg3MzI4NDI3ODI1ODc1IDIzLjMxNzY0OTkzNTE4MDA5MywgNjAuMTYwNDY4MDI1OTU1MSAyMy40MDc4NzI0MzQ2NTYxOTUsIDYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgTTYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQzYwLjA0NzI0NTYzMDE4MTM3IDIzLjc0OTE0NTU0MjI4NzI1LCA1OS45OTgyMTc1NTE4NTk1NzUgMjMuODc0NzkzNzM4NTUxMDc3LCA1OS45NDYwNTQxMzMyNzUyOSAyNC4wMDg0NzcxMjMyNjQ4NDYgTTYwLjA5NjI3MzcwODUwMzE3NiAyMy42MjM0OTczNDYwMjM0MTcgQzYwLjA2MzcyNjE3NDE0OTQ5NCAyMy43MDY5MDk1Mjc4NjYwODgsIDYwLjAzMTE3ODYzOTc5NTgyIDIzLjc5MDMyMTcwOTcwODc2LCA1OS45NDYwNTQxMzMyNzUyOSAyNC4wMDg0NzcxMjMyNjQ4NDYgTTU5Ljk0NjA1NDEzMzI3NTI5IDI0LjAwODQ3NzEyMzI2NDg0NiBDNTkuODg0ODA5ODA3MzMwMTM2IDI0LjEzMzc1NDM5NDM1MTA4LCA1OS44MjM1NjU0ODEzODQ5OCAyNC4yNTkwMzE2NjU0MzczMSwgNTkuNzY0NTU2MjU2MDMyNDUgMjQuMzc5NzM2OTY1MTg1MzY2IE01OS45NDYwNTQxMzMyNzUyOSAyNC4wMDg0NzcxMjMyNjQ4NDYgQzU5Ljg5MzY0NzM0MDk3NzA3NiAyNC4xMTU2NzY5MzAzMzU2NjcsIDU5Ljg0MTI0MDU0ODY3ODg2IDI0LjIyMjg3NjczNzQwNjQ5LCA1OS43NjQ1NTYyNTYwMzI0NSAyNC4zNzk3MzY5NjUxODUzNjYgTTU5Ljc2NDU1NjI1NjAzMjQ1IDI0LjM3OTczNjk2NTE4NTM2NiBDNTkuNzA5Nzc5OTM3NTg0OTggMjQuNDcxNjYzNDg4NjMzOTY1LCA1OS42NTUwMDM2MTkxMzc1MSAyNC41NjM1OTAwMTIwODI1NjQsIDU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgTTU5Ljc2NDU1NjI1NjAzMjQ1IDI0LjM3OTczNjk2NTE4NTM2NiBDNTkuNjk0NTk5NDQ2NDQ3OTYgMjQuNDk3MTM5NjM5ODk4MjU4LCA1OS42MjQ2NDI2MzY4NjM0NiAyNC42MTQ1NDIzMTQ2MTExNDYsIDU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgTTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQzU5LjQ3MjYwMzAwOTA3NzY0IDI0Ljg0NzM3MTY0MjAyMjk4NSwgNTkuMzkyMTg2MTI2ODQyNjQgMjQuOTYwMDAyNDkzNDMzODM3LCA1OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBNNTkuNTUzMDE5ODkxMzEyNjQ0IDI0LjczNDc0MDc5MDYxMjEzMyBDNTkuNDY2OTA5ODYyNDY0NjkgMjQuODU1MzQ1Mzg5OTc3NDIsIDU5LjM4MDc5OTgzMzYxNjc0IDI0Ljk3NTk0OTk4OTM0MjcwMiwgNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgTTU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4yNDYxNTg1OTk0MDcyNyAyNS4xNDk4NTMyMzQ3MTY4NiwgNTkuMTc5NDI3MTUxODMyNTggMjUuMjI4NjQyOTA1OTg1Mzg1LCA1OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgTTU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IEM1OS4yMTcwNDA3NTM0NTc2IDI1LjE4NDIzMjYxMDQ2NTUxOCwgNTkuMTIxMTkxNDU5OTMzMjM2IDI1LjI5NzQwMTY1NzQ4MjY5NSwgNTkuMDQ1ODA3MDUzMzY1NjYgMjUuMzg2NDA3ODU4MTI4NzA2IE01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQzU4Ljk1NTE2NDQ4MDI3NjA0NiAyNS40NzcwNTA0MzEyMTgzMiwgNTguODY0NTIxOTA3MTg2NDMgMjUuNTY3NjkzMDA0MzA3OTM0LCA1OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjU3IE01OS4wNDU4MDcwNTMzNjU2NiAyNS4zODY0MDc4NTgxMjg3MDYgQzU4Ljk1NjMxOTkyODEzMjcyIDI1LjQ3NTg5NDk4MzM2MTY0LCA1OC44NjY4MzI4MDI4OTk3ODYgMjUuNTY1MzgyMTA4NTk0NTc2LCA1OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjU3IE01OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjU3IEM1OC42NTg4OTc0MDkxMTg3NSAyNS43NTg4MjQ2MjQ4OTQwMzYsIDU4LjU2NDE5OTQ2MDEwODc5IDI1LjgzOTAyOTY5NjQyMjQxLCA1OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBNNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY1NyBDNTguNjQ4MDgyODk0MDUxNyAyNS43Njc5ODQwNTE5MzA5LCA1OC41NDI1NzA0Mjk5NzQ2OTUgMjUuODU3MzQ4NTUwNDk2MTM4LCA1OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBNNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQzU4LjMzMjQ3NDU0OTQ4NDcxIDI2LjAyMTIyNTUyNjM0NDExOCwgNTguMjI2Njk4MDM1NTIxMDcgMjYuMDk2NzQ4NTA1NzA2MjY2LCA1OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IE01OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBDNTguMzE0OTgyNzUwNTM3ODEgMjYuMDMzNzE0NDMwNzQ2MTY2LCA1OC4xOTE3MTQ0Mzc2MjcyNyAyNi4xMjE3MjYzMTQ1MTAzNiwgNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBNNTguMTAxOTI4MjkwNjEyMTM2IDI2LjE4NTgzMjM5MTMxMjY0NCBDNTcuOTYxMDQwNTEwNTE0MzQgMjYuMjY5NzgzMjg2MjA2NTM2LCA1Ny44MjAxNTI3MzA0MTY1MyAyNi4zNTM3MzQxODExMDA0MjUsIDU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgTTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgQzU4LjAwMTAwMzA5Nzg5ODUzIDI2LjI0NTk3MDc1Mjc3ODUxNSwgNTcuOTAwMDc3OTA1MTg0OTIgMjYuMzA2MTA5MTE0MjQ0Mzg2LCA1Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IE01Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IEM1Ny42NTIxMzMwMTA1ODcyNiAyNi40NDM3MDk0NzQ0Mzk1ODYsIDU3LjU1NzM0MTU1NTk4OTE0IDI2LjQ5MDA1MDE5Mjg0NjcyNywgNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBNNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBDNTcuNTk5OTQzNjk4OTE3NDMgMjYuNDY5MjIzMjc0MjQ1Nzc4LCA1Ny40NTI5NjI5MzI2NDk0OSAyNi41NDEwNzc3OTI0NTkxMSwgNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBNNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBDNTcuMjgwMTA3ODMyNDEyMDEgMjYuNjE2MTUzMDA4NzY5ODkyLCA1Ny4xODQ1NTEwNDE1NTkxNzUgMjYuNjUzNDM5Mzg0MjY0NDk1LCA1Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgTTU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgQzU3LjI0MDA2NTc0NjUxNzQ5NCAyNi42MzE3Nzc0NzkxNzA0MjQsIDU3LjEwNDQ2Njg2OTc3MDE1IDI2LjY4NDY4ODMyNTA2NTU2LCA1Ni45OTA2ODQ4NDYwMjM0MiAyNi43MjkwODYyMDg1MDMxNzMgTTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDNTYuODM5NTE3OTY2OTEwNDggMjYuNzc0MDkwNTM4MzExMTc4LCA1Ni42ODgzNTEwODc3OTc1MyAyNi44MTkwOTQ4NjgxMTkxODcsIDU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgTTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDNTYuOTA1Mjc2ODMzNTA0NTE2IDI2Ljc1NDUxMzI3NTUwNDksIDU2LjgxOTg2ODgyMDk4NTYwNCAyNi43Nzk5NDAzNDI1MDY2MywgNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBNNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNTExODgzMDI3NzE2ODA2IDI2Ljg2NDM0ODM4NjU5OTkzLCA1Ni40MjkxNTExMTk3Mjk2MiAyNi44ODE2OTU0NDM1MDMyMDUsIDU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBNNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBDNTYuNDc2Njg2Mjg5OTE1MzYgMjYuODcxNzI4MzY2NDQ0NTM2LCA1Ni4zNTg3NTc2NDQxMjY3MjYgMjYuODk2NDU1NDAzMTkyNDIsIDU2LjE5MDE2MDQ1MTQwMzY3IDI2LjkzMTgwNjUxNzAxMzYxMiBNNTYuMTkwMTYwNDUxNDAzNjcgMjYuOTMxODA2NTE3MDEzNjEyIEM1Ni4wNDYwODE0NjY4NjMzOCAyNi45NDk3NjU5NDM3MDcyNzcsIDU1LjkwMjAwMjQ4MjMyMzA5IDI2Ljk2NzcyNTM3MDQwMDk0NSwgNTUuNzgwMDg0MjI3MzYxNjYgMjYuOTgyOTIyNDY1MDMzMzQ3IE01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQzU2LjEwNDE1NjUxNzk3MDI1IDI2Ljk0MjUyNjg5NjEzNjE2NSwgNTYuMDE4MTUyNTg0NTM2ODMgMjYuOTUzMjQ3Mjc1MjU4NzE4LCA1NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgTTU1Ljc4MDA4NDIyNzM2MTY2IDI2Ljk4MjkyMjQ2NTAzMzM0NyBDNTUuNjc4Nzc2NTg4OTc4ODM1IDI2Ljk4NzExMjU3OTk1MTI1LCA1NS41Nzc0Njg5NTA1OTYwMTQgMjYuOTkxMzAyNjk0ODY5MTU1LCA1NS4zNjcxODc1IDI3IE01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgQzU1LjY0OTg0OTUzOTMxNDIzIDI2Ljk4ODMwOTAxMTU3NTQxNCwgNTUuNTE5NjE0ODUxMjY2OCAyNi45OTM2OTU1NTgxMTc0ODQsIDU1LjM2NzE4NzUgMjcgTTU1LjM2NzE4NzUgMjcgQzU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcgTTU1LjM2NzE4NzUgMjcgQzU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcsIDU1LjM2NzE4NzUgMjcgTTU1LjM2NzE4NzUgMjcgQzE2LjA1MjEyNTAxODQ5NjgzOCAyNywgLTIzLjI2MjkzNzQ2MzAwNjMyNCAyNywgLTU1LjM2NzE4NzUgMjcgTTU1LjM2NzE4NzUgMjcgQzE4LjU2NzM3OTM4NDUzNjA0NiAyNywgLTE4LjIzMjQyODczMDkyNzkwNyAyNywgLTU1LjM2NzE4NzUgMjcgTS01NS4zNjcxODc1IDI3IEMtNTUuMzY3MTg3NSAyNywgLTU1LjM2NzE4NzUgMjcsIC01NS4zNjcxODc1IDI3IE0tNTUuMzY3MTg3NSAyNyBDLTU1LjM2NzE4NzUgMjcsIC01NS4zNjcxODc1IDI3LCAtNTUuMzY3MTg3NSAyNyBNLTU1LjM2NzE4NzUgMjcgQy01NS41MDY3MzE1OTc5MzMwNiAyNi45OTQyMjg0MTM0MTY5MTMsIC01NS42NDYyNzU2OTU4NjYxMiAyNi45ODg0NTY4MjY4MzM4MjIsIC01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgTS01NS4zNjcxODc1IDI3IEMtNTUuNTAyMDQwODE0NTA0NDcgMjYuOTk0NDIyNDI1NjU0NjMsIC01NS42MzY4OTQxMjkwMDg5MyAyNi45ODg4NDQ4NTEzMDkyNjMsIC01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgTS01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgQy01NS44NzU3NDczODQzMDg5NjYgMjYuOTcwOTk4MDY0OTU1NDIsIC01NS45NzE0MTA1NDEyNTYyNyAyNi45NTkwNzM2NjQ4Nzc0OTQsIC01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgTS01NS43ODAwODQyMjczNjE2NiAyNi45ODI5MjI0NjUwMzMzNDcgQy01NS44OTI5MzE2OTE4NDU2NzUgMjYuOTY4ODU2MDQzMjU0ODE0LCAtNTYuMDA1Nzc5MTU2MzI5NjkgMjYuOTU0Nzg5NjIxNDc2MjgsIC01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgTS01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQy01Ni4yODU4NjYwNTkwMDI1MiAyNi45MTE3MzkxNjEzNjYwNTQsIC01Ni4zODE1NzE2NjY2MDEzNzQgMjYuODkxNjcxODA1NzE4NSwgLTU2LjU5NDYxNDkzNTcwMzk5NCAyNi44NDcwMDEzMjk2OTY2NTMgTS01Ni4xOTAxNjA0NTE0MDM2NyAyNi45MzE4MDY1MTcwMTM2MTIgQy01Ni4zMjg1OTE4ODQ3OTQzNSAyNi45MDI3ODA0OTc3ODI4OTIsIC01Ni40NjcwMjMzMTgxODUwMyAyNi44NzM3NTQ0Nzg1NTIxNzYsIC01Ni41OTQ2MTQ5MzU3MDM5OTQgMjYuODQ3MDAxMzI5Njk2NjUzIE0tNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBDLTU2LjczNjY3NjEwMjE3NTY5IDI2LjgwNDcwNzg4Nzk5NjIsIC01Ni44Nzg3MzcyNjg2NDczODQgMjYuNzYyNDE0NDQ2Mjk1NzU0LCAtNTYuOTkwNjg0ODQ2MDIzNDIgMjYuNzI5MDg2MjA4NTAzMTczIE0tNTYuNTk0NjE0OTM1NzAzOTk0IDI2Ljg0NzAwMTMyOTY5NjY1MyBDLTU2Ljc1MDk0MjgxMDcxNDUzIDI2LjgwMDQ2MDUwNDgzODQzNywgLTU2LjkwNzI3MDY4NTcyNTA2IDI2Ljc1MzkxOTY3OTk4MDIxOCwgLTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBNLTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDLTU3LjA3ODUxMzIyNjUzMTY5IDI2LjY5NDgxNTQ2ODA4Mjg3MiwgLTU3LjE2NjM0MTYwNzAzOTk2IDI2LjY2MDU0NDcyNzY2MjU3LCAtNTcuMzc1NjY0NjIzMjY0ODQ2IDI2LjU3ODg2NjYzMzI3NTI4NiBNLTU2Ljk5MDY4NDg0NjAyMzQyIDI2LjcyOTA4NjIwODUwMzE3MyBDLTU3LjEzNjM0MzYxMTMzMDc3IDI2LjY3MjI0OTk4MTkxMTAyMywgLTU3LjI4MjAwMjM3NjYzODEzIDI2LjYxNTQxMzc1NTMxODg3MywgLTU3LjM3NTY2NDYyMzI2NDg0NiAyNi41Nzg4NjY2MzMyNzUyODYgTS01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNTEzMDUzMjM0NDk2NDMgMjYuNTExNzAxNDMzOTE4MTk0LCAtNTcuNjUwNDQxODQ1NzI4MDIgMjYuNDQ0NTM2MjM0NTYxMSwgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAyNi4zOTczNjg3NTYwMzI0NDYgTS01Ny4zNzU2NjQ2MjMyNjQ4NDYgMjYuNTc4ODY2NjMzMjc1Mjg2IEMtNTcuNTAwMjY5NjUxMjc1NzkgMjYuNTE3OTUwOTQ2OTQ0MTcsIC01Ny42MjQ4NzQ2NzkyODY3MyAyNi40NTcwMzUyNjA2MTMwNTQsIC01Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IDI2LjM5NzM2ODc1NjAzMjQ0NiBDLTU3LjgyMDEwNDI3MDEzNTEzIDI2LjM1Mzc2MzA1NzE2MDQxOCwgLTU3Ljg5MzI4NDA3NTA4NDkxIDI2LjMxMDE1NzM1ODI4ODM4NiwgLTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgTS01Ny43NDY5MjQ0NjUxODUzNjYgMjYuMzk3MzY4NzU2MDMyNDQ2IEMtNTcuODMxNzI2MTE1MjkyMjMgMjYuMzQ2ODM3OTQwNTg0ODA1LCAtNTcuOTE2NTI3NzY1Mzk5MDk2IDI2LjI5NjMwNzEyNTEzNzE2NSwgLTU4LjEwMTkyODI5MDYxMjEzNiAyNi4xODU4MzIzOTEzMTI2NDQgTS01OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTguMTkzOTgzNTYwNjc4MDUgMjYuMTIwMTA2MTkxNzkyMDY1LCAtNTguMjg2MDM4ODMwNzQzOTcgMjYuMDU0Mzc5OTkyMjcxNDg1LCAtNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgTS01OC4xMDE5MjgyOTA2MTIxMzYgMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTguMTc1NDIzMTk3OTAxNDggMjYuMTMzMzU4MDM2MTA0MjIzLCAtNTguMjQ4OTE4MTA1MTkwODI1IDI2LjA4MDg4MzY4MDg5NTgwNiwgLTU4LjQzODI1MTA2MzQ0ODM0IDI1Ljk0NTcwMjU0Njk4MTk3IE0tNTguNDM4MjUxMDYzNDQ4MzQgMjUuOTQ1NzAyNTQ2OTgxOTcgQy01OC41NDYzNDY5MDM4NDQyMyAyNS44NTQxNTAwNDAyODU1MzYsIC01OC42NTQ0NDI3NDQyNDAxMyAyNS43NjI1OTc1MzM1ODkxMDUsIC01OC43NTM1OTUzNTgxMjg3MDYgMjUuNjc4NjE5NTUzMzY1NjYgTS01OC40MzgyNTEwNjM0NDgzNCAyNS45NDU3MDI1NDY5ODE5NyBDLTU4LjUzOTgzMTk2MTk2MTcyNCAyNS44NTk2Njc5MTQ1OTE3MTgsIC01OC42NDE0MTI4NjA0NzUxIDI1Ljc3MzYzMzI4MjIwMTQ3LCAtNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IE0tNTguNzUzNTk1MzU4MTI4NzA2IDI1LjY3ODYxOTU1MzM2NTY2IEMtNTguODQ0OTAxNTE3NTcyNDQgMjUuNTg3MzEzMzkzOTIxOTMsIC01OC45MzYyMDc2NzcwMTYxNjQgMjUuNDk2MDA3MjM0NDc4MiwgLTU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBNLTU4Ljc1MzU5NTM1ODEyODcwNiAyNS42Nzg2MTk1NTMzNjU2NiBDLTU4Ljg0MzYzNDEyMDk4MTIgMjUuNTg4NTgwNzkwNTEzMTY2LCAtNTguOTMzNjcyODgzODMzNjk0IDI1LjQ5ODU0MjAyNzY2MDY3MiwgLTU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBNLTU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBDLTU5LjEyNTc3NDEyNDg0MDcgMjUuMjkxOTkwOTE1MjUwNzIsIC01OS4yMDU3NDExOTYzMTU3NSAyNS4xOTc1NzM5NzIzNzI3MzcsIC01OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBNLTU5LjA0NTgwNzA1MzM2NTY2IDI1LjM4NjQwNzg1ODEyODcwNiBDLTU5LjE0MjQxMTUyNjk5MDk3NiAyNS4yNzIzNDcxNzE2NTExOTgsIC01OS4yMzkwMTYwMDA2MTYyOTYgMjUuMTU4Mjg2NDg1MTczNjksIC01OS4zMTI4OTAwNDY5ODE5NyAyNS4wNzEwNjM1NjM0NDgzNCBNLTU5LjMxMjg5MDA0Njk4MTk3IDI1LjA3MTA2MzU2MzQ0ODM0IEMtNTkuMzg5MjE3MTg5NjE2MzMgMjQuOTY0MTYwNzQ4NzQ0MDQyLCAtNTkuNDY1NTQ0MzMyMjUwNyAyNC44NTcyNTc5MzQwMzk3NDQsIC01OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIE0tNTkuMzEyODkwMDQ2OTgxOTcgMjUuMDcxMDYzNTYzNDQ4MzQgQy01OS4zOTc1NzY2MTQ4NDA1MjQgMjQuOTUyNDUyNjQ1MjQ0NjUsIC01OS40ODIyNjMxODI2OTkwOCAyNC44MzM4NDE3MjcwNDA5NiwgLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgTS01OS41NTMwMTk4OTEzMTI2NDQgMjQuNzM0NzQwNzkwNjEyMTMzIEMtNTkuNjA3Mjc4NjI3MTgwMTkgMjQuNjQzNjgyODgyODA4MzU2LCAtNTkuNjYxNTM3MzYzMDQ3NzUgMjQuNTUyNjI0OTc1MDA0NTgsIC01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBNLTU5LjU1MzAxOTg5MTMxMjY0NCAyNC43MzQ3NDA3OTA2MTIxMzMgQy01OS42MTE0ODE3MzgyNTI2NyAyNC42MzY2MjkxNTIzMDAxMzQsIC01OS42Njk5NDM1ODUxOTI3IDI0LjUzODUxNzUxMzk4ODE0LCAtNTkuNzY0NTU2MjU2MDMyNDQgMjQuMzc5NzM2OTY1MTg1MzcgTS01OS43NjQ1NTYyNTYwMzI0NCAyNC4zNzk3MzY5NjUxODUzNyBDLTU5LjgxMTMwOTg2NjA3NzMxIDI0LjI4NDEwMDkyNzI0MjkxNCwgLTU5Ljg1ODA2MzQ3NjEyMjE3NiAyNC4xODg0NjQ4ODkzMDA0NjIsIC01OS45NDYwNTQxMzMyNzUyOCAyNC4wMDg0NzcxMjMyNjQ4NSBNLTU5Ljc2NDU1NjI1NjAzMjQ0IDI0LjM3OTczNjk2NTE4NTM3IEMtNTkuODI2NTI3Mzk5OTI3OTcgMjQuMjUyOTcyOTY0MjE4NTU3LCAtNTkuODg4NDk4NTQzODIzNDkgMjQuMTI2MjA4OTYzMjUxNzQ1LCAtNTkuOTQ2MDU0MTMzMjc1MjggMjQuMDA4NDc3MTIzMjY0ODUgTS01OS45NDYwNTQxMzMyNzUyOCAyNC4wMDg0NzcxMjMyNjQ4NSBDLTYwLjAwNTc5MTA0NTQ4MTA3IDIzLjg1NTM4NDUzNzgzMzI1LCAtNjAuMDY1NTI3OTU3Njg2ODU0IDIzLjcwMjI5MTk1MjQwMTY1LCAtNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBNLTU5Ljk0NjA1NDEzMzI3NTI4IDI0LjAwODQ3NzEyMzI2NDg1IEMtNTkuOTk4MzgzODI4NDU0MzE1IDIzLjg3NDM2NzYwODE1OTU2LCAtNjAuMDUwNzEzNTIzNjMzMzQgMjMuNzQwMjU4MDkzMDU0MjcsIC02MC4wOTYyNzM3MDg1MDMxNzYgMjMuNjIzNDk3MzQ2MDIzNDE3IE0tNjAuMDk2MjczNzA4NTAzMTc2IDIzLjYyMzQ5NzM0NjAyMzQxNyBDLTYwLjEzMDc5MjA2MjEwOTA5NCAyMy41MDc1NTIyNDAwOTcyODcsIC02MC4xNjUzMTA0MTU3MTUwMSAyMy4zOTE2MDcxMzQxNzExNTYsIC02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgTS02MC4wOTYyNzM3MDg1MDMxNzYgMjMuNjIzNDk3MzQ2MDIzNDE3IEMtNjAuMTM0Mjk5NzA3NjE4NzUgMjMuNDk1NzcwMjY2NTE4NSwgLTYwLjE3MjMyNTcwNjczNDMyIDIzLjM2ODA0MzE4NzAxMzU4MywgLTYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBNLTYwLjIxNDE4ODgyOTY5NjY1IDIzLjIyNzQyNzQzNTcwMzk5NCBDLTYwLjIzNDU2NDM2NDE3OTExIDIzLjEzMDI1MjA1NTg0NjEwNywgLTYwLjI1NDkzOTg5ODY2MTU2IDIzLjAzMzA3NjY3NTk4ODIyLCAtNjAuMjk4OTk0MDE3MDEzNjEgMjIuODIyOTcyOTUxNDAzNjcgTS02MC4yMTQxODg4Mjk2OTY2NSAyMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4yNDMyMjYxNTYyNzYyNiAyMy4wODg5NDIwNzUwOTM2ODQsIC02MC4yNzIyNjM0ODI4NTU4NiAyMi45NTA0NTY3MTQ0ODMzNzYsIC02MC4yOTg5OTQwMTcwMTM2MSAyMi44MjI5NzI5NTE0MDM2NyBNLTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMzE2NzI1MzI1MTQ5MTMgMjIuNjgwNzI0MDQxNDQwNTksIC02MC4zMzQ0NTY2MzMyODQ2NCAyMi41Mzg0NzUxMzE0Nzc1MSwgLTYwLjM1MDEwOTk2NTAzMzM1IDIyLjQxMjg5NjcyNzM2MTY2MiBNLTYwLjI5ODk5NDAxNzAxMzYxIDIyLjgyMjk3Mjk1MTQwMzY3IEMtNjAuMzEwMTYwMzAxMTUwMDU2IDIyLjczMzM5MTc1NzgxMDU0OCwgLTYwLjMyMTMyNjU4NTI4NjQ5IDIyLjY0MzgxMDU2NDIxNzQzLCAtNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIE0tNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIEMtNjAuMzU1NjE1NzMwNzgxNjMgMjIuMjc5Nzc5NTg0NjYzNjgsIC02MC4zNjExMjE0OTY1Mjk5MSAyMi4xNDY2NjI0NDE5NjU2OTUsIC02MC4zNjcxODc1IDIyIE0tNjAuMzUwMTA5OTY1MDMzMzUgMjIuNDEyODk2NzI3MzYxNjYyIEMtNjAuMzU2NTIwMTAzODYzNDEgMjIuMjU3OTEzODU5NTE1OTYsIC02MC4zNjI5MzAyNDI2OTM0NyAyMi4xMDI5MzA5OTE2NzAyNTIsIC02MC4zNjcxODc1IDIyIE0tNjAuMzY3MTg3NSAyMiBDLTYwLjM2NzE4NzUgMjIsIC02MC4zNjcxODc1IDIyLCAtNjAuMzY3MTg3NSAyMiBNLTYwLjM2NzE4NzUgMjIgQy02MC4zNjcxODc1IDIyLCAtNjAuMzY3MTg3NSAyMiwgLTYwLjM2NzE4NzUgMjIgTS02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSA0LjQ1MDM4OTkzODg2MTMzNSwgLTYwLjM2NzE4NzUgLTEzLjA5OTIyMDEyMjI3NzMzLCAtNjAuMzY3MTg3NSAtMjIgTS02MC4zNjcxODc1IDIyIEMtNjAuMzY3MTg3NSA1LjM3ODAwNDcwMDYxNzUxNywgLTYwLjM2NzE4NzUgLTExLjI0Mzk5MDU5ODc2NDk2NywgLTYwLjM2NzE4NzUgLTIyIE0tNjAuMzY3MTg3NSAtMjIgQy02MC4zNjcxODc1IC0yMiwgLTYwLjM2NzE4NzUgLTIyLCAtNjAuMzY3MTg3NSAtMjIgTS02MC4zNjcxODc1IC0yMiBDLTYwLjM2NzE4NzUgLTIyLCAtNjAuMzY3MTg3NSAtMjIsIC02MC4zNjcxODc1IC0yMiBNLTYwLjM2NzE4NzUgLTIyIEMtNjAuMzYxNDYwMTkyNjMwNzkgLTIyLjEzODQ3MzUyNTI0NTkxNywgLTYwLjM1NTczMjg4NTI2MTU3NCAtMjIuMjc2OTQ3MDUwNDkxODM1LCAtNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IE0tNjAuMzY3MTg3NSAtMjIgQy02MC4zNjE1OTQ3MjczNDAyNiAtMjIuMTM1MjIwNzc1ODY2NzksIC02MC4zNTYwMDE5NTQ2ODA1MyAtMjIuMjcwNDQxNTUxNzMzNTgzLCAtNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IE0tNjAuMzUwMTA5OTY1MDMzMzUgLTIyLjQxMjg5NjcyNzM2MTY2IEMtNjAuMzMyODUxNzMyODY2MTMgLTIyLjU1MTM1MDM5ODk2NDEzLCAtNjAuMzE1NTkzNTAwNjk4OTEgLTIyLjY4OTgwNDA3MDU2NjYsIC02MC4yOTg5OTQwMTcwMTM2MSAtMjIuODIyOTcyOTUxNDAzNjcgTS02MC4zNTAxMDk5NjUwMzMzNSAtMjIuNDEyODk2NzI3MzYxNjYgQy02MC4zMzEwMzAwNzEzMzY0MyAtMjIuNTY1OTY0NjI2MjM2MTEsIC02MC4zMTE5NTAxNzc2Mzk1IC0yMi43MTkwMzI1MjUxMTA1NiwgLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBNLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDLTYwLjI4MTgyNDc2NjE5MjM1IC0yMi45MDQ4NTY4NjMzMDE0NDcsIC02MC4yNjQ2NTU1MTUzNzEwOSAtMjIuOTg2NzQwNzc1MTk5MjIzLCAtNjAuMjE0MTg4ODI5Njk2NjUgLTIzLjIyNzQyNzQzNTcwMzk5NCBNLTYwLjI5ODk5NDAxNzAxMzYxIC0yMi44MjI5NzI5NTE0MDM2NyBDLTYwLjI3MzgzMzQ2NzEwNzI0NSAtMjIuOTQyOTY5MTE2MjQ5Nzg1LCAtNjAuMjQ4NjcyOTE3MjAwODg1IC0yMy4wNjI5NjUyODEwOTU5LCAtNjAuMjE0MTg4ODI5Njk2NjUgLTIzLjIyNzQyNzQzNTcwMzk5NCBNLTYwLjIxNDE4ODgyOTY5NjY1IC0yMy4yMjc0Mjc0MzU3MDM5OTQgQy02MC4xNzIwMDkyNjUyMjY3OCAtMjMuMzY5MTA2MDk1MzAwODg2LCAtNjAuMTI5ODI5NzAwNzU2ODkgLTIzLjUxMDc4NDc1NDg5Nzc4LCAtNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgTS02MC4yMTQxODg4Mjk2OTY2NSAtMjMuMjI3NDI3NDM1NzAzOTk0IEMtNjAuMTc0Nzg2NzQ5MjUyOTUgLTIzLjM1OTc3NjY5MDkxMjEzNSwgLTYwLjEzNTM4NDY2ODgwOTI2IC0yMy40OTIxMjU5NDYxMjAyNzUsIC02MC4wOTYyNzM3MDg1MDMxNzYgLTIzLjYyMzQ5NzM0NjAyMzQxNyBNLTYwLjA5NjI3MzcwODUwMzE3NiAtMjMuNjIzNDk3MzQ2MDIzNDE3IEMtNjAuMDQ2MjgzODUzMzk3OTggLTIzLjc1MTYxMDM2NDkzOTQ1MiwgLTU5Ljk5NjI5Mzk5ODI5Mjc5IC0yMy44Nzk3MjMzODM4NTU0ODUsIC01OS45NDYwNTQxMzMyNzUyOSAtMjQuMDA4NDc3MTIzMjY0ODQ2IE0tNjAuMDk2MjczNzA4NTAzMTc2IC0yMy42MjM0OTczNDYwMjM0MTcgQy02MC4wNTE0Njg3NDUyMjMyNiAtMjMuNzM4MzIyNjI1OTk1MzgsIC02MC4wMDY2NjM3ODE5NDMzNDUgLTIzLjg1MzE0NzkwNTk2NzM0NywgLTU5Ljk0NjA1NDEzMzI3NTI5IC0yNC4wMDg0NzcxMjMyNjQ4NDYgTS01OS45NDYwNTQxMzMyNzUyOSAtMjQuMDA4NDc3MTIzMjY0ODQ2IEMtNTkuODgzMzkwNzcwNjUwNzIgLTI0LjEzNjY1NzA4MDI3MzgxNCwgLTU5LjgyMDcyNzQwODAyNjE1IC0yNC4yNjQ4MzcwMzcyODI3ODUsIC01OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzY2IE0tNTkuOTQ2MDU0MTMzMjc1MjkgLTI0LjAwODQ3NzEyMzI2NDg0NiBDLTU5LjkwNzUxNDgxNTMyMDU3IC0yNC4wODczMTA1NTgxNzMwMiwgLTU5Ljg2ODk3NTQ5NzM2NTg0NSAtMjQuMTY2MTQzOTkzMDgxMTk2LCAtNTkuNzY0NTU2MjU2MDMyNDUgLTI0LjM3OTczNjk2NTE4NTM2NiBNLTU5Ljc2NDU1NjI1NjAzMjQ1IC0yNC4zNzk3MzY5NjUxODUzNjYgQy01OS42ODYxOTkyNjI0NjUwNiAtMjQuNTExMjM2OTY3NTcxMjksIC01OS42MDc4NDIyNjg4OTc2NyAtMjQuNjQyNzM2OTY5OTU3MjEzLCAtNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzMgTS01OS43NjQ1NTYyNTYwMzI0NSAtMjQuMzc5NzM2OTY1MTg1MzY2IEMtNTkuNjk4NjE2NjM0MDczODggLTI0LjQ5MDM5NzkyOTE5MjYzMiwgLTU5LjYzMjY3NzAxMjExNTMxNiAtMjQuNjAxMDU4ODkzMTk5ODk4LCAtNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzMgTS01OS41NTMwMTk4OTEzMTI2NDQgLTI0LjczNDc0MDc5MDYxMjEzMyBDLTU5LjQ3NDcyMzM3OTAxODE5NCAtMjQuODQ0NDAxODc5MTQzOTM0LCAtNTkuMzk2NDI2ODY2NzIzNzQgLTI0Ljk1NDA2Mjk2NzY3NTc0LCAtNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IE0tNTkuNTUzMDE5ODkxMzEyNjQ0IC0yNC43MzQ3NDA3OTA2MTIxMzMgQy01OS41MDQ1Nzk2OTc4MjI0NyAtMjQuODAyNTg1NTAyODYyNDg4LCAtNTkuNDU2MTM5NTA0MzMyMyAtMjQuODcwNDMwMjE1MTEyODQ3LCAtNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IE0tNTkuMzEyODkwMDQ2OTgxOTcgLTI1LjA3MTA2MzU2MzQ0ODM0IEMtNTkuMjQ0MDkzMTg2OTk1MjYgLTI1LjE1MjI5MTg2MjU0MzY0MiwgLTU5LjE3NTI5NjMyNzAwODU0NiAtMjUuMjMzNTIwMTYxNjM4OTQzLCAtNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBNLTU5LjMxMjg5MDA0Njk4MTk3IC0yNS4wNzEwNjM1NjM0NDgzNCBDLTU5LjIxNDgxNjAwNzI3MDc3IC0yNS4xODY4NTkzNjMzMjQ3MzIsIC01OS4xMTY3NDE5Njc1NTk1OCAtMjUuMzAyNjU1MTYzMjAxMTIzLCAtNTkuMDQ1ODA3MDUzMzY1NjYgLTI1LjM4NjQwNzg1ODEyODcwNiBNLTU5LjA0NTgwNzA1MzM2NTY2IC0yNS4zODY0MDc4NTgxMjg3MDYgQy01OC45NDYzMDU2MjE3MjExNSAtMjUuNDg1OTA5Mjg5NzczMjE1LCAtNTguODQ2ODA0MTkwMDc2NjQgLTI1LjU4NTQxMDcyMTQxNzcyLCAtNTguNzUzNTk1MzU4MTI4NzA2IC0yNS42Nzg2MTk1NTMzNjU2NTcgTS01OS4wNDU4MDcwNTMzNjU2NiAtMjUuMzg2NDA3ODU4MTI4NzA2IEMtNTguOTU4NzgyNjI2OTY3NjA0IC0yNS40NzM0MzIyODQ1MjY3NiwgLTU4Ljg3MTc1ODIwMDU2OTU1IC0yNS41NjA0NTY3MTA5MjQ4MDcsIC01OC43NTM1OTUzNTgxMjg3MDYgLTI1LjY3ODYxOTU1MzM2NTY1NyBNLTU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IEMtNTguNjMxMjA4MDE4MTQ4NTM0IC0yNS43ODIyNzYzNDI3NjcxMzcsIC01OC41MDg4MjA2NzgxNjgzNiAtMjUuODg1OTMzMTMyMTY4NjE3LCAtNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk2NiBNLTU4Ljc1MzU5NTM1ODEyODcwNiAtMjUuNjc4NjE5NTUzMzY1NjU3IEMtNTguNjc2MTM4MTg1NjgwNTg1IC0yNS43NDQyMjI0MzIwMDgwODcsIC01OC41OTg2ODEwMTMyMzI0NjUgLTI1LjgwOTgyNTMxMDY1MDUyLCAtNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk2NiBNLTU4LjQzODI1MTA2MzQ0ODM0IC0yNS45NDU3MDI1NDY5ODE5NjYgQy01OC4zMzQxMjA3OTEzMDE1IC0yNi4wMjAwNTAxMzIyNzg4NzUsIC01OC4yMjk5OTA1MTkxNTQ2NTQgLTI2LjA5NDM5NzcxNzU3NTc4NCwgLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IE0tNTguNDM4MjUxMDYzNDQ4MzQgLTI1Ljk0NTcwMjU0Njk4MTk2NiBDLTU4LjMzMzQxNTUwNzUwNTUxIC0yNi4wMjA1NTM2OTUyMzAzNTgsIC01OC4yMjg1Nzk5NTE1NjI2NyAtMjYuMDk1NDA0ODQzNDc4NzUsIC01OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBNLTU4LjEwMTkyODI5MDYxMjEzNiAtMjYuMTg1ODMyMzkxMzEyNjQ0IEMtNTcuOTg1NTUyMjI5Mjg0MDk2IC0yNi4yNTUxNzc0NzIwOTkxNjMsIC01Ny44NjkxNzYxNjc5NTYwNTYgLTI2LjMyNDUyMjU1Mjg4NTY4LCAtNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgTS01OC4xMDE5MjgyOTA2MTIxMzYgLTI2LjE4NTgzMjM5MTMxMjY0NCBDLTU3Ljk2MjYxMzUyNTg2NDYgLTI2LjI2ODg0NTk3MjUwNzMzLCAtNTcuODIzMjk4NzYxMTE3MDYgLTI2LjM1MTg1OTU1MzcwMjAxMywgLTU3Ljc0NjkyNDQ2NTE4NTM2NiAtMjYuMzk3MzY4NzU2MDMyNDQ2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MDI1MDg3OTcyNDUwMyAtMjYuNDY3OTY5Mjc0MDg1MjEzLCAtNTcuNDU4MDkzMTI5MzA0NjkgLTI2LjUzODU2OTc5MjEzNzk4NCwgLTU3LjM3NTY2NDYyMzI2NDg0NiAtMjYuNTc4ODY2NjMzMjc1Mjg2IE0tNTcuNzQ2OTI0NDY1MTg1MzY2IC0yNi4zOTczNjg3NTYwMzI0NDYgQy01Ny42MTMwMjA2NzMwNjY4MDYgLTI2LjQ2MjgzMDMzMTEzNDU3NiwgLTU3LjQ3OTExNjg4MDk0ODI1NCAtMjYuNTI4MjkxOTA2MjM2NzAzLCAtNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgTS01Ny4zNzU2NjQ2MjMyNjQ4NDYgLTI2LjU3ODg2NjYzMzI3NTI4NiBDLTU3LjI2MTk3MTEzNjQzNzAzIC0yNi42MjMyMjk5Njk0OTk5MzUsIC01Ny4xNDgyNzc2NDk2MDkyMyAtMjYuNjY3NTkzMzA1NzI0NTgsIC01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIE0tNTcuMzc1NjY0NjIzMjY0ODQ2IC0yNi41Nzg4NjY2MzMyNzUyODYgQy01Ny4yNTk3MzY4Nzc0MjY3MiAtMjYuNjI0MTAxNzgwMDcxMDg2LCAtNTcuMTQzODA5MTMxNTg4NTkgLTI2LjY2OTMzNjkyNjg2Njg4MywgLTU2Ljk5MDY4NDg0NjAyMzQyIC0yNi43MjkwODYyMDg1MDMxNzMgTS01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEMtNTYuODMzMDQ3MzMxNjM3Nzg2IC0yNi43NzYwMTY5Mjk4OTYzNSwgLTU2LjY3NTQwOTgxNzI1MjE0NSAtMjYuODIyOTQ3NjUxMjg5NTI1LCAtNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgTS01Ni45OTA2ODQ4NDYwMjM0MiAtMjYuNzI5MDg2MjA4NTAzMTczIEMtNTYuOTA4MTY4MjY0ODA3NDkgLTI2Ljc1MzY1MjQ1OTEwOTU0LCAtNTYuODI1NjUxNjgzNTkxNTYgLTI2Ljc3ODIxODcwOTcxNTkwNSwgLTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIE0tNTYuNTk0NjE0OTM1NzAzOTk0IC0yNi44NDcwMDEzMjk2OTY2NTMgQy01Ni40OTE3NzM4ODQ4OTkyNDYgLTI2Ljg2ODU2NDgzMDQ1MTE1LCAtNTYuMzg4OTMyODM0MDk0NDkgLTI2Ljg5MDEyODMzMTIwNTY1LCAtNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBNLTU2LjU5NDYxNDkzNTcwMzk5NCAtMjYuODQ3MDAxMzI5Njk2NjUzIEMtNTYuNDM5NDQxOTE2NjE2MTI2IC0yNi44Nzk1Mzc2OTAzMDM1ODIsIC01Ni4yODQyNjg4OTc1MjgyNSAtMjYuOTEyMDc0MDUwOTEwNTEyLCAtNTYuMTkwMTYwNDUxNDAzNjcgLTI2LjkzMTgwNjUxNzAxMzYxMiBNLTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQy01Ni4wMjk3NzYyNjk4MDI4MyAtMjYuOTUxNzk4Mzg0NDAxMzEsIC01NS44NjkzOTIwODgyMDIgLTI2Ljk3MTc5MDI1MTc4OTAxLCAtNTUuNzgwMDg0MjI3MzYxNjYgLTI2Ljk4MjkyMjQ2NTAzMzM0NyBNLTU2LjE5MDE2MDQ1MTQwMzY3IC0yNi45MzE4MDY1MTcwMTM2MTIgQy01Ni4wNTk0MzcxNzg5NDQ2OSAtMjYuOTQ4MTAxMTU1OTMyNDk1LCAtNTUuOTI4NzEzOTA2NDg1NzA0IC0yNi45NjQzOTU3OTQ4NTEzNzcsIC01NS43ODAwODQyMjczNjE2NiAtMjYuOTgyOTIyNDY1MDMzMzQ3IE0tNTUuNzgwMDg0MjI3MzYxNjYgLTI2Ljk4MjkyMjQ2NTAzMzM0NyBDLTU1LjY2NTIzNDQ0MDkyNDg4IC0yNi45ODc2NzI2ODczMzc4OSwgLTU1LjU1MDM4NDY1NDQ4ODA5IC0yNi45OTI0MjI5MDk2NDI0MzYsIC01NS4zNjcxODc1IC0yNyBNLTU1Ljc4MDA4NDIyNzM2MTY2IC0yNi45ODI5MjI0NjUwMzMzNDcgQy01NS42NjM0NzM5NDM5NzU3NCAtMjYuOTg3NzQ1NTAyMDMwMzIsIC01NS41NDY4NjM2NjA1ODk4MiAtMjYuOTkyNTY4NTM5MDI3MjkyLCAtNTUuMzY3MTg3NSAtMjcgTS01NS4zNjcxODc1IC0yNyBDLTU1LjM2NzE4NzUgLTI3LCAtNTUuMzY3MTg3NSAtMjcsIC01NS4zNjcxODc1IC0yNyBNLTU1LjM2NzE4NzUgLTI3IEMtNTUuMzY3MTg3NSAtMjcsIC01NS4zNjcxODc1IC0yNywgLTU1LjM2NzE4NzUgLTI3IiBzdHJva2U9IiM5MzcwREIiIHN0cm9rZS13aWR0aD0iMS4zIiBmaWxsPSJub25lIiBzdHJva2UtZGFzaGFycmF5PSIwIDAiIHN0eWxlPSIiLz48L2c+PGcgY2xhc3M9ImxhYmVsIiBzdHlsZT0iIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDUuMzY3MTg3NSwgLTEyKSI+PHJlY3QvPjxmb3JlaWduT2JqZWN0IHdpZHRoPSI5MC43MzQzNzUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0ibm9kZUxhYmVsIj48cD5HbyBzaG9wcGluZzwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCIgaWQ9ImZsb3djaGFydC1DLTMiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMS44NTE1NjI1LCAzMTAuNTc4MTI1KSI+PHBvbHlnb24gcG9pbnRzPSI3MC41NzgxMjUsMCAxNDEuMTU2MjUsLTcwLjU3ODEyNSA3MC41NzgxMjUsLTE0MS4xNTYyNSAwLC03MC41NzgxMjUiIGNsYXNzPSJsYWJlbC1jb250YWluZXIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC03MC4wNzgxMjUsIDcwLjU3ODEyNSkiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00My41NzgxMjUsIC0xMikiPjxyZWN0Lz48Zm9yZWlnbk9iamVjdCB3aWR0aD0iODcuMTU2MjUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0ibm9kZUxhYmVsIj48cD5MZXQgbWUgdGhpbms8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJub2RlIGRlZmF1bHQiIGlkPSJmbG93Y2hhcnQtRC01IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Mi40Njg3NSwgNDgyLjE1NjI1KSI+PHJlY3QgY2xhc3M9ImJhc2ljIGxhYmVsLWNvbnRhaW5lciIgc3R5bGU9IiIgeD0iLTU0LjQ2ODc1IiB5PSItMjciIHdpZHRoPSIxMDguOTM3NSIgaGVpZ2h0PSI1NCIvPjxnIGNsYXNzPSJsYWJlbCIgc3R5bGU9IiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI0LjQ2ODc1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjQ4LjkzNzUiIGhlaWdodD0iMjQiPjxkaXYgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHN0eWxlPSJkaXNwbGF5OiB0YWJsZS1jZWxsOyB3aGl0ZS1zcGFjZTogbm93cmFwOyBsaW5lLWhlaWdodDogMS41OyBtYXgtd2lkdGg6IDIwMHB4OyB0ZXh0LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBjbGFzcz0ibm9kZUxhYmVsIj48cD5MYXB0b3A8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjxnIGNsYXNzPSJub2RlIGRlZmF1bHQiIGlkPSJmbG93Y2hhcnQtRS03IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMjEuODUxNTYyNSwgNDgyLjE1NjI1KSI+PHJlY3QgY2xhc3M9ImJhc2ljIGxhYmVsLWNvbnRhaW5lciIgc3R5bGU9IiIgeD0iLTU0LjkxNDA2MjUiIHk9Ii0yNyIgd2lkdGg9IjEwOS44MjgxMjUiIGhlaWdodD0iNTQiLz48ZyBjbGFzcz0ibGFiZWwiIHN0eWxlPSIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNC45MTQwNjI1LCAtMTIpIj48cmVjdC8+PGZvcmVpZ25PYmplY3Qgd2lkdGg9IjQ5LjgyODEyNSIgaGVpZ2h0PSIyNCI+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgc3R5bGU9ImRpc3BsYXk6IHRhYmxlLWNlbGw7IHdoaXRlLXNwYWNlOiBub3dyYXA7IGxpbmUtaGVpZ2h0OiAxLjU7IG1heC13aWR0aDogMjAwcHg7IHRleHQtYWxpZ246IGNlbnRlcjsiPjxzcGFuIGNsYXNzPSJub2RlTGFiZWwiPjxwPmlQaG9uZTwvcD48L3NwYW4+PC9kaXY+PC9mb3JlaWduT2JqZWN0PjwvZz48L2c+PGcgY2xhc3M9Im5vZGUgZGVmYXVsdCIgaWQ9ImZsb3djaGFydC1GLTkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM3OS44ODI4MTI1LCA0ODIuMTU2MjUpIj48cmVjdCBjbGFzcz0iYmFzaWMgbGFiZWwtY29udGFpbmVyIiBzdHlsZT0iIiB4PSItNTMuMTE3MTg3NSIgeT0iLTI3IiB3aWR0aD0iMTA2LjIzNDM3NSIgaGVpZ2h0PSI1NCIvPjxnIGNsYXNzPSJsYWJlbCIgc3R5bGU9IiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIzLjExNzE4NzUsIC0xMikiPjxyZWN0Lz48Zm9yZWlnbk9iamVjdCB3aWR0aD0iNDYuMjM0Mzc1IiBoZWlnaHQ9IjI0Ij48ZGl2IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzdHlsZT0iZGlzcGxheTogdGFibGUtY2VsbDsgd2hpdGUtc3BhY2U6IG5vd3JhcDsgbGluZS1oZWlnaHQ6IDEuNTsgbWF4LXdpZHRoOiAyMDBweDsgdGV4dC1hbGlnbjogY2VudGVyOyI+PHNwYW4gY2xhc3M9Im5vZGVMYWJlbCI+PHA+PGkgY2xhc3M9ImZhIGZhLWNhciI+PC9pPiBDYXI8L3A+PC9zcGFuPjwvZGl2PjwvZm9yZWlnbk9iamVjdD48L2c+PC9nPjwvZz48L2c+PC9nPjwvc3ZnPg==", view.getContent()); assertEquals("image/svg+xml", view.getContentType()); } diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java index 7ca514921..6081c96e5 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java @@ -64,7 +64,7 @@ public void importDiagram_AsInlinePNG() throws Exception { new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); assertEquals("Sequence diagram example", view.getTitle()); - assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("data:image/png;base64,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", view.getContent()); assertEquals("image/png", view.getContentType()); } From c514f4a41471a60c1673485531230c857756a6f0 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Oct 2025 11:54:31 +0100 Subject: [PATCH 398/418] Adds a specific exception for HttpClient errors. --- .../src/main/java/com/structurizr/http/HttpClient.java | 6 +++--- .../java/com/structurizr/http/HttpClientException.java | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 structurizr-client/src/main/java/com/structurizr/http/HttpClientException.java diff --git a/structurizr-client/src/main/java/com/structurizr/http/HttpClient.java b/structurizr-client/src/main/java/com/structurizr/http/HttpClient.java index 69e2db865..b82a75a65 100644 --- a/structurizr-client/src/main/java/com/structurizr/http/HttpClient.java +++ b/structurizr-client/src/main/java/com/structurizr/http/HttpClient.java @@ -63,7 +63,7 @@ public RemoteContent get(String url) { */ public RemoteContent get(String url, boolean cache) { if (!isAllowed(url)) { - throw new RuntimeException("Access to " + url + " is not permitted"); + throw new HttpClientException("Access to " + url + " is not permitted"); } RemoteContent remoteContent = contentCache.get(url); @@ -97,10 +97,10 @@ public RemoteContent get(String url, boolean cache) { contentCache.put(url, remoteContent); } } else { - throw new RuntimeException("The content from " + url + " could not be loaded: HTTP status=" + httpStatus); + throw new HttpClientException("The content from " + url + " could not be loaded: HTTP status=" + httpStatus); } } catch (Exception ioe) { - throw new RuntimeException("The content from " + url + " could not be loaded: " + ioe.getMessage()); + throw new HttpClientException("The content from " + url + " could not be loaded: " + ioe.getMessage()); } } diff --git a/structurizr-client/src/main/java/com/structurizr/http/HttpClientException.java b/structurizr-client/src/main/java/com/structurizr/http/HttpClientException.java new file mode 100644 index 000000000..d298f3223 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/http/HttpClientException.java @@ -0,0 +1,9 @@ +package com.structurizr.http; + +public class HttpClientException extends RuntimeException { + + public HttpClientException(String message) { + super(message); + } + +} \ No newline at end of file From 10895ef7d3e842f02c2a52680b7788beb1fdfa58 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Oct 2025 13:37:53 +0100 Subject: [PATCH 399/418] . --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 0f3098447..4a5a08bdd 100644 --- a/changelog.md +++ b/changelog.md @@ -20,7 +20,7 @@ - structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. - structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. - structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). -- structurizr-dsl: Deprecates `setRestricted(boolean)` in favour of finer-grained features. +- structurizr-dsl: Deprecates `StructurizrDSLParser.setRestricted(boolean)` in favour of finer-grained features. - structurizr-export: Removes support for deprecated enterprise and location concepts. - structurizr-export: PlantUML exporters - replaces skinparams with styles. - structurizr-export: PlantUML exporters - adds support for dark mode exports. From 35a6a57db645e9e4b8bf2aaf925abba656ddaa86 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Oct 2025 13:38:07 +0100 Subject: [PATCH 400/418] Switch to new staging API. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 86760cb7a..18adb8a7b 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ subprojects { proj -> repositories { maven { name = "ossrh" - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + url = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" credentials { username = findProperty('ossrhUsername') password = findProperty('ossrhPassword') From 62baaef90651b952d5540d8d68dae7f1fbe0ac3c Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 19 Oct 2025 17:12:21 +0100 Subject: [PATCH 401/418] Clean up, bump some dependencies, fixes tests. --- .gitignore | 1 + structurizr-client/build.gradle | 9 --------- .../com/structurizr/api/BackwardsCompatibilityTests.java | 3 +-- .../api/WorkspaceApiClientIntegrationTests.java | 0 .../structurizr/api/WorkspaceRulesValidationTests.java | 2 +- .../test/java/com/structurizr/view/ThemeUtilsTests.java | 9 +++++++-- .../backwardsCompatibility/structurizr-31-workspace.json | 0 .../structurizr-36141-workspace.json | 0 .../structurizr-39459-workspace.json | 0 .../backwardsCompatibility/views-without-order.json | 0 .../ChildDeploymentNodeNamesAreNotUnique.json | 0 .../workspaceValidation/ComponentNamesAreNotUnique.json | 0 ...AssociatedWithComponentViewIsMissingFromTheModel.json | 0 .../workspaceValidation/ContainerNamesAreNotUnique.json | 0 ...ntAssociatedWithDynamicViewIsMissingFromTheModel.json | 0 .../ElementReferencedByViewIsMissingFromTheModel.json | 0 .../PeopleAndSoftwareSystemNamesAreNotUnique.json | 0 .../RelationshipDescriptionsAreNotUnique.json | 0 ...elationshipReferencedByViewIsMissingFromTheModel.json | 0 ...AssociatedWithContainerViewIsMissingFromTheModel.json | 0 ...ssociatedWithDeploymentViewIsMissingFromTheModel.json | 0 ...ciatedWithSystemContextViewIsMissingFromTheModel.json | 0 .../TopLevelDeploymentNodeNamesAreNotUnique.json | 0 ...sAreNotUniqueButTheyExistInDifferentEnvironments.json | 0 ...ociatedWithFilteredViewIsMissingFromTheWorkspace.json | 0 .../workspaceValidation/ViewKeysAreNotUnique.json | 0 structurizr-dsl/build.gradle | 2 +- .../src/test/java/com/structurizr/dsl/DslTests.java | 2 +- .../com/structurizr/dsl/ImageViewContentParserTests.java | 4 ++++ 29 files changed, 16 insertions(+), 16 deletions(-) rename structurizr-client/src/{integrationTest => test}/java/com/structurizr/api/BackwardsCompatibilityTests.java (93%) rename structurizr-client/src/{integrationTest => test}/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java (100%) rename structurizr-client/src/{integrationTest => test}/java/com/structurizr/api/WorkspaceRulesValidationTests.java (99%) rename structurizr-client/src/{integrationTest => test}/resources/backwardsCompatibility/structurizr-31-workspace.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/backwardsCompatibility/structurizr-36141-workspace.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/backwardsCompatibility/structurizr-39459-workspace.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/backwardsCompatibility/views-without-order.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ComponentNamesAreNotUnique.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ContainerNamesAreNotUnique.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json (100%) rename structurizr-client/src/{integrationTest => test}/resources/workspaceValidation/ViewKeysAreNotUnique.json (100%) diff --git a/.gitignore b/.gitignore index a5e1d9c61..4a491ce38 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ build out bin **/bin/ +**/target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* diff --git a/structurizr-client/build.gradle b/structurizr-client/build.gradle index a868ccf81..fd7c55622 100644 --- a/structurizr-client/build.gradle +++ b/structurizr-client/build.gradle @@ -6,13 +6,4 @@ dependencies { api 'org.apache.httpcomponents.client5:httpclient5:5.5.1' api 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' -} - -sourceSets { - test { - java { - srcDir 'src/test/java' - srcDir 'src/integrationTest/java' - } - } } \ No newline at end of file diff --git a/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/src/test/java/com/structurizr/api/BackwardsCompatibilityTests.java similarity index 93% rename from structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java rename to structurizr-client/src/test/java/com/structurizr/api/BackwardsCompatibilityTests.java index ecdc97a12..58933c9c0 100644 --- a/structurizr-client/src/integrationTest/java/com/structurizr/api/BackwardsCompatibilityTests.java +++ b/structurizr-client/src/test/java/com/structurizr/api/BackwardsCompatibilityTests.java @@ -3,7 +3,6 @@ import com.structurizr.Workspace; import com.structurizr.documentation.Format; import com.structurizr.documentation.Section; -import com.structurizr.model.Location; import com.structurizr.util.WorkspaceUtils; import org.junit.jupiter.api.Test; @@ -13,7 +12,7 @@ class BackwardsCompatibilityTests { - private static final File PATH_TO_WORKSPACE_FILES = new File("./src/integrationTest/resources/backwardsCompatibility"); + private static final File PATH_TO_WORKSPACE_FILES = new File("./src/test/resources/backwardsCompatibility"); @Test void test() throws Exception { diff --git a/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java similarity index 100% rename from structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java rename to structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java diff --git a/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceRulesValidationTests.java b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceRulesValidationTests.java similarity index 99% rename from structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceRulesValidationTests.java rename to structurizr-client/src/test/java/com/structurizr/api/WorkspaceRulesValidationTests.java index 85f5ea596..6ff8ebe84 100644 --- a/structurizr-client/src/integrationTest/java/com/structurizr/api/WorkspaceRulesValidationTests.java +++ b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceRulesValidationTests.java @@ -10,7 +10,7 @@ public class WorkspaceRulesValidationTests { - private static final File PATH_TO_WORKSPACE_FILES = new File("./src/integrationTest/resources/workspaceValidation"); + private static final File PATH_TO_WORKSPACE_FILES = new File("./src/test/resources/workspaceValidation"); @Test void exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { diff --git a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java index 1ecae3995..ae4fe5a30 100644 --- a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java @@ -1,6 +1,7 @@ package com.structurizr.view; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; import com.structurizr.model.Relationship; import com.structurizr.model.SoftwareSystem; import com.structurizr.model.Tags; @@ -31,7 +32,9 @@ void loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { softwareSystem.addTags("Amazon Web Services - Alexa For Business"); workspace.getViews().getConfiguration().setThemes("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json"); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); // there should still be zero styles in the workspace assertEquals(0, workspace.getViews().getConfiguration().getStyles().getElements().size()); @@ -146,7 +149,9 @@ void loadThemes_ReplacesRelativeIconReferences() throws Exception { softwareSystem.addTags("Amazon Web Services - Alexa For Business"); workspace.getViews().getConfiguration().setThemes("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/theme.json"); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); // there should still be zero styles in the workspace assertEquals(0, workspace.getViews().getConfiguration().getStyles().getElements().size()); diff --git a/structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-31-workspace.json b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-31-workspace.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-31-workspace.json rename to structurizr-client/src/test/resources/backwardsCompatibility/structurizr-31-workspace.json diff --git a/structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-36141-workspace.json b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-36141-workspace.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-36141-workspace.json rename to structurizr-client/src/test/resources/backwardsCompatibility/structurizr-36141-workspace.json diff --git a/structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-39459-workspace.json b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-39459-workspace.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/backwardsCompatibility/structurizr-39459-workspace.json rename to structurizr-client/src/test/resources/backwardsCompatibility/structurizr-39459-workspace.json diff --git a/structurizr-client/src/integrationTest/resources/backwardsCompatibility/views-without-order.json b/structurizr-client/src/test/resources/backwardsCompatibility/views-without-order.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/backwardsCompatibility/views-without-order.json rename to structurizr-client/src/test/resources/backwardsCompatibility/views-without-order.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json rename to structurizr-client/src/test/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ComponentNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ComponentNamesAreNotUnique.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ComponentNamesAreNotUnique.json rename to structurizr-client/src/test/resources/workspaceValidation/ComponentNamesAreNotUnique.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json rename to structurizr-client/src/test/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ContainerNamesAreNotUnique.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ContainerNamesAreNotUnique.json rename to structurizr-client/src/test/resources/workspaceValidation/ContainerNamesAreNotUnique.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json rename to structurizr-client/src/test/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json rename to structurizr-client/src/test/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json rename to structurizr-client/src/test/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json rename to structurizr-client/src/test/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json rename to structurizr-client/src/test/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json rename to structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json rename to structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json rename to structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json rename to structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json b/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json rename to structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json b/structurizr-client/src/test/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json rename to structurizr-client/src/test/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json diff --git a/structurizr-client/src/integrationTest/resources/workspaceValidation/ViewKeysAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ViewKeysAreNotUnique.json similarity index 100% rename from structurizr-client/src/integrationTest/resources/workspaceValidation/ViewKeysAreNotUnique.json rename to structurizr-client/src/test/resources/workspaceValidation/ViewKeysAreNotUnique.json diff --git a/structurizr-dsl/build.gradle b/structurizr-dsl/build.gradle index 262cdde49..11cd9dd1a 100644 --- a/structurizr-dsl/build.gradle +++ b/structurizr-dsl/build.gradle @@ -5,7 +5,7 @@ dependencies { api project(':structurizr-export') api project(':structurizr-component') - testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.24' + testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.25' testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.25' testImplementation 'org.jruby:jruby-core:9.4.12.0' diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index fd3284149..1e40f4e02 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1416,7 +1416,7 @@ void springPetClinic() throws Exception { fail(); } catch (Exception e) { System.out.println(e.getMessage()); - assertTrue(e.getMessage().startsWith("!components is not available when the parser is running in restricted mode")); + assertTrue(e.getMessage().startsWith("!components is not permitted (feature structurizr.feature.dsl.componentfinder is not enabled)")); } File workspaceFile = new File("src/test/resources/dsl/spring-petclinic/workspace.dsl"); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java index 90c274bc6..40cb94dab 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -6,6 +6,7 @@ import com.structurizr.view.ColorScheme; import com.structurizr.view.ImageView; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -371,6 +372,7 @@ void test_parseImage_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { } @Test + @Tag("IntegrationTest") void test_parseImage() { parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); @@ -382,6 +384,7 @@ void test_parseImage() { } @Test + @Tag("IntegrationTest") void test_parseImage_Url_Light() { parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); @@ -394,6 +397,7 @@ void test_parseImage_Url_Light() { } @Test + @Tag("IntegrationTest") void test_parseImage_Url_Dark() { parser = new ImageViewContentParser(); ImageViewDslContext context = new ImageViewDslContext(imageView); From b68b29f3a5e25fe1a9a6c05f8030e7449a058748 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 20 Oct 2025 13:56:15 +0100 Subject: [PATCH 402/418] Moving neo4j example. --- settings.gradle | 3 +- structurizr-neo4j/README.md | 5 --- structurizr-neo4j/build.gradle | 8 ----- .../com/structurizr/neo4j/SimpleLoader.java | 36 ------------------- .../test/java/com/structurizr/Example.java | 28 --------------- 5 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 structurizr-neo4j/README.md delete mode 100644 structurizr-neo4j/build.gradle delete mode 100644 structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java delete mode 100644 structurizr-neo4j/src/test/java/com/structurizr/Example.java diff --git a/settings.gradle b/settings.gradle index 092c80c08..4226e5c13 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,5 +8,4 @@ include 'structurizr-core' include 'structurizr-dsl' include 'structurizr-export' include 'structurizr-import' -include 'structurizr-inspection' -include 'structurizr-neo4j' \ No newline at end of file +include 'structurizr-inspection' \ No newline at end of file diff --git a/structurizr-neo4j/README.md b/structurizr-neo4j/README.md deleted file mode 100644 index c01eb1a5e..000000000 --- a/structurizr-neo4j/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# structurizr-neo4j - -[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-neo4j.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-neo4j) - -This library provides utilities to import a Structurizr workspace into Neo4j. \ No newline at end of file diff --git a/structurizr-neo4j/build.gradle b/structurizr-neo4j/build.gradle deleted file mode 100644 index 4703501bf..000000000 --- a/structurizr-neo4j/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -dependencies { - - api project(':structurizr-core') - implementation 'org.neo4j.driver:neo4j-java-driver:5.28.4' - - testImplementation project(':structurizr-client') - -} \ No newline at end of file diff --git a/structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java b/structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java deleted file mode 100644 index 727b06c5e..000000000 --- a/structurizr-neo4j/src/main/java/com/structurizr/neo4j/SimpleLoader.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.structurizr.neo4j; - -import com.structurizr.Workspace; -import com.structurizr.model.Element; -import com.structurizr.model.Relationship; -import com.structurizr.util.StringUtils; -import org.neo4j.driver.Driver; -import org.neo4j.driver.SessionConfig; - -public class SimpleLoader { - - public void load(Workspace workspace, Driver driver, String database) { - try (var session = driver.session(SessionConfig.builder().withDatabase(database).build())) { - for (Element element : workspace.getModel().getElements()) { - session.run(String.format( - "CREATE ( :Element { id: '%s', name: \"%s\", type: \"%s\" })", - element.getId(), element.getName(), element.getClass().getSimpleName().toLowerCase() - )); - } - - session.run("CREATE INDEX element_index FOR (n:Element) ON (n.id)"); - - for (Relationship relationship : workspace.getModel().getRelationships()) { - session.run(String.format( - """ - MATCH ( from:Element { id: '%s' } ), ( to:Element { id: '%s' } ) - CREATE (from)-[:HAS_RELATIONSHIP_WITH {role: '%s'}]->(to)""", - relationship.getSource().getId(), - relationship.getDestination().getId(), - !StringUtils.isNullOrEmpty(relationship.getDescription()) ? relationship.getDescription() : "uses" - )); - } - } - } - -} \ No newline at end of file diff --git a/structurizr-neo4j/src/test/java/com/structurizr/Example.java b/structurizr-neo4j/src/test/java/com/structurizr/Example.java deleted file mode 100644 index 4ef9f5290..000000000 --- a/structurizr-neo4j/src/test/java/com/structurizr/Example.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.structurizr; - -import com.structurizr.neo4j.SimpleLoader; -import com.structurizr.util.WorkspaceUtils; -import org.neo4j.driver.AuthTokens; -import org.neo4j.driver.Driver; -import org.neo4j.driver.GraphDatabase; -import org.neo4j.driver.Result; - -import java.io.File; - -public class Example { - - public static void main(String[] args) throws Exception { - Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("workspace.json")); - - try (Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "password"))) { - try (var session = driver.session()) { - session.run("DROP DATABASE structurizr IF EXISTS"); - Result result = session.run("CREATE DATABASE structurizr"); - System.out.println(result.consume()); - } - - new SimpleLoader().load(workspace, driver, "structurizr"); - } - } - -} \ No newline at end of file From ea34e48a032789da2f19c4ecd0ca099bea31d7e2 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 21 Oct 2025 11:43:08 +0100 Subject: [PATCH 403/418] structurizr-dsl: Identifiers are no longer stored as lower case in the JSON (`structurizr.dsl.identifier` property on elements and relationships). --- changelog.md | 1 + .../structurizr/dsl/IdentifiersRegister.java | 45 +++++++++---- .../java/com/structurizr/dsl/DslTests.java | 10 +-- .../dsl/IdentifierRegisterTests.java | 66 ++++++++++++++++++- 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/changelog.md b/changelog.md index 4a5a08bdd..c8d49cf06 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ - structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. - structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). - structurizr-dsl: Deprecates `StructurizrDSLParser.setRestricted(boolean)` in favour of finer-grained features. +- structurizr-dsl: Identifiers are no longer stored as lower case in the JSON (`structurizr.dsl.identifier` property on elements and relationships). - structurizr-export: Removes support for deprecated enterprise and location concepts. - structurizr-export: PlantUML exporters - replaces skinparams with styles. - structurizr-export: PlantUML exporters - adds support for dark mode exports. diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java index 654538752..3ef9f1a37 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java @@ -70,8 +70,13 @@ public Set getRelationshipIdentifiers() { * @return an Element, or null if one doesn't exist */ public Element getElement(String identifier) { - identifier = identifier.toLowerCase(); - return elementsByIdentifier.get(identifier); + for (String key : elementsByIdentifier.keySet()) { + if (key.equalsIgnoreCase(identifier)) { + return elementsByIdentifier.get(key); + } + } + + return null; } /** @@ -89,8 +94,6 @@ public void register(String identifier, Element element) { identifier = UUID.randomUUID().toString(); } - identifier = identifier.toLowerCase(); - if (identifierScope == IdentifierScope.Hierarchical) { identifier = calculateHierarchicalIdentifier(identifier, element); } @@ -99,17 +102,17 @@ public void register(String identifier, Element element) { for (String id : elementsByIdentifier.keySet()) { Element e = elementsByIdentifier.get(id); - if (e.equals(element) && !id.equals(identifier)) { + if (e.equals(element) && !id.equalsIgnoreCase(identifier)) { if (id.matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")) { - throw new RuntimeException("Please assign an identifier to \"" + element.getCanonicalName() + "\" before using it with !ref"); + throw new RuntimeException("Please assign an identifier to \"" + element.getCanonicalName() + "\" before using it"); } else { throw new RuntimeException("The element is already registered with an identifier of \"" + id + "\""); } } } - Element e = elementsByIdentifier.get(identifier); - Relationship r = relationshipsByIdentifier.get(identifier); + Element e = getElement(identifier); + Relationship r = getRelationship(identifier); if ((e == null && r == null) || (e == element)) { elementsByIdentifier.put(identifier, element); @@ -125,8 +128,13 @@ public void register(String identifier, Element element) { * @return a Relationship, or null if one doesn't exist */ public Relationship getRelationship(String identifier) { - identifier = identifier.toLowerCase(); - return relationshipsByIdentifier.get(identifier); + for (String key : relationshipsByIdentifier.keySet()) { + if (key.equalsIgnoreCase(identifier)) { + return relationshipsByIdentifier.get(key); + } + } + + return null; } /** @@ -144,10 +152,21 @@ public void register(String identifier, Relationship relationship) { identifier = UUID.randomUUID().toString(); } - identifier = identifier.toLowerCase(); + // check whether this relationship has already been registered with another identifier + for (String id : relationshipsByIdentifier.keySet()) { + Relationship r = relationshipsByIdentifier.get(id); + + if (r.equals(relationship) && !id.equalsIgnoreCase(identifier)) { + if (id.matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")) { + throw new RuntimeException("Please assign an identifier to \"" + relationship.getCanonicalName() + "\" before using it"); + } else { + throw new RuntimeException("The relationship is already registered with an identifier of \"" + id + "\""); + } + } + } - Element e = elementsByIdentifier.get(identifier); - Relationship r = relationshipsByIdentifier.get(identifier); + Element e = getElement(identifier); + Relationship r = getRelationship(identifier); if ((e == null && r == null) || (r == relationship)) { relationshipsByIdentifier.put(identifier, relationship); diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 1e40f4e02..55e03d01e 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -1059,18 +1059,20 @@ void test_identifiers() throws Exception { IdentifiersRegister register = parser.getIdentifiersRegister(); assertEquals("user", register.findIdentifier(user)); - assertEquals("softwaresystem", register.findIdentifier(softwareSystem)); - assertEquals("softwaresystem.container", register.findIdentifier(container)); + assertEquals("softwareSystem", register.findIdentifier(softwareSystem)); + assertEquals("softwareSystem.container", register.findIdentifier(container)); assertEquals("rel", register.findIdentifier(relationship)); assertSame(user, register.getElement("user")); assertSame(softwareSystem, register.getElement("softwareSystem")); + assertSame(softwareSystem, register.getElement("softwaresystem")); assertSame(container, register.getElement("softwareSystem.container")); + assertSame(container, register.getElement("softwaresystem.container")); assertSame(relationship, register.getRelationship("rel")); assertEquals("user", user.getProperties().get("structurizr.dsl.identifier")); - assertEquals("softwaresystem", softwareSystem.getProperties().get("structurizr.dsl.identifier")); - assertEquals("softwaresystem.container", container.getProperties().get("structurizr.dsl.identifier")); + assertEquals("softwareSystem", softwareSystem.getProperties().get("structurizr.dsl.identifier")); + assertEquals("softwareSystem.container", container.getProperties().get("structurizr.dsl.identifier")); assertEquals("rel", relationship.getProperties().get("structurizr.dsl.identifier")); assertNull(impliedRelationship.getProperties().get("structurizr.dsl.identifier")); } diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java index 15936e9ee..8a141ce65 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java @@ -1,10 +1,10 @@ package com.structurizr.dsl; +import com.structurizr.model.Relationship; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; class IdentifierRegisterTests extends AbstractTests { @@ -56,7 +56,67 @@ void test_register_ThrowsAnException_WhenTheElementHasAlreadyBeenRegisteredWithA register.register("x", softwareSystem); fail(); } catch (Exception e) { - assertEquals("Please assign an identifier to \"SoftwareSystem://Software System\" before using it with !ref", e.getMessage()); + assertEquals("Please assign an identifier to \"SoftwareSystem://Software System\" before using it", e.getMessage()); + } + } + + @Test + void test_register_WhenTheElementHasAlreadyBeenRegisteredWithTheSameIdentifierCasedDifferently() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + register.register("SoftwareSystem", softwareSystem); + register.register("softwareSystem", softwareSystem); + register.register("softwaresystem", softwareSystem); + register.register("SOFTWARESYSTEM", softwareSystem); + } + + @Test + void test_getElement() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + register.register("SoftwareSystem", softwareSystem); + + assertSame(softwareSystem, register.getElement("SoftwareSystem")); + assertSame(softwareSystem, register.getElement("softwareSystem")); + assertSame(softwareSystem, register.getElement("softwaresystem")); + assertSame(softwareSystem, register.getElement("SOFTWARESYSTEM")); + } + + @Test + void test_getRelationships() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship rel = a.uses(b, "Uses"); + register.register("Rel", rel); + + assertSame(rel, register.getRelationship("Rel")); + assertSame(rel, register.getRelationship("rel")); + assertSame(rel, register.getRelationship("REL")); + } + + @Test + void test_register_ThrowsAnException_WhenTheRelationshipHasAlreadyBeenRegisteredWithADifferentIdentifier() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship rel = a.uses(b, "Uses"); + try { + register.register("Rel1", rel); + register.register("Rel2", rel); + fail(); + } catch (Exception e) { + assertEquals("The relationship is already registered with an identifier of \"Rel1\"", e.getMessage()); + } + } + + @Test + void test_register_ThrowsAnException_WhenTheRelationshipHasAlreadyBeenRegisteredWithAnInternalIdentifier() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship rel = a.uses(b, "Uses"); + try { + register.register("", rel); + register.register("Rel", rel); + fail(); + } catch (Exception e) { + assertEquals("Please assign an identifier to \"Relationship://SoftwareSystem://A -> SoftwareSystem://B (Uses)\" before using it", e.getMessage()); } } From 471b99c9c786cbd62fb89f48b47b54047ee15bd3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Wed, 22 Oct 2025 08:42:43 +0100 Subject: [PATCH 404/418] Removes !ref parser code. --- .../java/com/structurizr/dsl/RefParser.java | 52 ------------ .../com/structurizr/dsl/RefParserTests.java | 79 ------------------- 2 files changed, 131 deletions(-) delete mode 100644 structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java delete mode 100644 structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java deleted file mode 100644 index d9df9b1f8..000000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RefParser.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.model.ModelItem; -import com.structurizr.model.StaticStructureElement; - -final class RefParser extends AbstractParser { - - private static final String GRAMMAR = "%s "; - - private final static int IDENTIFIER_INDEX = 1; - - ModelItem parse(DslContext context, Tokens tokens) { - // !ref - - if (tokens.hasMoreThan(IDENTIFIER_INDEX)) { - throw new RuntimeException("Too many tokens, expected: " + String.format(GRAMMAR, tokens.get(0))); - } - - if (!tokens.includes(IDENTIFIER_INDEX)) { - throw new RuntimeException("Expected: " + String.format(GRAMMAR, tokens.get(0))); - } - - String s = tokens.get(IDENTIFIER_INDEX); - - ModelItem modelItem; - - if (s.contains("://")) { - modelItem = context.getWorkspace().getModel().getElementWithCanonicalName(s); - } else { - modelItem = context.getElement(s); - - if (modelItem == null) { - modelItem = context.getRelationship(s); - } - } - - if (modelItem == null) { - throw new RuntimeException("An element/relationship identified by \"" + s + "\" could not be found"); - } - - if (context instanceof GroupableDslContext && modelItem instanceof StaticStructureElement) { - GroupableDslContext groupableDslContext = (GroupableDslContext)context; - StaticStructureElement staticStructureElement = (StaticStructureElement)modelItem; - if (groupableDslContext.hasGroup()) { - staticStructureElement.setGroup(groupableDslContext.getGroup().getName()); - } - } - - return modelItem; - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java deleted file mode 100644 index 10e0cf947..000000000 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/RefParserTests.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.model.ModelItem; -import com.structurizr.model.Person; -import com.structurizr.model.Relationship; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class RefParserTests extends AbstractTests { - - private RefParser parser = new RefParser(); - - @Test - void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { - try { - parser.parse(context(), tokens("!ref", "name", "tokens")); - fail(); - } catch (Exception e) { - assertEquals("Too many tokens, expected: !ref ", e.getMessage()); - } - } - - @Test - void test_parse_ThrowsAnException_WhenTheIdentifierOrCanonicalNameIsNotSpecified() { - try { - parser.parse(context(), tokens("!extend")); - fail(); - } catch (Exception e) { - assertEquals("Expected: !extend ", e.getMessage()); - } - } - - @Test - void test_parse_ThrowsAnException_WhenTheReferencedElementCannotBeFound() { - try { - parser.parse(context(), tokens("!ref", "Person://User")); - fail(); - } catch (Exception e) { - assertEquals("An element/relationship identified by \"Person://User\" could not be found", e.getMessage()); - } - } - - @Test - void test_parse_FindsAnElementByCanonicalName() { - Person user = workspace.getModel().addPerson("User"); - ModelItem element = parser.parse(context(), tokens("!ref", "Person://User")); - - assertSame(user, element); - } - - @Test - void test_parse_FindsAnElementByIdentifier() { - Person user = workspace.getModel().addPerson("User"); - - ModelDslContext context = context(); - IdentifiersRegister register = new IdentifiersRegister(); - register.register("user", user); - context.setIdentifierRegister(register); - - ModelItem modelItem = parser.parse(context, tokens("!ref", "user")); - assertSame(modelItem, user); - } - - @Test - void test_parse_FindsARelationshipByIdentifier() { - Person user = workspace.getModel().addPerson("User"); - Relationship relationship = user.interactsWith(user, "Description"); - - ModelDslContext context = context(); - IdentifiersRegister register = new IdentifiersRegister(); - register.register("rel", relationship); - context.setIdentifierRegister(register); - - ModelItem modelItem = parser.parse(context, tokens("!ref", "rel")); - assertSame(modelItem, relationship); - } - -} \ No newline at end of file From 96c264f12bbf6d0d63f659c19daf89313e4dab3d Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 23 Oct 2025 10:41:37 +0100 Subject: [PATCH 405/418] Fixes failing integration tests. --- .../export/dot/DOTDiagramExporterTests.java | 6 +++++- .../export/ilograph/IlographExporterTests.java | 6 +++++- .../export/mermaid/MermaidDiagramExporterTests.java | 6 +++++- .../plantuml/C4PlantUMLDiagramExporterTests.java | 11 +++++++++-- .../StructurizrPlantUMLDiagramExporterTests.java | 9 +++++++-- .../importer/diagrams/image/ImageImporterTests.java | 11 +++++++++-- .../importer/diagrams/kroki/KrokiImporterTests.java | 11 +++++++++-- .../diagrams/mermaid/MermaidImporterTests.java | 11 +++++++++-- .../diagrams/plantuml/PlantUMLImporterTests.java | 13 ++++++++++--- 9 files changed, 68 insertions(+), 16 deletions(-) diff --git a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java index 81da0720a..ca459bddd 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -3,6 +3,7 @@ import com.structurizr.Workspace; import com.structurizr.export.AbstractExporterTests; import com.structurizr.export.Diagram; +import com.structurizr.http.HttpClient; import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; @@ -501,7 +502,10 @@ public void test_BigBankPlcExample() throws Exception { @Tag("IntegrationTest") public void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); DOTExporter exporter = new DOTExporter(); diff --git a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java index a9057c03d..88d8184f4 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -3,6 +3,7 @@ import com.structurizr.Workspace; import com.structurizr.export.AbstractExporterTests; import com.structurizr.export.WorkspaceExport; +import com.structurizr.http.HttpClient; import com.structurizr.model.CustomElement; import com.structurizr.model.Model; import com.structurizr.util.WorkspaceUtils; @@ -647,7 +648,10 @@ void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); workspace.getViews().getConfiguration().getStyles().addElementStyle("Amazon Web Services - Route 53").addProperty(IlographExporter.ILOGRAPH_ICON, "AWS/Networking/Route-53.svg"); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + IlographExporter ilographExporter = new IlographExporter(); WorkspaceExport export = ilographExporter.export(workspace); diff --git a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java index 31097f38b..1259c57be 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -3,6 +3,7 @@ import com.structurizr.Workspace; import com.structurizr.export.AbstractExporterTests; import com.structurizr.export.Diagram; +import com.structurizr.http.HttpClient; import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; @@ -21,7 +22,10 @@ public class MermaidDiagramExporterTests extends AbstractExporterTests { @Tag("IntegrationTest") public void test_AmazonWebServicesExample() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); MermaidDiagramExporter exporter = new MermaidDiagramExporter(); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index f35eb809e..76230eec7 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -3,6 +3,7 @@ import com.structurizr.Workspace; import com.structurizr.export.AbstractExporterTests; import com.structurizr.export.Diagram; +import com.structurizr.http.HttpClient; import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; @@ -494,7 +495,10 @@ public void test_BigBankPlcExample() throws Exception { @Tag("IntegrationTest") public void test_AmazonWebServicesExampleWithoutTags() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); workspace.getViews().getViews().forEach(v -> v.addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "false")); @@ -557,7 +561,10 @@ public void test_AmazonWebServicesExampleWithoutTags() throws Exception { @Tag("IntegrationTest") public void test_AmazonWebServicesExampleWithTags() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + workspace.getViews().getDeploymentViews().iterator().next().enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 300, 300); workspace.getViews().getConfiguration().addProperty(C4PlantUMLExporter.C4PLANTUML_TAGS_PROPERTY, "true"); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 9ab8c70f4..999a1a5b3 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -3,6 +3,7 @@ import com.structurizr.Workspace; import com.structurizr.export.AbstractExporterTests; import com.structurizr.export.Diagram; +import com.structurizr.http.HttpClient; import com.structurizr.model.*; import com.structurizr.util.WorkspaceUtils; import com.structurizr.view.*; @@ -3509,7 +3510,9 @@ void dark_group() { @Tag("IntegrationTest") public void amazonWebServicesExample_Light() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); Collection diagrams = exporter.export(workspace); @@ -3862,7 +3865,9 @@ public void amazonWebServicesExample_Light() throws Exception { @Tag("IntegrationTest") public void amazonWebServicesExample_Dark() throws Exception { Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); - ThemeUtils.loadThemes(workspace); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Dark); Collection diagrams = exporter.export(workspace); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java index 1ea507825..722ccc141 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java @@ -1,6 +1,7 @@ package com.structurizr.importer.diagrams.image; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; import com.structurizr.view.ImageView; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -16,7 +17,10 @@ public void importDiagram_Url() throws Exception { Workspace workspace = new Workspace("Name", "Description"); ImageView view = workspace.getViews().createImageView("key"); - new ImageImporter().importDiagram(view, "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png"); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new ImageImporter(httpClient).importDiagram(view, "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png"); assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png", view.getContent()); assertEquals("image/png", view.getContentType()); assertEquals("alexa-for-business.png", view.getTitle()); @@ -29,7 +33,10 @@ public void importDiagram_Url_Inline() throws Exception { workspace.getViews().getConfiguration().addProperty(ImageImporter.IMAGE_INLINE_PROPERTY, "true"); ImageView view = workspace.getViews().createImageView("key"); - new ImageImporter().importDiagram(view, "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png"); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new ImageImporter(httpClient).importDiagram(view, "https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png"); assertEquals("data:image/png;base64,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", view.getContent()); assertEquals("image/png", view.getContentType()); assertEquals("alexa-for-business.png", view.getTitle()); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java index cfcb4d461..9fac27fd2 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java @@ -1,6 +1,7 @@ package com.structurizr.importer.diagrams.kroki; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; import com.structurizr.view.ImageView; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -51,7 +52,10 @@ public void importDiagram_AsInlinePNG() throws Exception { workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_INLINE_PROPERTY, "true"); ImageView view = workspace.getViews().createImageView("key"); - new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new KrokiImporter(httpClient).importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); assertEquals("key", view.getKey()); assertNull(view.getElement()); assertNull(view.getElementId()); @@ -85,7 +89,10 @@ public void importDiagram_AsInlineSVG() throws Exception { workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_INLINE_PROPERTY, "true"); ImageView view = workspace.getViews().createImageView("key"); - new KrokiImporter().importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new KrokiImporter(httpClient).importDiagram(view, "graphviz", new File("./src/test/resources/diagrams/kroki/diagram.dot")); assertEquals("key", view.getKey()); assertNull(view.getElement()); assertNull(view.getElementId()); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java index 0aebd6a12..ca4cd4452 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java @@ -1,6 +1,7 @@ package com.structurizr.importer.diagrams.mermaid; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; import com.structurizr.view.ImageView; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -54,7 +55,10 @@ public void importDiagram_AsInlinePNG() throws Exception { workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_INLINE_PROPERTY, "true"); ImageView view = workspace.getViews().createImageView("key"); - new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new MermaidImporter(httpClient).importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); assertEquals("key", view.getKey()); assertNull(view.getElement()); assertNull(view.getElementId()); @@ -90,7 +94,10 @@ public void importDiagram_AsInlineSVG() throws Exception { workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_INLINE_PROPERTY, "true"); ImageView view = workspace.getViews().createImageView("key"); - new MermaidImporter().importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new MermaidImporter(httpClient).importDiagram(view, new File("./src/test/resources/diagrams/mermaid/flowchart.mmd")); assertEquals("key", view.getKey()); assertNull(view.getElement()); assertNull(view.getElementId()); diff --git a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java index 6081c96e5..a9c270024 100644 --- a/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java @@ -1,6 +1,7 @@ package com.structurizr.importer.diagrams.plantuml; import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; import com.structurizr.view.ImageView; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -62,9 +63,12 @@ public void importDiagram_AsInlinePNG() throws Exception { workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_INLINE_PROPERTY, "true"); ImageView view = workspace.getViews().createImageView("key"); - new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new PlantUMLImporter(httpClient).importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); assertEquals("Sequence diagram example", view.getTitle()); - assertEquals("data:image/png;base64,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", view.getContent()); + assertEquals("data:image/png;base64,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", view.getContent()); assertEquals("image/png", view.getContentType()); } @@ -89,7 +93,10 @@ public void importDiagram_AsInlineSVG() throws Exception { workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_INLINE_PROPERTY, "true"); ImageView view = workspace.getViews().createImageView("key"); - new PlantUMLImporter().importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + + new PlantUMLImporter(httpClient).importDiagram(view, new File("./src/test/resources/diagrams/plantuml/with-title.puml")); assertEquals("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IlNFUVVFTkNFIiBoZWlnaHQ9IjE1OHB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6MjQ2cHg7aGVpZ2h0OjE1OHB4O2JhY2tncm91bmQ6I0ZGRkZGRjsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0NiAxNTgiIHdpZHRoPSIyNDZweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNlcXVlbmNlIGRpYWdyYW0gZXhhbXBsZTwvdGl0bGU+PGRlZnMvPjxnPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyMTguNjc0OCIgeD0iMTAiIHk9IjI3Ljk5NTEiPlNlcXVlbmNlIGRpYWdyYW0gZXhhbXBsZTwvdGV4dD48Zz48dGl0bGU+Qm9iPC90aXRsZT48cmVjdCBmaWxsPSIjMDAwMDAwIiBmaWxsLW9wYWNpdHk9IjAuMDAwMDAiIGhlaWdodD0iNDkuMTMyOCIgd2lkdGg9IjgiIHg9Ijg2LjQ3NzUiIHk9IjczLjU5MzgiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTtzdHJva2UtZGFzaGFycmF5OjUsNTsiIHgxPSI4OS45NDkyIiB4Mj0iODkuOTQ5MiIgeTE9IjczLjU5MzgiIHkyPSIxMjIuNzI2NiIvPjwvZz48Zz48dGl0bGU+QWxpY2U8L3RpdGxlPjxyZWN0IGZpbGw9IiMwMDAwMDAiIGZpbGwtb3BhY2l0eT0iMC4wMDAwMCIgaGVpZ2h0PSI0OS4xMzI4IiB3aWR0aD0iOCIgeD0iMTQxLjg5MjEiIHk9IjczLjU5MzgiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTtzdHJva2UtZGFzaGFycmF5OjUsNTsiIHgxPSIxNDUuMDU4NiIgeDI9IjE0NS4wNTg2IiB5MT0iNzMuNTkzOCIgeTI9IjEyMi43MjY2Ii8+PC9nPjxnIGNsYXNzPSJwYXJ0aWNpcGFudCBwYXJ0aWNpcGFudC1oZWFkIiBkYXRhLXBhcnRpY2lwYW50PSJCb2IiPjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDEuMDU2NiIgeD0iNjkuOTQ5MiIgeT0iNDIuMjk2OSIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjI3LjA1NjYiIHg9Ijc2Ljk0OTIiIHk9IjYyLjI5MiI+Qm9iPC90ZXh0PjwvZz48ZyBjbGFzcz0icGFydGljaXBhbnQgcGFydGljaXBhbnQtdGFpbCIgZGF0YS1wYXJ0aWNpcGFudD0iQm9iIj48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQxLjA1NjYiIHg9IjY5Ljk0OTIiIHk9IjEyMS43MjY2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjcuMDU2NiIgeD0iNzYuOTQ5MiIgeT0iMTQxLjcyMTciPkJvYjwvdGV4dD48L2c+PGcgY2xhc3M9InBhcnRpY2lwYW50IHBhcnRpY2lwYW50LWhlYWQiIGRhdGEtcGFydGljaXBhbnQ9IkFsaWNlIj48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQ3LjY2NyIgeD0iMTIyLjA1ODYiIHk9IjQyLjI5NjkiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMy42NjciIHg9IjEyOS4wNTg2IiB5PSI2Mi4yOTIiPkFsaWNlPC90ZXh0PjwvZz48ZyBjbGFzcz0icGFydGljaXBhbnQgcGFydGljaXBhbnQtdGFpbCIgZGF0YS1wYXJ0aWNpcGFudD0iQWxpY2UiPjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDcuNjY3IiB4PSIxMjIuMDU4NiIgeT0iMTIxLjcyNjYiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzMy42NjciIHg9IjEyOS4wNTg2IiB5PSIxNDEuNzIxNyI+QWxpY2U8L3RleHQ+PC9nPjxnIGNsYXNzPSJtZXNzYWdlIiBkYXRhLXBhcnRpY2lwYW50LTE9IkJvYiIgZGF0YS1wYXJ0aWNpcGFudC0yPSJBbGljZSI+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxMzMuODkyMSwxMDAuNzI2NiwxNDMuODkyMSwxMDQuNzI2NiwxMzMuODkyMSwxMDguNzI2NiwxMzcuODkyMSwxMDQuNzI2NiIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MTsiIHgxPSI5MC40Nzc1IiB4Mj0iMTM5Ljg5MjEiIHkxPSIxMDQuNzI2NiIgeTI9IjEwNC43MjY2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzEuNDE0NiIgeD0iOTcuNDc3NSIgeT0iOTkuNjYwNiI+aGVsbG88L3RleHQ+PC9nPjwhLS1TUkM9W0F5YWlvS2JMMjR1akI0dERJcXZMSUNiQ0oyekFwNUw4aEtaQ0JTWDl2TkJBSnJCR2pMRG1wQ2E0SWJlZlBBSmN2RUczMDAwMF0tLT48L2c+PC9zdmc+", view.getContent()); assertEquals("image/svg+xml", view.getContentType()); } From bccf1b3c1926fb9c65ec979f90006b7be9d3aaa7 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 24 Oct 2025 17:01:08 +0100 Subject: [PATCH 406/418] Tidies up changelog. --- changelog.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/changelog.md b/changelog.md index c8d49cf06..907833ea6 100644 --- a/changelog.md +++ b/changelog.md @@ -5,23 +5,21 @@ - structurizr-autolayout: Adds support for custom padding view/viewset properties: `structurizr.groupPadding`,`structurizr.boundaryPadding`, and `structurizr.deploymentNodePadding`. - structurizr-core: Removes support for deprecated enterprise and location concepts. - structurizr-core: Adds support for filtered deployment views (https://github.com/structurizr/java/issues/409). -- structurizr-core: Adds support for separate images for light and dark color schemes. +- structurizr-core: Image views can have separate images for light and dark color schemes. - structurizr-component: Fixes https://github.com/structurizr/java/issues/437 (Make ComponentFinder.run() not fail on empty Set). - structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`). -- structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode. -- structurizr-dsl: Adds a `Bucket` shape. -- structurizr-dsl: Adds a `Shell` shape. -- structurizr-dsl: Adds a `Terminal` shape. -- structurizr-dsl: Adds an 'instanceOf' keyword (an alternative for `softwareSystemInstance` and `containerInstance`). +- structurizr-dsl: Adds support for defining element and relationship styles for light and dark color schemes. +- structurizr-dsl: Added `Bucket`, `Shell`, and `Terminal` shapes. +- structurizr-dsl: Adds an `instanceOf` keyword as an alternative for `softwareSystemInstance` and `containerInstance`. - structurizr-dsl: Relationships to/from software system/container instances can be now defined by using the software system/container identifier. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships). -- structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes. +- structurizr-dsl: Adds a new operator (`-/>`) for removing relationships between software system/container instances, with a view to redefining them via infrastructure nodes. - structurizr-dsl: Adds support for a `jump` property on relationship styles. - structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block. -- structurizr-dsl: Constants and variables are inherited when extending a DSL workspace. -- structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no files, plugins, scripts). +- structurizr-dsl: Constants and variables are now inherited when extending a DSL workspace. +- structurizr-dsl: DSL source is only stored in the JSON workspace when the DSL is deemed as "portable" (i.e. no file references, plugins, scripts). - structurizr-dsl: Deprecates `StructurizrDSLParser.setRestricted(boolean)` in favour of finer-grained features. -- structurizr-dsl: Identifiers are no longer stored as lower case in the JSON (`structurizr.dsl.identifier` property on elements and relationships). +- structurizr-dsl: Identifiers are no longer stored as lower case in the JSON (the `structurizr.dsl.identifier` property on elements and relationships). - structurizr-export: Removes support for deprecated enterprise and location concepts. - structurizr-export: PlantUML exporters - replaces skinparams with styles. - structurizr-export: PlantUML exporters - adds support for dark mode exports. From 69ca73706a3e9dbe4211b09824a311e60ad23268 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 28 Oct 2025 11:58:04 +0100 Subject: [PATCH 407/418] Bumps dependencies prior to release. --- structurizr-component/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle index 1c296c092..79940da9f 100644 --- a/structurizr-component/build.gradle +++ b/structurizr-component/build.gradle @@ -1,8 +1,8 @@ dependencies { api project(':structurizr-core') - implementation 'org.apache.bcel:bcel:6.10.0' - implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.2' + implementation 'org.apache.bcel:bcel:6.11.0' + implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.27.1' testImplementation project(':structurizr-annotation') From d34a43abb7c3d7eb5331a634811b1312fd9b66e5 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Tue, 28 Oct 2025 13:28:22 +0100 Subject: [PATCH 408/418] Updated to reflect release. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 907833ea6..b31e9ce0b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## v5.0.0 (unreleased) +## v5.0.0 (28th October 2025) - structurizr-autolayout: Adds support for custom padding view/viewset properties: `structurizr.groupPadding`,`structurizr.boundaryPadding`, and `structurizr.deploymentNodePadding`. - structurizr-core: Removes support for deprecated enterprise and location concepts. From 5012bb259c4a138f83733cc25e63cdcc028fdbe1 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 30 Oct 2025 15:48:55 +0000 Subject: [PATCH 409/418] Fixes #449. --- changelog.md | 4 +++ gradle.properties | 2 +- .../com/structurizr/http/RemoteContent.java | 1 + .../java/com/structurizr/view/ThemeUtils.java | 2 +- .../com/structurizr/view/ThemeUtilsTests.java | 25 ++++++++++++++++++- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index b31e9ce0b..d162e48ad 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## v5.0.1 (unreleased) + +-structurizr-core: Fixes https://github.com/structurizr/java/issues/449 (allow text/plain content types when loading themes). + ## v5.0.0 (28th October 2025) - structurizr-autolayout: Adds support for custom padding view/viewset properties: `structurizr.groupPadding`,`structurizr.boundaryPadding`, and `structurizr.deploymentNodePadding`. diff --git a/gradle.properties b/gradle.properties index 638aa1b58..3e9c95f94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=5.0.0 \ No newline at end of file +version=5.0.1 \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java b/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java index 4a4b9ae0d..337668bee 100644 --- a/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java +++ b/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java @@ -6,6 +6,7 @@ public final class RemoteContent { public static final String CONTENT_TYPE_JSON = "application/json"; + public static final String CONTENT_TYPE_PLAIN_TEXT = "text/plain"; private final String content; private final byte[] bytes; diff --git a/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java index 0ecd531c6..869c7bd5f 100644 --- a/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java +++ b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java @@ -90,7 +90,7 @@ public static void loadThemes(Workspace workspace, HttpClient httpClient) throws for (String themeLocation : workspace.getViews().getConfiguration().getThemes()) { if (Url.isUrl(themeLocation)) { RemoteContent remoteContent = httpClient.get(themeLocation); - if (remoteContent.getContentType().equals(RemoteContent.CONTENT_TYPE_JSON)) { + if (remoteContent.getContentType().startsWith(RemoteContent.CONTENT_TYPE_JSON) || remoteContent.getContentType().startsWith(RemoteContent.CONTENT_TYPE_PLAIN_TEXT)) { Theme theme = fromJson(remoteContent.getContentAsString()); String baseUrl = themeLocation.substring(0, themeLocation.lastIndexOf('/') + 1); diff --git a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java index ae4fe5a30..9d61853f2 100644 --- a/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java +++ b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java @@ -26,7 +26,7 @@ void loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { @Test @Tag("IntegrationTest") - void loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { + void loadThemes_LoadsThemesWhenThemesAreDefined_AndContentTypeIsApplicationJson() throws Exception { Workspace workspace = new Workspace("Name", "Description"); SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); softwareSystem.addTags("Amazon Web Services - Alexa For Business"); @@ -47,6 +47,29 @@ void loadThemes_LoadsThemesWhenThemesAreDefined() throws Exception { assertEquals("https://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png", style.getIcon()); } + @Test + @Tag("IntegrationTest") + void loadThemes_LoadsThemesWhenThemesAreDefined_AndContentTypeIsPlainText() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + softwareSystem.addTags("Amazon Web Services - Alexa For Business"); + workspace.getViews().getConfiguration().setThemes("https://raw.githubusercontent.com/structurizr/themes/refs/heads/master/amazon-web-services-2020.04.30/theme.json"); + + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + + // there should still be zero styles in the workspace + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getElements().size()); + + // but we should be able to find a style included in the theme + ElementStyle style = workspace.getViews().getConfiguration().getStyles().findElementStyle(softwareSystem); + assertNotNull(style); + assertEquals("#d6242d", style.getStroke()); + assertEquals("#d6242d", style.getColor()); + assertEquals("https://raw.githubusercontent.com/structurizr/themes/refs/heads/master/amazon-web-services-2020.04.30/alexa-for-business.png", style.getIcon()); + } + @Test void toJson() throws Exception { Workspace workspace = new Workspace("Name", "Description"); From 15c75f2a679a42413905818fc8069cbe16df0909 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 1 Nov 2025 08:34:17 +0000 Subject: [PATCH 410/418] Updated for release. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d162e48ad..5c0d34f22 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## v5.0.1 (unreleased) +## v5.0.1 (1st November 2025) -structurizr-core: Fixes https://github.com/structurizr/java/issues/449 (allow text/plain content types when loading themes). From 79fa039094e726132250f50d6f8579b67a86337f Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 8 Nov 2025 09:10:13 +0000 Subject: [PATCH 411/418] Fixes a NPE. --- .../AbstractDocumentableInspection.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java index d4e3fe5ac..7b2eb945d 100644 --- a/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java @@ -40,12 +40,14 @@ public final Violation run(Documentable documentable) { protected Set findEmbeddedViewKeys(Documentable documentable) { Set keys = new LinkedHashSet<>(); - for (Section section : documentable.getDocumentation().getSections()) { - keys.addAll(findEmbeddedViewKeys(section)); - } + if (documentable.getDocumentation() != null) { + for (Section section : documentable.getDocumentation().getSections()) { + keys.addAll(findEmbeddedViewKeys(section)); + } - for (Decision decision : documentable.getDocumentation().getDecisions()) { - keys.addAll(findEmbeddedViewKeys(decision)); + for (Decision decision : documentable.getDocumentation().getDecisions()) { + keys.addAll(findEmbeddedViewKeys(decision)); + } } return keys; From 04def03f4a02437e1e4ad3bf17cf133453a4e0a6 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 8 Nov 2025 09:10:30 +0000 Subject: [PATCH 412/418] Bumps version. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3e9c95f94..731f3e4c3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=5.0.1 \ No newline at end of file +version=5.0.2 \ No newline at end of file From 8ad78b1a16d83cc99128bf9e50f0ade0bb95bd31 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 8 Nov 2025 09:12:57 +0000 Subject: [PATCH 413/418] structurizr-client: Adds a `getWorkspaceAsJson()` to `WorkspaceApiClient`. --- changelog.md | 4 ++ .../structurizr/api/WorkspaceApiClient.java | 60 ++++++++++++------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/changelog.md b/changelog.md index 5c0d34f22..eae71b392 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## v5.0.2 (unreleased) + +- structurizr-client: Adds a `getWorkspaceAsJson()` to `WorkspaceApiClient`. + ## v5.0.1 (1st November 2025) -structurizr-core: Fixes https://github.com/structurizr/java/issues/449 (allow text/plain content types when loading themes). diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java index ce68fa41f..f45d0bdb6 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -227,6 +227,44 @@ private boolean manageLockForWorkspace(long workspaceId, boolean lock) throws St * @throws StructurizrClientException if there are problems related to the network, authorization, JSON deserialization, etc */ public Workspace getWorkspace(long workspaceId) throws StructurizrClientException { + String json = getWorkspaceAsJson(workspaceId); + + try { + if (encryptionStrategy == null) { + if (json.contains("\"encryptionStrategy\"") && json.contains("\"ciphertext\"")) { + log.warn("The JSON may contain a client-side encrypted workspace, but no passphrase has been specified."); + } + + JsonReader jsonReader = new JsonReader(); + jsonReader.setIdGenerator(idGenerator); + return jsonReader.read(new StringReader(json)); + } else { + EncryptedWorkspace encryptedWorkspace = new EncryptedJsonReader().read(new StringReader(json)); + + if (encryptedWorkspace.getEncryptionStrategy() != null) { + encryptedWorkspace.getEncryptionStrategy().setPassphrase(encryptionStrategy.getPassphrase()); + return encryptedWorkspace.getWorkspace(); + } else { + // this workspace isn't encrypted, even though the client has an encryption strategy set + JsonReader jsonReader = new JsonReader(); + jsonReader.setIdGenerator(idGenerator); + return jsonReader.read(new StringReader(json)); + } + } + } catch (Exception e) { + log.error(e); + throw new StructurizrClientException(e); + } + } + + /** + * Gets the workspace with the given ID, as a JSON string. + * + * @param workspaceId the workspace ID + * @return a JSON string + * @throws StructurizrClientException if there are problems related to the network, authorization, JSON deserialization, etc + */ + public String getWorkspaceAsJson(long workspaceId) throws StructurizrClientException { if (workspaceId <= 0) { throw new IllegalArgumentException("The workspace ID must be a positive integer."); } @@ -251,27 +289,7 @@ public Workspace getWorkspace(long workspaceId) throws StructurizrClientExceptio if (response.getCode() == HttpStatus.SC_OK) { archiveWorkspace(workspaceId, json); - if (encryptionStrategy == null) { - if (json.contains("\"encryptionStrategy\"") && json.contains("\"ciphertext\"")) { - log.warn("The JSON may contain a client-side encrypted workspace, but no passphrase has been specified."); - } - - JsonReader jsonReader = new JsonReader(); - jsonReader.setIdGenerator(idGenerator); - return jsonReader.read(new StringReader(json)); - } else { - EncryptedWorkspace encryptedWorkspace = new EncryptedJsonReader().read(new StringReader(json)); - - if (encryptedWorkspace.getEncryptionStrategy() != null) { - encryptedWorkspace.getEncryptionStrategy().setPassphrase(encryptionStrategy.getPassphrase()); - return encryptedWorkspace.getWorkspace(); - } else { - // this workspace isn't encrypted, even though the client has an encryption strategy set - JsonReader jsonReader = new JsonReader(); - jsonReader.setIdGenerator(idGenerator); - return jsonReader.read(new StringReader(json)); - } - } + return json; } else { ApiResponse apiResponse = ApiResponse.parse(json); throw new StructurizrClientException(apiResponse.getMessage()); From efe39afae9b36ee467ca3a130282d1a46fe0a7a7 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sat, 8 Nov 2025 09:13:07 +0000 Subject: [PATCH 414/418] . --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index eae71b392..26c0c9183 100644 --- a/changelog.md +++ b/changelog.md @@ -6,7 +6,7 @@ ## v5.0.1 (1st November 2025) --structurizr-core: Fixes https://github.com/structurizr/java/issues/449 (allow text/plain content types when loading themes). +- structurizr-core: Fixes https://github.com/structurizr/java/issues/449 (allow text/plain content types when loading themes). ## v5.0.0 (28th October 2025) From 8ab8684c1da25c84ccc29715e27b69679e9d0312 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 9 Nov 2025 08:56:52 +0000 Subject: [PATCH 415/418] structurizr-client: Adds branches and users information to the admin API response. --- changelog.md | 1 + .../structurizr/api/WorkspaceMetadata.java | 20 +++++++++ .../com/structurizr/api/WorkspaceUsers.java | 42 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 structurizr-client/src/main/java/com/structurizr/api/WorkspaceUsers.java diff --git a/changelog.md b/changelog.md index 26c0c9183..17bc04ad7 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## v5.0.2 (unreleased) - structurizr-client: Adds a `getWorkspaceAsJson()` to `WorkspaceApiClient`. +- structurizr-client: Adds branches and users information to the admin API response. ## v5.0.1 (1st November 2025) diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java index 81df2367d..c6c5e1bba 100644 --- a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java @@ -12,6 +12,10 @@ public class WorkspaceMetadata { private String publicUrl; private String shareableUrl; + private String[] branches; + + private WorkspaceUsers users; + WorkspaceMetadata() { } @@ -79,4 +83,20 @@ void setShareableUrl(String shareableUrl) { this.shareableUrl = shareableUrl; } + public String[] getBranches() { + return branches; + } + + void setBranches(String[] branches) { + this.branches = branches; + } + + public WorkspaceUsers getUsers() { + return users; + } + + void setUsers(WorkspaceUsers users) { + this.users = users; + } + } \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceUsers.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceUsers.java new file mode 100644 index 000000000..1c94eb959 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceUsers.java @@ -0,0 +1,42 @@ +package com.structurizr.api; + +public class WorkspaceUsers { + + private String owner; + private String[] admin; + private String[] write; + private String[] read; + + public String getOwner() { + return owner; + } + + void setOwner(String owner) { + this.owner = owner; + } + + public String[] getAdmin() { + return admin; + } + + void setAdmin(String[] admin) { + this.admin = admin; + } + + public String[] getWrite() { + return write; + } + + void setWrite(String[] write) { + this.write = write; + } + + public String[] getRead() { + return read; + } + + void setRead(String[] read) { + this.read = read; + } + +} \ No newline at end of file From 493710fba13dfec09261b929d8c625f79160b683 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 9 Nov 2025 12:04:49 +0000 Subject: [PATCH 416/418] Updated to reflect release. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 17bc04ad7..d2c53e6f2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## v5.0.2 (unreleased) +## v5.0.2 (9th November 2025) - structurizr-client: Adds a `getWorkspaceAsJson()` to `WorkspaceApiClient`. - structurizr-client: Adds branches and users information to the admin API response. From 155da59dca85b2cd6d791c2e6355abaa0349f7f3 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Fri, 21 Nov 2025 14:10:29 +0000 Subject: [PATCH 417/418] structurizr-export: Adds the `addSkinParam()` method back to the PlantUML exporters. --- changelog.md | 4 ++ gradle.properties | 2 +- .../plantuml/AbstractPlantUMLExporter.java | 33 +++++++++++++ .../export/plantuml/C4PlantUMLExporter.java | 3 +- .../plantuml/StructurizrPlantUMLExporter.java | 3 +- .../C4PlantUMLDiagramExporterTests.java | 40 ++++++++++++++++ ...ructurizrPlantUMLDiagramExporterTests.java | 48 +++++++++++++++++++ 7 files changed, 130 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index d2c53e6f2..396b73915 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## v5.0.3 (21st November 2025) + +- structurizr-export: Adds the `addSkinParam()` method back to the PlantUML exporters. + ## v5.0.2 (9th November 2025) - structurizr-client: Adds a `getWorkspaceAsJson()` to `WorkspaceApiClient`. diff --git a/gradle.properties b/gradle.properties index 731f3e4c3..8e1b695b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password -version=5.0.2 \ No newline at end of file +version=5.0.3 \ No newline at end of file diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java index 52a974b27..d078aa65e 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java @@ -14,6 +14,10 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; + +import static java.lang.String.format; public abstract class AbstractPlantUMLExporter extends AbstractDiagramExporter { @@ -27,6 +31,20 @@ public abstract class AbstractPlantUMLExporter extends AbstractDiagramExporter { public static final String DIAGRAM_TITLE_TAG = "Diagram:Title"; public static final String DIAGRAM_DESCRIPTION_TAG = "Diagram:Description"; + private final Map skinParams = new LinkedHashMap<>(); + + protected Map getSkinParams() { + return skinParams; + } + + public void addSkinParam(String name, String value) { + skinParams.put(name, value); + } + + public void clearSkinParams() { + skinParams.clear(); + } + public AbstractPlantUMLExporter() { this(ColorScheme.Light); } @@ -208,6 +226,21 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { writer.writeLine("set separator none"); } + protected void writeSkinParams(IndentingWriter writer) { + if (!skinParams.isEmpty()) { + writer.writeLine(); + writer.writeLine("skinparam {"); + writer.indent(); + for (final String name : skinParams.keySet()) { + writer.writeLine(format("%s %s", name, skinParams.get(name))); + } + writer.outdent(); + writer.writeLine("}"); + } + + writer.writeLine(); + } + protected void writeIncludes(ModelView view, IndentingWriter writer) { String commaSeparatedIncludes = getViewOrViewSetProperty(view, PLANTUML_INCLUDES_PROPERTY, ""); if (!StringUtils.isNullOrEmpty(commaSeparatedIncludes)) { diff --git a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java index 4f309a149..bba5d53c5 100644 --- a/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -76,7 +76,8 @@ protected void writeHeader(ModelView view, IndentingWriter writer) { } } - writer.writeLine(); + writeSkinParams(writer); + writer.writeLine(""); writer.writeLine(); diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java index 76230eec7..9497bb5c9 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -1755,4 +1755,44 @@ public void elementWithUrl() { @enduml""", diagram.getDefinition()); } + @Test + void skinparams() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.addAllElements(); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + exporter.addSkinParam("linetype", "ortho"); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title System Landscape View + + set separator none + top to bottom direction + + skinparam { + linetype ortho + } + + + + !include + !include + + System(A, "A", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); + } + } \ No newline at end of file diff --git a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java index 999a1a5b3..b781d9a21 100644 --- a/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -4216,4 +4216,52 @@ public void amazonWebServicesExample_Dark() throws Exception { @enduml""", diagram.getLegend().getDefinition()); } + @Test + void skinparams() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.addAllElements(); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + exporter.addSkinParam("linetype", "ortho"); + Diagram diagram = exporter.export(view); + + assertEquals(""" + @startuml + title System Landscape View + + set separator none + top to bottom direction + hide stereotype + + skinparam { + linetype ortho + } + + + + rectangle "==A\\n[Software System]" <> as A + + @enduml""", diagram.getDefinition()); + } + } \ No newline at end of file From 5783db719d701978e3a833bd5596736e69545121 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Sun, 1 Feb 2026 14:29:39 +0000 Subject: [PATCH 418/418] . --- README.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/README.md b/README.md index d486bd395..d6ec8cb2d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,3 @@ # Structurizr for Java -This repository contains the source code for the following libraries: - -- [structurizr-client](structurizr-client): JSON serialisation/deserialisation utilities, and clients for the cloud service/on-premises workspace/admin APIs. -- [structurizr-core](structurizr-core): The core library for creating a workspace with Java code. -- [structurizr-component](structurizr-component): A library to discover components from Java code. -- [structurizr-dsl](structurizr-dsl): A text-based DSL wrapper around Structurizr for Java. -- [structurizr-export](structurizr-export): Export models and views to external formats (e.g. PlantUML, Mermaid, etc). -- [structurizr-import](structurizr-import): Utilities to import diagrams and documentation into a Structurizr workspace. -- [structurizr-autolayout](structurizr-autolayout): Apply Graphviz automatic layout to views. -- [structurizr-inspection](structurizr-inspection): A Checkstyle inspired approach to verifying workspace content. - -- [Documentation](https://docs.structurizr.com) -- [Changelog](changelog.md) \ No newline at end of file +> The code in this repo has been moved to https://github.com/structurizr/structurizr \ No newline at end of file

dd3DUkU34M6&(8%_rNx;>CX|5w1Q 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 diff --git a/docs/images/styling-elements-3.png b/docs/images/styling-elements-3.png deleted file mode 100644 index 9fe17c82c4f9d0d1b9c5081adb7b91e6afa338b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 175296 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г^knELgSyJ}0FHy41ke$kItjWHUvbGS}moP>c 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 z zp8R#t#qswmf9_^78tI`yUs|_@{$fG={gxm#2H{4rj)Ix-->>||dr8m{d3>&FG}a4LF19J3&k;R|IMJ>J75sKP*Fwh zU!3dTZ@E<(NP<%{WhMMqgG#_4UQ=VI|LWfO|2Nb>=GQ!e{O5@Rnj6g4A3QpafPZSr Lx0Q+%%!B_2pSdZ= diff --git a/docs/images/styling-elements-4.png b/docs/images/styling-elements-4.png deleted file mode 100644 index 58ef6d5d98cfb09ece933349cf8329e8e388f71f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 193247 zcmeFaWmr|~_C5>FZ5qy$9i7U}LTQ9wmX>6Y$Z zbo`&Cd!OIlb9%0G&WHE?w7+<5U94x0XT&|mxW|}mUj;ddQ-sunSXfx6q$EX^u&@a7 zu&|DI;vWb9a!Ech8w-mbOG@;PvZMCGkW-bp{=tcr<$jlV8z!-HH^UVO$Sz-YziF5e za#Nn-wQ(7K_*aqoW7eD*+ zJbEfp-9obe_NNoraB$4bAMsrM@1()S$It6K_TR{j`I&Y~Z0wSfyy%es@~*hJy$k=X z9Hhqno>&?hy|VZITTSAaZT`1%;1`J@JhRyru3Y|4=J`_<*xWAvtsJ=h4D@x~l=g2< z|3~BhI^7d1TJXP>1IL_+pziv&3}v9|DgHrQ2QS= z|7~&dPto#cM*b`C{!_I4wn+M?X!&go{!h{J*Vy?}MgJ5n{}e6%f488viFKW*_l~0- zMq?<6_`toRLuG;8&f0M=lkfUpie^{qJKsCH&mq>gOrGtG=tS_nD!uV&Fv4+YJO9Q= z*=K1nT)LfjxV_@BgU{iR zxk$OaMf814%|S54yVDT2o6d1!)^Xy(5q0RdFz4?yQIATJDg7qH%#IHXALSV6p>`Sr zzU*K54ZYHS6%ZY<1*hlWfp+!=YFayjvbJjd{q`g+Nh8OyE3^AvledmbPSTm+qf5b$ zIGFfSI z12_3;a)oastiUpFm&pc=r zmKcw`+TMM#l#78Ht@d?)*rDcj_c_8s^Ss7^cezS*0drsPz#pgE2_7dN&@T#CE+gAY z;mH0~cgGKltF=6XWcN=TH;QE^$qHrsEL0dT=Sq#1ETq>^_Yg}kAB?E!H9c@>Qk}2n z=hn9IlFQPTg&&Cff{nh8+w7(eT*)(+Pz!wRtVNq2XqJ4M;&!*BlkG%w&Fx?1k{N60 z1>@;uqE%ZynDt#Xr~6IN4_yP4Nj6KW1@ton3XjfpBeHB_S9pRcd+1h0RjYe;cqa5u7rMZ!BSG+awS=*AgC4d9k8>H5|1BKHge@IF}nSz@r~Y^^9SMgs}n~#|Xr*H(;e7J)AI9;mFuj;a= zlpWXM_!~Wmiy}PDMIW4klUMEcP|?ed*GY1;?8jp{PHDuEsb>~hXDrdM@Q1Py6&NrT zo)Xl^sl4avo@uAIs1m+dn$lASGs8y~QLSelBegkpYeI)l#~*#<%SXS+M7tmHU4li! z%r$U_9Ch{v7*FP`F&gI`=kuYh>>7LR&XVCfsc!=2*@Ghk0YSJU+ogwN@N$yh7Bf7^dm*onPsGN&176y0f4zMuYifbtBvAV-}>{U|^Rv)+A zsXg5G<6GXZW!~Le|K20OcIHzqCGE>ka*zFe3@CN3ljNHh{+MHn`KSO_GVTi|5NZM( zmgKNlE0P>--Mi5I45>D3EpPKtsr;NKOQ;6tvq#!035u4-`>4_TI+i8feon6zwVK(m zU&18xtV<&Tq%&VR0nB}Hu86+oe1^;+q8$oYEkKF|?G94rFB8uPS)4Lt zgqf1L3UIYQO0lq@imsa>5ZD_G*{K|Y*e+8d{=zPQKx1q_$oWl~N#4wVoKMF$A=`Bl7bI%xL?fILueI*NDy zWoA@z$d{HCb)~uI-#V|0(3^{@N7HqrO?%w7EtMUwi{%*;fk}qY6~$z|ke*TKg;07# z0;^wbcG8h*`)hGmM9V#9zO?IA+FEb5u!x;lKg|O?d&k7gpM85*j{TuI^y}C-TC4yq z8~jskp_f505e;td?dgXzw)!(2?OafMVZU~_THU-aG_lXb%!JyXjngB>?h|qT(p+PP&@8`PB)-l6jWd8*OX+P+`F1ieoxGp|=6bh}n6g(=;7aTjr~pvCek{-bK{6xze( zD0}?-1t*W5&BQ10BU{OV;i|Y4$zX;<;{EByh6j^}lGhHj2K?)FPD>)6k)hx3lSnL= zi^&YSy9q#r0SBpL$otdc&)~V^ylelYRsF=gjn95(vGs)z?bB&$(Q1C;AKm%QNG04Y z*0ty4IN&Fmp9X|Id6fw))T%G#^{fi}EFVp%IZ4+etBVRa<_%Iv2aJT)4ted4vR%p^ zW|P5!|K$r$tN~-@=x5NVZvTM8Pv!XzHUD>71X&S70!rCXhTZ5}r zSdWSsE+FodI*6{ES=V6bkqF&oa(3Ta?zYmRuSJzU*CP#>#Xw{+W}gyGGFjNnFCNp_ zb1%Txn*Yr=aWUxwtLqi?c_;>a*TqV;kE_#K;iXX<0p!{&^@(kR`jmQ^a?M8tM}agS z$z05|Ik6959*AFr$=-NAFJMYJBRg8lvF~)=aV}`RM2kkfs4G5wBr=Cw&^|Y)F}(CZ z0d=ED4i=%;c|gf%^KObCCL<0}j9Y`{m`#&b2+<_~DU&rgNMpgdE=lBf8=vL#d9n)y zJ5dVoq=5f5A6V!7eJ8j=MkQ*?SV|&o4T*!5cNxDNDBz2Bo&cJhI!$$9eRg|>Z#fZ; zZV25#pvEbm&(cs4v2l2Y4iT=~{;A^cf~VdW3)D?ryngA`iQc(qiq|e%p+CDc@GWZx zo`QykCl;!Y5I?Ua>=P0E$TlIK%ia5530Ak5jzV?LMd{GaU0k_|+P~J}%msinU^RMK z{v}VVFbWoN;MgNI(9Rx*SLPAfb@tvd6yb;atv*V(U)NX_x{VMuP)KHe2UYI&wzp~k z0FLA`Qa&bFT8Ur;V02q=?KJcj31v7;ay3F%V(pzkn)-m0N-sOh7hjF&_pyDVE}CSu z5qH7YcOaK%Z`lzRCy0bRV0JGp=Kxq{HRO0QzPubn;<4Jf-;iW!CYpY8kHhI1;vJ=l z9lb-@?nJfTP*=FoZ|pRx0d(+8PsjcUs&|O z;6rc~ztGGBm(^Jgg#2`vvIx{$QBLH&tL-*pujeg;n771~ z&fT(nw!@8D*e~wi@NwoUQMc^kJ@V0t_^UVsIPr6T?gIRQ29AorYYkz5Lh7hE+Qc(N zp>e_1@UTtl=(*3Ow<4gh`^Ne0{nZ`bKSMTZ>qY>~Yw6p`frAO=zl?$%TO_WFj;*2F z650@5pD4E0K_2HKM9Cy*x5(?cOyBfxywJ+;NdUD9h%HSZq&Vy}(ocAQ)+Y?Fl`b8O zr;{3&Blvi^7|cJutu&`Tu~enl+{8l3%aM2Gy2b`S*i+{_{icy_rv+*p%9qQ4wGJ;M>f}ApO}*Af-`7{) z91@U8-PE%=7lBAJNqTnQ+DAHQgpK>4&hIw>O$aI{)zO9?zTZfxH|8+bud(c(^zDM; zlJ{NR3B0*y!QIF8pK%S-sl^mce`+)OP4yT^0g8~`WW}@46T$Z_YQ|B$D@3V}jdI0q z8rFZ3H$ILdf-}3ZaHfPaM{trTe>0=m3XYh}X`q_EL!~_c^LZKFfoE-g%@$R+gq_%g zQ7t06A-DzPy@^6+F)gE}V`t7g&P)?{X!6N|DEh7{DhNAK@pRb%M^NLQu-jcBjc>vi z8KlHb48N0pBG!>+r2Q)Q$bTChxz|`ZQgVqB4sJLglTbYAD2DZ?pK$H@r=IdIXACfW zaSK*%L|nW+hht*yELu&?Z&4v0W7Tf|PN?=b-A6wMsJ;%l6tH31?8KvU+?ah}68&!7 z>CNFPnUqN<^7=)TBuy}}T+79!@+V5HrJeP^Gx-%@@;tmAcIc7xyyUmrhH(q>s~AG47xUsO$p_gCon1s5-KKzg5Xjt+pujXlaJKKiu3CHK|b53@*f z9b&w%wuA%vYIL+jAYZ~`8i4|^PFU2d4ukc_IH@}J(hD|YmjT6b$xZ()sc|Jjoz};= zb8I5ljKalr|CxEN1UA%?*f{Sn4Wo0nAp;bVY@gSOyTqTy6bzR_^T{UfNpvRpJSFM` zyvcD4#o&*W-?{t#MiGn!U=9P0nBT2Y3V9gfJ>HVXMEV1#0ex$)$6sCXdfMHY2$H_* zd}=P=tAC~k7{S@3Z=LWwdHpE}X+|HUG1Qk3sRZX;TDh#Yr^w(ot@6SCQcUg|98;NnFu_Nu9t}HiV9AZQyKlUf@%5$GhXe?G+Zcegml0KaP`2+&$8<~9mx$1aaV&D0VU~tU@!GMA?M^rW&PiP+`P=pDO(Nm6 z!%paQALwz2gh^l)HYP55Xow`N<6?+X{l%fec@gR0jiBW!92HSrNqX8C>R@h7MpFq` zj1Yn%K=qYZ_Z7%l3TCJ`>}3~;cQ@rVFQ_k=w^IzYm5Faew0T^rDHw9rQY>VE-3E6` z8+0a1i>_~=BDZtlTo^feSvIG(CH{?vJ5hNys)9ltfpTX1){ijTMNy1@8+Tx#_Y8xb z*#O!xmTu?yFvyY2X?689w4`nFv_455cST>HQNA;vb@5dSfEQm-oRQ1xtfO<79|l9+ z0vpE$7?^v@zD5e#V9XGr29^#qvI?7yL@y;v?lw7ywfM1?{E?w$M-G&-^2ap%0^Y*f zWke9@meJcd$RLQ6^iRenX!O3B2>br5d5%3tG!Z2g&yt}{&sR@V-muz3^VewO!CVdh zYc&9_!GRs9RS*d*xI!f(vsUQbqqD!7Mix`m$M5l|izPFU<$pdgd@h`}*5!BLFk=p! za9CSs2-c(Oy**V+%^?#__p@%gcsF7FSKYK)v8q2di>w*mL1(;zUJRj|10s1Us+9_6 zvRICly*)bLVj&kjQ6;}557%j`th<;*I@;+|L{{o*sh4#a2s#K@FmGTIX%Kr>2`nlj z>l#2kVlT8XxIfpvPpC+OVki5QLmD8?)_U74wb%JsS4EPk0bGwKL6c)%rZ)`hRcwRn zI*blj1NRT*L30OMyvj7|WJ)di>{H^MC&pj3nVo)PKU6kzei!!5jVvH!8+&#mOh~fI z^H$oLwk^wy=`Vr3FJ5dH#6g<^bLY2+XEyce4CK4aslpdK|1Il@a3IL4-t%a(JlcM8 z$!(nB-sI9X?cxSXOh+&0zm=0`JLuY|=4bvJe9R^#{SWX zd+ZembsC?vp0SN%r2(d6Mhb*XoTjQQEMyo0`M3Ic7dBXaIZrU@3?Hh9 z`>f)Hmm&@WoWz4p?{YGA-ovq{5LDgK)xkXQYhQh`ViMj&@Fv&tMb8%;Gp%wTF^&qM zvk!gDM;3d6-fa#h&QSlx8IyoB%Cl#tzz!tS;hXhdjg|H|CuJZbT24ZMS|U$VW6YKf zYIECvwkoXCm$5geb^~fq&pI;@)`rp(crZ0O5V~&85RL7}4^v7Rn2DBuiaX0b1dWw^oY@7Al||$*du}ns~o1s$CplA2m+WWsx;TZUflH^2h*vi=dTs` zH(9pz)uw86orl^aAq{x8n3j_p^uB@|D}nNK=R91-;z0vj`%Q3gRKe3cvVrQQ@WJRS zItyha)B{mtm=?fc(GJS4ZIspB8=#{l5m+k%5S(ixU&;gSY5&=4Y}lW8X;zg>0vd44 z8Nl@v)wD+-&p@;hW$nz8j|J8WT&wF|XBql%iWsj~^`X0O89M90xl9pGlKQcL8f37z z=QiZ#?c+G}%jlM}$yV=Q1=5ydY5%+wN?h>UT+st$sKNN|{jH_Xlw4vk;VOV|9S)v{ z5NAZiQEboDj0D6VOvwY4Ro3Gfn=#jwtVXeX43h7Chjj`GV*(Wf00@j_mj^hqITK1F9D53-d ztC?HN*LVO*iH7Vs7fG9=i+;|yoENJ4qE?5FmCpW;wvZ9n`NoTW9gW-T?4ImW_Wmu_ zfOfnllidDTlgs~T3-3601J{E=BxCx9M97{?mBu1E3E_7ATfeTWUFxavl_8$_|Irq_ zhgI`q$dPIWlHAFQ+#sowf2v0U5M|u#W!)UH`oNy4T@GC)#6A_@cJ*n)@Tm%D;o+^RUN28P(F^$619}_hwH+@8}dGqr?22vNYKkN z(#D*|jHI>)f`Vq^DpcU3?dz<-vmaSGMM5F0T?>b1$?e{t>)x^8jR1e<(8^55E=r9A z#h@ZSHleb)0;{2`E>r_|w_u{sQ37`5M?)7iB$5%jZMiz7w4R!#lKu;_q2T|E*iA2!YhK>yI>L~KEV z8O-P|t&B%6`DK&v-AYvU1!OXnN=|1Y5uiMM#M#(|roOLs*bS@L5+6W87}r@97{(}E zRUi5^l#lQs`tt+xrg4U!EhzAvfInJLl4)AfWlh75oOF%1(60d@!Jv#LF1!>jB-2Va z8%aP@ygNJaMKY=M`8l;G*m&Q>0gtAi?==-vdpNU#gJTS{sKR*+h-DSaL5@T{C0uar zEw@^=^YBT!c_ndBO##o`A@rAN{-pcnGQLs#rryHf;twaMXqfldK^T$B9Cth_dHQT( zQBKp<7T@@nL#3LTj#zwGpa^(I5@-_PpREFk!h{GMkNx>K`Tc=SVwa>Y9gzF^Cd}uY zmOPOP=30a9huHb`8kCC}{r{7cp*YR%`+1HF=^hf_goPNc+8L1evHScSNtm+Mtn9D0 z8+cPCnV7?xihq}_6zEY}#KIix6`NbX;G3|sT(wdzK|7#`Td<_>2^xbyl{v0>Fb~2% zDG%BdN9|#lMr@Po&Iwms-W2-r`VU0wdzR>b@ef2ZsFiOp65d7Wab`#oB;Z8d=3yf?==sIHB zfb}(QXmlqa@w$5_X{>sV1#!vivZz#)ka%t7fjLYcff-A+UHw*x zG@x;X${}|g4NlchHV1h-o$E&hu3+(7R#d$o=l^E3wm#o8?#Yw+-K{h5h8+ zm6A5m{pmO+)U%@@RTWLS0(`@~N6+Q5m9^j}vQa+4(eS@>3Rd~e*rLp@#GVu0ZP5DV z=zP_hXo@?3IJ%}oXxU1dmVbLb9RzHE*Sr9wu^GIzc#q3oez=L54}m5ZRPGd6E{pbw z2vD?lq&4kGES%QJE;VHeOS=N~ax4@X;D=}~KKKB$8sEcFwhIU90y}L-oX5Si^@$!8^gP_K~Nxmi<=(yF*blB!>d* zX-8VapDPdcVS2EYJuDPA7%vqQ3>QIt0A>Gk@g@Ij6LCv3;KL*?CEJHTK4c(B3INzU zMSFyn@NUv0YSBqrLjR_`xCF+FEHIXbkaZ>bjr-Y=@{MpXrWI$a$Fw%fyHFEgp;}2neeu#x0S{_5@ZPM)&ffjioW;P;)AocK8es_$Z(414 z`?aXfpRv?%4%XwFiGs!(f*u$e2l^DimJ8W*PDHq(Eo19dSKO{gEw=HxnS0B2`z*`$ zTX3dwQ$e5ZiSWiu=z5k1pBe0O)u5tap~AtreW#&@!N$2UThu)M@pI4gk79w3V3pu5j+0}yVo z)s|k{pM715jFmsYoDbi&5;TUTxe2>4Y+^gJec%@KQ9WMp#)TU{>G;wowjva&zp7r# zBejwEvzgUy>Gev+F;F8TbXBpjx%r>M|D6(bOm4?&zfDZI?E})u2TTCz@YYt;-_hv| z+gI~~b(gQ>#x>Ap9K5M54U$-OQIs>R$0iZ&9;LjhBuk2nDc{P{dyaYcdWa?d55K9GO5#2BisAmMRWr(#PVFH z4`;wJ`~K_118#-44>RBdGOiyZAOvEHyZ=I#b{%^x@aHbTuQ1W=#nF)2m(z5acl$MY z^FDCXVb1wAd(-={Rn5pQylT_n)efZ<5T12MfN?PV&CXa=O5F!}AsXb8^WFXVo%CO; zKSlUowSOshc%@=>H5vpg565O>pGOl7JTSe^fO*A40TKo_*Qarg_SX6J_C#7+YhV3r ztETsxhgstg@94l5$ne$#ql2%!t8v%ZxtZUTUV#|4J>rW$^ofAcIlr+O|51u+kTf_X z>SD>ItL!gYbu{Q@qkj|^k&&0`2nP<^5UE4I2O#WF2RJPatO~&_F>&|rg_O;1o>(LA zT_cV51}X8U(;gM~udNnZE;Ybl4(RmX5rFgzra-@89vD#te+A;Y$Y>bP7uNx}Rs?2+cnYgif&eX?H-fJFc&Pc{a3$3Y|gb&?!q z$>Xx{=HTDYg4%JWwd^z&?7!lb-*FE$F3)YJ^Fr2Wn`_fjlNp^a(po5+rS@WMzYa@R zbyZ+K&{Fvp{%a|`D7L2>cp48!b`^(fNi|RLfw33kb=wf?+e>Y2L}# zIv;D8bh;PH273eoTE5%h?1a8Wn-2HzZ1&yUJnZy9YX(unM(LzywSYM~(A+do9V8gx zUC}uh*#4^cCi;LNA3ey|c3yk$@cls3<^^}lHgwuWcrxw)>^+T}%`I5^bXFbB&>5l` zkmF-XQA7ShBbSmM2ClH5)YIAT7&d#D3$x@RW~!45*C8V_s1u)-y?|xFGm4`<4o6%i zg4LKxCte*Rz<$9~$l6W|ADfCGD>vF4M`oYg>{+H`+vg=9+|`$qTG&3LI{TY`ZyahLD?j(aq3=2Sa6AL!v2@`S+W3iY463U z_tz$sUM?>C=j-oA#UF&8ZIPonxkLH(6qgb=Tyt?CjYK3a>9I(PA$f{g<}pTEc;vHqKuX5+o3;77D7tUBYT?}#`7e4QfVq!cc=yW}M@MPvGFYvcaCvjPNBDu*P6~l{h zKFnKO&&+nx?c>O(KFJX^KSiQVZ88>Wy}q2iQ%~mY(T6MYsDzWKaI&_?=ypCE|E!hx zE4DpNw7GPkz1I{>nwMo@8^DJ=8pGn;ms6uzk`xL;E+ z8uh(K94TeU-^Qw@W~f#;#pHS0%CzLkI(Wl?&lYD`A?>?L&U#|s?y_O(k7V#>S=-w; ztA;XbCCOC2<~;85X0@v>`m1`8PE?+yDW5vqDb}UO9w>mn*(M0Q!d)n zy!@Xtm7T5@O+C)-PAs)9T2OCTxYr?`eLDjwrjfq)?Eyz6JLl0dBhg7dwl|#GsGS=)P)SELv|NgyLhaEVN@zYoTk5X? zy)?xgZb^$6yp37FfN@X2z#y>-#3;%g}K`; z(soSAZ`;)`*GnO_isN!s7qWW%44DcDF1L0i*4*p8s!7GC7DBs~yzsb$btTfP&k$FL zQ7^ou=Ox&RrfTLWmTU^mqK7`W4vA?Fp*1K%4If~kjnx=lPg8pM;D%B)=&}p$C&iR> zQy_V>7o^)^}EJ$AxhP3nr&tMriX^ao~AffmtDUrYYIlO_Z^f3MQ6FQ|Ya)Z}O?HS(LrPlf1 zDQT2O8>JRZ_Ix&cR6<8$Zh&30P*lvCurX$We5jhudH+c^f4FKF<~_96*)?Y$4>-0^ zjM40wox?C1lyZ8^kOzAHyTT4dNf_ha$97|NLi(M0=JcJUq??T@!E76g>2C`>&H_qsI~tMk+ZLtqjcXB|_}1bM<*+amGN)zou>tLkV%#A$*nZYsYw^U(st{9(_L_As2LlDhZev!WO>CcV&>F)3$rEg z`I(tCYeAh^?JSH{tdj&qZa`7qDx{nQFH~sSE-sHZGaXoBnmi|6Hj*54|Lns;m6f~F zxZ3m1E$5y&SSKtV5oH;msb-@(!RyS=s_xSBH*gRYh*1-LsKgMOy-(S4*Uiy%1mTBujgYuuA4C#p~NX1C&X!??9?8!Gvo-IXe6Q~I_) zO~${&k(o}`A<5VxWOV4{WalJ!E0%t`*57G_T>Mk+=@t>QK6EA7zn1$S!_;pMCP+w{ zYG3HpMQUusT*vq4t$qHrHH|xUshwMYla)2>#6ZDygZ0$N3p9jkHD|9xla#Z4Yrr#l`sz89O zm2CX6HM*Lk#$~i533lgq?7-#bCaypj(+0{2K7{~UVnP0#ipk;gV38mi^PWt*B2HmU zMEA)eE)sc{NhxGYg^ttB0coa^mh_4~arZ416%bcC(l+l1@ zp|gURDyEQ|{O?!<7Dqx*?|U7W?~gfn?7FzmDPvy9nyjzH0>ci31|HbZM;_ZzDP4Z} zXcmkLT$;A?W9qaq;W(%)uc+rpbm0>deDN8{UjgB@IAFOEwc3sAl(dhy=9s|?_G zAKt&2%8<-Ss(ADHna^hwlWG!oeHli4d`@1z|M^v)UDLWt)?(bcpo36rqu}&H&*5~m z%cjv{aU62FX)AtfypR-ug^l~`k55R{iM_y{aAC7onq;Ybxsl`KTT6WKbuRb1iNUR9nhp5d)W zI@MnOig~Uihpt!eShk#5_Hw1zc$~|bi+{-Se^MbvvxrD_`R6|h{7cY(eR#ZTul$93 zQSZ~ddgOVUFVZORVBA#C+j5=HdoKwN733VZvmlLGf2{gfF@H);%8i5DTW`qn5dUvi z0H3|Ty3*Pty|MAByn7-`UxYF7sZHWnLX3_^LlCBG>Eoi8Pc3dH#F^Uj--I3OS@)2f zzRtee_}tk)wfZk%4cWD`yN?MM(Q7K^D?R39{`uv%^sioDahm3vfQ2Dt+6z^uD1Rp|ArSXn%*hupaibj;R3^JEnQvTu zn3LwJ^!UslN@jian$=-VN=!mbUBXw}&g8R#_9+_)n8@_Nc9f54>TUlL6hDQ>w?n;f zYc%=c7ohs2g)qw3F9&K4U2^Pd#vXB4b#jXzmlBf>n76;DE=oBWEQ1vqz&SbjR;v6o}7{O|E<2v|zWSLYl*&f@7h( z=_yR!(Ls!dZA}S~L7s-nBi}}jJPI|xsgRy-QJy%Ss{JJ3=b*9hqzp0r{ubx#I)&2XB7h6TJ-R!Xz`cb# z_?bZI-fKEb1=(exJ)c{YE!Uvm1;B_8w91lo(hm{{;OpFn!U+$TpRBi{-sK0pvvx26|L&Q zbks4t2RC7-#enx|%#7{^-+-FzNm^ZKzzNf>VG(PwCMmLDViny!%#}s(l4W4}5poTZQC-ByX;bxhu!K#t6m zp$m1PPEDG}0k#opE;7QhnItNutvYo_x(A>tlJ?>+>>EX(LM}zV6J5@Is9mUSxs1RH zgL|Gm9_Z01lIUPgM7Ypvqo5|Z@w(VrO$Ph(q;&$0^iSk`HNfUDW<{j;J&Q_tDslbJ z9ZYEb%>=k|V9qvRVr6v;rs}h(9wv|E@GGtt#Y8_-#FCu(`PCCdWpCu}0hG@FQ0<&! zCwM|S&lY+nDL$aVHRw${VV8e`qKT?aY2n}eLP5(UCd#MzY?2W~3o(_XY%u>-?a6!L zofWeO=L6rb!QIjpTt7DF>=xz=e~pCECn&CHhjjIUN8R~vyI0?8{`@eIDld@Ct`a?d zLVmu{I{ExE_TRsSIb<7SBM+7(j2Hi1Em9v+-QKzDPe}!LSc^62J^lGz&tJi#?J|aq zHg!M2@K+&z$>cc#P-8!3OdN6?F6nigE%f=!+@ngVoEcXo(LXW>zGV7mYo^%FSe2aa zr({~Bzp>9gC4Jo#4+RelR1mIrU$^ECs9BWmb*{c;r}~wX@E5rT5%hPj-r4S8t8L+qar?*{v_8WtPL3Rtr z6$=;?q1gDhGV0&uO0kD)-oV4ULobi3ZR!y2%$fN%Y2wWCx_%-9m(_dux!cz6fi_V* zWyPjLw&kzyD^+im4=I(FPMPeEaXA&x(fJ|D2+GKVq%BIjWtVfUlck>ztUVz4o4aB> zB?c@Vm>FS*UW9r%z67doWIrp6Lne$Nal)^aDp2~d>_z(Pi3107gDYV&w_*z)SF2YE zFg8)J(tH;S&0!B+qAMN99)3q@ypYOrR~Icq21=wSOtB$KZ@BE*$4!-{Q%2*j--5X~ zN&pm6VbdCQFzw=>LZQehjaMeblpep-RJ`%_hKb=hdmJOix^Ddm*MRuan+sVFNO7Dl zj>^e}D4j-Y&O2kk!085F9JE5m!nt?_;mP)q`{gXue_yHL$i_Lz6V>Q=V2r4&E&FVm zPZOlG4?0+37xr<|Ki|~qU8-DkcDtD*veFiQ4tjtmA`WE#=5;n*n0lYTs8b=S&Z2cW z4E>o@@dKo8HRoZfFnf(JPwQptVy`)j5okI8*y$J8^LnB|I}ZyIR3YfC7FiQ+UV@(f zbjE^xw@)Wtz&g~TVLP1C)c&QEqbupgY8yf z_Dt2HcQ#yi(N8VS?%>`vtu=wMi@{&ryPp#AoCpk_zF0<55HOk`0NMSqFTS-|gYOiGY z#Wo!o#Yky1LcN>*JU~vb@tgu!B(>(%l?t1y9?5BLQ=~s zD0!2MtoU>T_o|FH z$S7mUooC#3y2&%{?_Bdn=DEV0@{=scC>D4317Sf;eS7_=b1POp`IB*Zq8`~D`ZnBF zG0$w#U>tS9{AfR_+tsa#>$M|kh8!}oMz_k=1?T!Fx^3SV#;gR!NVaSCKY9D-E`a_s zL(?B$T4pVpuhnf%$33IwGxr^y?NcCQS4(+hlb>PL z6zjuWN{%+(#p(N=p5~Z}f>fhS!*?#kk9%SNHX%>!I`Ht+ab!f%O=;#6S{lL0N zO!~hqz7e(-Zt}TOuyud?CA?>R1hAVetK?0?UlSI_8=lzXe&KSG;y=30tS00BIn=H+ zwQ&8hxEOtPT_16tN_9>VWq@&RMB2uk77>YYmCSiw^SQOVv01lXl3Rn>XL` z*n~_IkyAM_?fqD9=`3nJ6JNKZj&$b`aW@oy~M$RlNCLSK| zm8oODeObk8y(!~+dwEpB6RuoRoN2&mV8A7`CLyiFb#1KNLr5Nuy{|w5rc@eecnrn= zPc*2frCSAeZ{+kob-INe=ZA|@g@MGyfuxgn=}GWf?LAvq%@>pCDl%+W&#Ohg3dyomJ9?YLDAcv&Ch=`3 z`hG9#Wv;FYo||JH601>57Af~t8k^&>qHo_Ua!$Q5{7!cL3DS`hXBPJ>S)z5S>CT>; z@@j^3J1kpZ;}%fR*EKd{8sfjZ=WRmQ)epI*gAPy6kX==??bb@C7ai3ki1EjYp7kkN zX({mw^!sY}a4N0M#J@Z3;FWyU7I(VVoi}?tvQ$dVb_uK&D@=8p1A%TCy;F;b7pccl zh1W*q{KA#)b4~{kS8R39x6KuvJr7gVT@9cpVx3auy%RTk5V2Whu`<~nUa7Q(D`l4ItK}PfJ6%QOU z4TlQ~UQOb??v%&qpQPaZs*A-1hh!FMhMiNqjRc@>M2Zi79u z%c**;Nqm~Ot0NgEu{0ZZYP|~<09uiv1%)_M@3{4Z4CN<*deYslZ1p_I zIONG8qG(H-(&I&$9)a<#mjQ2Mphzh4sWAvffn&s`I`V9DDxG2tga&&rDQcG#VlBo) zwR#t}hE6C8)mfO}_MYEN^!{Ld|MlaFyyEiOs79f6N!~0UuD|;eaIdai(u;W2XRZEjImzA+g@zVHTq(UctOY7)U35p6Tj&`=zXE+?XUdTqK?{GDiN| zRwF$BCN{4}&TvrYx9=@T1E-?2R2p^kn!!fbioKuTOG%!I)i=HwJ0C0Eg_}zwu%d-4 zKM3Br{3?po;hWTuPT^C->S6Y@l*Y$!4VD27;`O*+hmnI%I8@Fj=K^pw8r`ErWiZtwoM^DWMb*Dj-x;w~E~H9vj=$r{wlSj7lugf8r#ha(Qta4y zKTt!62*#gc9|5G_v22Ki;1oWuQi8bmk6n{-;&Q#QOH{PI=XXxy_9bR&tEu~o7pcjF zRBzqLTtZ%{D=|^OK+(dm5%xMN%7BW8q;1=Yr3?Jxxo%;bx%w%R>6pv&U1O8yhBXNC zcHT7tPZ^a9=D(q>lEd;YdEDiAS7!vgeZ&RbW65Ygi6OLSL$nhpTL_BIdmL%xMC`w2 zVVId}dLDCCw;};;?p~?Mu7<#xD(mRiYBCw$te%~-AGf7L(;&G~V=6 zaHi|r7ZZy+bP(}mOE6Dnj=&vEw`Ph{*kNJ+CVxSQT;`TOJ5(L?q>nKStC>gyG1NKU zUr{&=Rx7sa5yCNhUvlHC1lznyCyo4Iv_WZ6jN@0bI1+(dx9>Z)bcQf1A!t%JO8^FK z-2+raW9Mb(*=G%r_m;AhRmbk`-R(Gs&bPqgeArEjBaVB?n9z=e0kK3 zYMR+Yja-#g2D}0-UF&-p+{=TxeMhBox$-;Q5SWKZFBs^rh0@RRE_vCDwr4x4Sx@dF*lSnwLtEHcOaQA(Lwgf zJ&FhmBM0RQrOW=5ojKLs27$+9@5$u{qFp`S%VTHPVp=K(sT&OqjmB-Y5-KIiHx67T zo_CuaXxysRcInr5o-*dEHs9Mz@S|wWeV+9Apk~k0(5+Gqv??*15ohR0LooUt6C*V3 zna;(XJQH~aCMz!3B`q24Jz)sV@Q8N8Gqv;QX#M41v2@#(TH`GWM4rWb9dVtYY*LOl z=c!eGf330Pb=jBYFeFF3A|TZzDme9s59)rn(TMj0E}zfW&iJvNHM_4b^z#Squ#U>{ z8q&JBn0;QMkGp(d1j<{XssLLQ*27=74%4YM%9YFc!L3VmloK=!8vv_O^yJsExou)K zH&tJ?yqtv#cO4^K%8)KBHgMkpWzY|vNqG*+o#&q5FQmPfQlJ$(f)vomL{DXP31OEj z1Ygk|rk%n}T5xkQnjNAsgpOT+0{?mbKl=a2|NcLVKcm)c`vw-}M9DkLV@KQbo~q{k z<_DFli<1=R$bu&=f27%&N4~!1(cm?lOncj1iAc?YxGA|Hg%k|4D4#|*hCngq(=h<( zPM)od&=2r0gAd*oY5WlGxx1rZY|!|fFBc)StTztIPwA!(=R_+zI}1nYE6+nZl^2H{ zMB4)Pf&iCsfm`z7!%LS_A5hYiE}x3FD|poox-7~Hm!_}KC-8=YY~7m7-3H5txESth zG{tK*MAjJjy-4VqI%5O)`&dKP%ryuFIlyJzkwSDl+F?zmLV126oAa{lfhQZc7&m z@l8jB`EsTiEt_c|b@uAV-aPUQ?V6W>&`5pb`x&enSU89{+`;X=>#!Vg8=AGImB47H zymRUy6o8*6U@&{I=z3g%rDyh58XooDEa9@LAa+T1B}rPnKI6BmuH0qCb1GL;U)Bot zWCT`lW*FMp-s^~qqklkPly z!_;o8Wp3S=EtK6UF7X{hXhe*jou@{PdnI76Kkx@e?!63;%G^bVt8Ya%+Xm6xawvZx zd(`Tp)8>tw>kBL90>l>u%lXvGYvK26dW|#R4C?F!%`84_<}_S7>NY2g?#xN}d=PzF zmuTEU_)NICC)*i^fZNc~(32%#{a6>9I9PI$#3YyWMZ&$$yT^N-U&iFFuB$rt#N78} z%Um)ek26UOLtisA;Xc|6Fx{+Io-e)7B$!U~Mf)-BLv>z{tgXXE7vhYg=eb#<5wlB% zITInu%fZP~pRZaR~Cfy4`PRK%Hg{ycwyX{drvz`oUeyyv@ff zL?mTa-ji*dEHTA!XdBv_Ps^iGf2NlA_W-?SN6%`?uUAMQTs&Cl&0urg6Frh2Q(Tjq6z-BbfE-AhI2vEOW zQ^WW8sN>VvKTN6HjRY>FnfB;h17irQb=8dNPdgRqgAGxgx#>AIMbFVy5+~$(stjl* z2nHhgZY=?l%>;B~s;Ol?d_(U5&#k^XPuw-jJzO~tTHrP8=e;+O59v0K6c3o@L>6Tg>L0~Z>YE8$4=$#yqFQd zyRx#m63b)AM;M9~eQWqgLE|!ZiNe5*DSrq@gct^OQZtQ19CX}{_=G8-BD1YOiXIT; z8Q?vtN9tttCV9slecJ7_VNKiFto8;|u0x{vZDINVl>OBVW2QrPvO_&^GNt81^Bg&x?y!e&S`BNiSC9m`!i+^Gwk^H6S^9sz8RU7s9E=9xbo zp1&)D*DLeMZ2SJHGe4|nz0m`9Mq32K5<|*MdV>Fty|)gFdRza70TD$66vO}l6#)eS zX^8>EK%_*vm2RXZhOj^+6s21w1cs2#!2qP2p+}?{VCaDv2HrJ$d(Lwh*zfb6^ZQ-b z`^5g|T>JPPR^023&s}S&S5&@GGj9anr9Qk+o+JX@RDQQ*(=T-9uV}MI;&RY&=}kx6-5yO(U~4A7V$l-0wl%)eF0G z=Weh6H_ms`M!`WRn$rPF(Xf(k6kiZbr?EBh6ulqk#f<{Yz0M+;rd~2Qn`ZvRafwoX zgzW}8$1{SE(@}kMTvOYwDd&JV{9ZgrNx8aw@@Q?blhfT%I#eu(YwbG|f<}ZUg7S{B z4~)dlemzhF&dr(y_lWf#86l$?9hV+N5)6R{_f&&B8UIWJSC$-I@D3{%g}vcD8adcy zanv@4qQv0CkHA2e5_=rZXt-wuLcm$SHFxG2BM>A*n5S0c?wGhwmuVIW_7EcaPo=S% zCX?F^K7;^A7BgD2;r)(;a__VL$n5?!EOZ28!<_hN5No%SaTn)aDXe6z)jqzh1UqN& z8yXEowxWFwFXFNO@4oT4JSdow6DEcz%Ev(^!n*|XJ>e+wOCZ}LQ(7oMc6EJAKodvN zCr+J^b(u;l>CGV*D|ICtZPGn|?+&bKgC}aby{oKQ^?jPNp1y4%2y37r7lg9-t+gs; z5s{ZKM=4-!`zg3DT3`g2N2EN8*hxhyUZ0uW^qKdk`!B7@3*4keRjX6)&B2~N`?KS5 zkGIp@8OkRK{r!o<9l^DoX(y}4l(u>w zgmEgRnWcR03nx8BIM$mpzA{Q*y-&$X1wtz8&@o)m)PH4~4~+hmCxC`Z2ln(o7Nfrh z`J*W4*uSclziINnFiq~V?#%w97GUM?llouyr2amgKf|?u>z4lsl>0wSim3`czX~dK zXH2efoj&yeL><^KvZHXWgFTD=4gNGUrQ%1~tBt&JA>fg7A9Z49=jRCk&6WGDdWoy_ zWc+{^MD|R9yXmywWl;CU&FP}iA?r9$j`FjXp^1 z#RBib3ir%w{*FYux!5 z;b+)r^hijyYT_AOYG~q}!+L7q!QtY_|IlkOJ6e+++oyyu(Cn0${_Rb#v&a@{J-Atd z>nR)M>4i`63?>H{#lL8^Ix=pqy;k0d#lmPbWfmhtKM*1WUEZ?OL%^{cF+BbsqDv%+ z;_vLcru)@4*J;bj9BZj7*%jLT^paJxzn$OgM;cAn1XOY&Ol)=M%wa#gsxsxlVo`nGssAlCwk57Qln7KjU1)*e}42z=F1kKB5H{cZKS zO;uWs$8em>m(7C_@!{4z*~8cpY$*Ohmop(e$fD$+Heth?zaS`AuwAqhX#DEnac}AweWN0W>Q0o&ded3yS+Rw+R5$le_Nkx6b00`G86&@pd149} z3@hRq4YJc&VW~d?593Q6({Gy=!0RUc84)^ZT_v!GJ)o32G2x9B&zTSqe5J`|}J-ivo7U#Lt? zpF}y86dsiwZL93R#vBp%`wQ46dPoOj^j}tr6=YE3c z2}FF~l^!DE9@0agD7T9j5=9uW0-p8o_~t5mcv+oEXHUQlQ~t%@w9_jl{O~2raRD{TK$}nVsTsC z(wc6Ohg~Gby{ls`f@ZDLHFRamQdX8v1*>}_oo8tT?NL0_zE-=8%x+IaM-0b9(fkiN zyI`&4Up|)$NVzviB$$R4H!onf0+8JLepMtD6*7wFjya;6*Ijw0;BVl6|voE;$3KPwqyif@Q$^!IR}DXhN{odO@V;97Hs{gbjNCKI;eumF_Qn_B@8H?18M>E zh6_Oc-r@ODv)R`>VKngz^DL7d#b{&2tG4JbCYn>c^sl{swKNZxc%zN*KB5MCX&*5v z9EV*O?^NbgWq<7PXw|PCMk%^iWFjJaJq|AGpspek56Hnxcuh>+uXcP*TI)QtLnUVR ztkt@8aC=(|N1M=2ZN^t9t8<1=u$t1qOd&Bqqk}v{Z#zX#!_QSpELbL_k68sJh740EAw08 z62Xr4>gT|>$WT7cG08C=rx2imqU?;BqdLEM$|%<}B8ZA^P~GvVr78mE>${QEGb`j0+MHhj%61A8qh7zW zbsZK&Ri`OZqP<>sv|@0Ju0ps75L=t|(au6u2qX;js?4N*wL-tm?^|j+kAf~yUkZ(@ zP=wr0MRP>WnEj;H0gaJkwKBxT2Bf~AyleBrcL=d^xTX!FY*j57@@qcP8-@1Ol!OJk^MG^hd8?vBXZ7kF(M~DHwF(vB=G*VmYgC{JSdQ zP_b+n2;+RkmQc!Qpe5_D=-`KWAVoEuI6^PTycsjhm}H|N$M?~7Ry-y;d*p)@0!QW3 zxLn3@&)pzvbmW7e5!xjT^l&^BD>*CFb!21Hd9$_uS-YYfpNf_ky$hi}Tr|(3PGpoD zc`VW4XKO*!QwbznS5>QimdLzj2xnbdTo76?3KVL2R&kh1XcRbrO0S_KRBk_x9TmLA zS5l#X%-$XrXY3xx`U2C)y(@--D@|wlBw?zC+nYTjLl@@e1)`Sv%g*>neEyJW;QNW8 zykcpoq;npXkw^_=Qx>f5I`hqV(tX8H{ju#c_m)I^moo?6aA;%N;PtDk(R`Jd*kfaD zX!j_}S+hBTX#N_v@taQ=-ZgFTHv6QeE@b(H2&ajCUTs~VuSCBQb6fiqU|&R*9uStG zp1RJ4_wmJhAmg2A_Yc(8fTxA_Ly5=M0WO%c%cp##@8igi7l0-8tTnp$msj>5v$=<) z(Vx6lkAgnY{^J-20YKBKXO9K7tnnH}-!3^k;LMs5{$&MF+ zh4oA{x#WVdxHc$~6gBYUF{x>0Z2@^TE7v7~D9SfgR~=Q;K8L7z=^1<$coF3-(YDEw z5Ig7${bp*EJCI2M237TRWSYBH?fpxP)3AyB? zUC~dN7lPCWn7f*q*IhtPnIneVFvCsPN`{U`h0GWqWG>L{8rI0|A-w6jGlDrBy^BAm`!c>y_w!Pen8<_Drd0Imc*p|4Qou8jG_rXBx zy~Ea5T~LB4MTDZWu->W$#)X7_=?ruqU-k!(dan6h6DtU7I(u0Uz-oer0}>QHrK198 z>VxaYu?x4{66U=_npMkzU_`WXKc1b|agK4%b%dc{Y>I-RNrM&N(K{d}UN`th=wjFZ zY=a>Birh;t;?&TiJc1a+8C?~SXELX`_wxNgm_J>3xS9C9n5Oy2Bc}q3%H3j6a3=Rp znh1Ai=!0ZZ5y3~GbD($HkQ!$HD6xs$Me*G1ybdxQMHabqsXd-!l4+Sz=%#rcW7j4b`DxnYqSX9aB!4&$ zeXDG+kJSN=SC|N%)@*&VPoz_dC5SJW(I#2cRGol zTk)B&GaxA_m{Otn)O^y732t!=5RaR<#EpdPe?mOu(n#E@W-fho?VMxOLglWJEt zkU%+0PZuY(s7mOGs@)*(Z+1}V-AeKDggZc5MGGUsV>3UFt8=@&>jBt3Gt{BEBiq4u zsrO)KS)*vpB-2LYh9dF#auE4@Ll1ah#0 zQa-m&o#ZxU{-9Gb<)vHpFx`kx7q02Lx=|IOUT*rv1rulQnGn!gv<017PGS{Beeq#6 z7b{NBii^<-IG6uHy z6z;}bxGoSBxRAr*Soy-|@EOVGip3S&um;Fip|@JR9*;kM!axT36WM{JR-{IWq@Z>k zN!%H}3U>*CuV${wLSJs@bO9c9^E4a^A<6{%sR7SR%ymjflsHY|11#_gJy+adWUy^+ zCc?-Zk8MJGxd@!UY!J;O#(pc6?~?Izk7nVSCXHDETl-MjHPAF*j-c@T^~Ji3#rIkU z6jGF})p114gO@(f8i$>8AuU#QjzrCLP;Gtv!7~@pb*@@2a6#HdA5p}(Fn?uI&mxqU zxgAT>w5;#;po#Z%R9E5#%Z?S9*|#*`^yl9Ob%)1#9dL3Q1Msn^EVNP)w_!?p9Flhh zo=D>Zs%tWCN97ZsO6(B)ldz9ly<=>?WPMy>1KZ7((kLUB>R!cJ`{!)FgZV0P#b~u} zu?QWsF|IIqsPGhP&(orul5G*oWzpG@Rgna(b4lJ63o&e(XkhJ%YzvB)lJJ(y?x-?~ zVpx`iZa2-Pn#sX44j?O=394hRU~1@VoN5zilYh8+0z0PM_Nsz7^zbMXNPPN&Bx;XG ztO9hekF1$v9K-l#a)t4GHnUJqp?WzM$eY(Q%FgdTrr#j6ji|4`5q0v;ke0iNeew3Q zIGy0wfx1=ukM$_6&twmOH%9yreH@|ogL6!qAO=@d+;#Yjq`1f>yh4J>7qrI=#BC?o4KBynW=OXji}HrI8;SPZ`5L7z>~tqp(4}@7+cYoT-wUKK@CA<}R84QvV$CE4% zUZiCSLG-i)=pg+(rlhAauNgiIo-c8VI#ICgHS(F-bEDR(_px-SSLcy2vr~-mi;DEz z-Z5Rr-}k#dsK~l2*5XkV(c?Pb-tpKVLL@hs>U@oxxP!^07YHwaP%NH1xm4YbGzRLdu<6BH`*)E72|qg^E8ufpU{x)u1wivgcJ!Ss0fo0Su23+`!!> z>2=O`@&j5*-i^9Ob+7f3+gf3~B}iu!KntaU^|08V@6t_lHA(Y^g1voN*Tj!^pWYaR~MsaRoAfQ1AdbqJS9a~iY8F0W#wAa^mub2WvV1s z%TD8I_1kRUSj;@)a+T>Q{Y!szKlV$|3`tdaLJTHf?Hs?LjBY9Y>((%vCF(j4@#JS@DT1rOS2Nkc+YG^ zjKJ`qCq!QjsyAfMTyM#Xe53->*{GHCVK#%vL&BJfHQiMtk3d2Xp&lL2n)P9JqDfo0 zdbs!GQM(Tk_SKydy`XMqZG8^Sko5YaUXxMgAeVlcxTw*mwn)207HF&<374oUlI>DU zm9)dQ0%Rm6H-gD66NDmGZJP#w{WhTLldDp!8k!K&rFa&RVLEOVHYMuYo9jDwXDle2{4 z2LRqMkGrzVGd5pf6_x5j?Gs7jHd|Z{+KN6dUZYtBZiGSHQ0DvRzXYnrCTYv&pIfAu z$CHL3XACg2N)=|Ijl-qTT>C3tZ@7${U(RY&-^bA~YlU#uZx*Qxy#q~YOb&aSNH)M- zb1kgw3&ibWHD3tv*1@e%TJ;D@vRlz?V(#;5&wCxFuDy1!Y$ErtmRibT_}H+31<|)& z{^07U^nxffQnLPTDBk6{D|{;BejDVBjeuK)z{9c#`f}a+>ys{f?8%c(zMXmXvBfZdtzv=(%j?o zgEO|+GQ>#eAFWpPUV2v_1fe0dc;Fo*D-S^(YVk!uPy4l7%!!(?Pob-yNd8=Ust83QJ;Bw z(NxfVUdQzBK#e~N;-AHXmTTthM_2_6bu@Jxwuqu~$1dmT7E*%QwaGOK3!O{%xDD(H zHwTx%a9^59x7UhRCJ3=*f8g@^$c(p=y=4S?L9-N2shl!iK7rntdi|nJKhrS@A8nPx zO8rRy}n7vWq>ZLmEKR_VJS`e@y^DLd-P=9#b(SXOATZbd1SRw zi9rZXCBNKf(~#gfI!jK@NJ*54dDXRKzK!i?2a||jIdR!+f`W#W@(n;FVZcIXE3%{&7|NJr-|2fz7mJK37)DKUs?iCz#pHcK3xnV!TAzI^)<+qN8Hy>xdH zzv{Je*82lxUW~)9J~}c#8XszHabjFj25?DJL?F{1R9!5yRFT+`zd>GROmzoqBznan zYXk17a_D`*84_;*O1O@lvN?$C#aGLLs^V7HCoo_=Lr0sF`06roZfod~F;-_Tk+z_~ zz+r*h>H4Na|mQ=i=iUWLtm(M9Nm zt#WEd^bGwj|M>J|(}aPE`4fDdwSy2Ga6LolvFBD`P^*rQ~^t2$ZyfmR^`x z91Sgf8H7LuMqD#xX6!Zy7(C-@x2?z^vR-x9vgx> z!?&PmC;K}c!ivx#I%uU77`Jk>=2?}93d+QlN#U-t>Tn|3YT`wLhrlV8SvUzYcSkd3 z!KGqCSu*E$|4QHK!285@_t;@~0BuIAzh%=B+v+$nGbUO>Qw@muPv0TokSV>&9mk(n z#@Jigs@s#PRG~5UGXG)m*F}9AWNrvli7Ln z$AtP$T-y1OHBfGoq|d*}uJx>?BVJ3Hq|U?3UQ_@59a8A(S2oR(B7svUHTv(@UI|n$ z(QBbl?0GT)7o+F+uGxaIG5Pjq(^kL2T}yoMwg|=h6U2KV|7rlTN~P$lDB{MLnlzb- z`WKdXR$tJy*Zzh?(eKfD!(PzkP0)n;XF`V)RwuYXBu(_Y)>Qn=3Vxt(fxBq*AGH7| z@Q&doH(#g9+oXK{W2wYjJ9lgW=<~1803B9h`4Bm1?RGY0$G(qOOEG>ts8B3+rIE7c znnPPl5VwKBUC;xjn*SACj?9n(r#ah`q2kj%&Ick>7jJ42c04w8*@?3*Yq==Oy8oHs zzrEw(9JFPBEOH_!^z8*u1w zX4P_wf(ETW$@%X5F>*3*xQNJk(12JnD~$DiY{7IumAIyrglv=-2*Hqh^ct}cJWxxX zai$mcoe}@zelb-MIcGyL($iD(V%#*Wq}4`DUNL{PoC;^C8WtQXDfxF-Tz)hrg?2Lc zH8+=LP;l`-PeR9W1C*!@^fkS^SoW5_dve>F5!cN2?-IKv`K8fp5ADgjZ}`D|L49Xi zJSNB=#FtEEU-Ks4@Hv1~aCdtDE<9t~ZlTxDZS4SS;N874>IG!?pGQ0eTMEkULY6oiXb6mtekIcC_ zLK_7BWPuFvymOSh8r0*fzMR!oBH{2BsRA9fA@jp`;5%!%hViAByieBM{mk$aN4+)i z`!=AJ+lPj|P;36QH`$39|M0UaP*9nNG(JgOdG81DPZCAnUn~B0QR^Ad1@qLCHP&QT zM+;`~x;-6!_2}+($RSsySEAo69%f(EBdtc(7}0yIxGQHnWvr1I}U0}k&7TPp#F^!@u|-vU7UmiqycmBa)skl(yJhyL+F(#&&3M& zI;fol7eP(H%{JSk28L5@czIGl`7}gyL3*GH9mu8cn38CxqlYl?@qf?8mR)|MUfCMY z>b1IkN}6C4dXaxPq6=j@Z;l@qcM&oql#7Fo<7{N}O0zRGm(UC?!DV1I%z9?D6L~1q znt*T|`d_$UB0#4qxGWc1$(C9eg9R3eJAKy zmp^< zf&WgD-DQ7ePU983B&GA&5Ju6m8rnlJ3Lx|&9MA$Rd}e2Dfp;in^+emL59JX}=6{sj zcL4BvFjUE`7cKLZABBGAuCj8_F}?BZd0p(2_XEofuhMmz=l2+ipF%*ql?NSr_tgKB zkQIrCv^N@+H2rZW8pYKo&I-U9S7VZ>?O~oE!hRq+$4bwg`JVCppdM(pX$P58nSLxV zSXBa>Hn6xMHPGt-?gr|#GpMsBbV0M&mX}s_rX*^Z$U21JlsX3FhZ&e%XQ3mYfx~TH zBKyMgUoNWovYPHiAo-Sgzv7K2Yz&g=hEFBjOO!R@qA*xJm8@}!i9R*#-f!9P{Ca)Esk&t1I!nZ>sDk=ZT-;IdPN_=7ep_(%>eY@GVq0O_KmP{ z(iM8tOBrZqzvjK{m{_vVat1lGNojPDM%j!52sVjPQ1^+lx4$&jumV_n{Wg+ADSI-L zd?%g*XBDb9kFOx>stXC4Q0ucBVz|bk+4`T#GY0ANmd};1T zn!M9iV$M?#N@lR7={2l5DuBYcOXE}_U1Y|IPs~ipwTC;Jg#qF(V<$A1uc1LiHDexC z(4|xe+M7U-^1ZwH&QCnfC}~{_l*ee24@f)}}4^+0W?o>ENS-V0JI9x!R=-ZiJ<~ehd z41F~B%m}jNO?y9lTv_>EmRFXwEPRrbM)u4e9eeGss@Wghcrf0)qlR>sD*9@p85q(8 z?)kauqn29HLEfjRRYcXwmjQa@dw!*!(JggyE(Q@dBY9BA{yFs5mUG*6gTY@)=&H%X z_f-N!1T{fKsXOPonX7~Dw{nXm6+PD-!M9HF88K~XR#4D2HxsHkntwcI616$(Vpx$n z1|Ia2a9$aZhh($Kwy3`UoyGr1l^4VNh!D@Q*QZy;B_nTWg0V47tiDZ0<~(?jISx84 z+*hHQk^n;ZS5a|ProFpZwWQAG>Q(CY;OV_Tu7}+f&JQKzPoiPwDa3L{&bv6T5~l&L zKaJYBhdQmw3qN?cWB6cxf+pmQ*bRB<gf2mGU#pVF`y|mbe4nYd zka66`0VcdbfwCtkXq!e_s@BT%PEq=13+7+{ie+dxn7!1=Y!nQW!}{J9xka=`86R-T{<4}4H@H&jU*A@O()B@? z&6d74HNC%T`6^IWDGA%CW%N}a9dKI84^e$N6(o9k)36Vg?klquKGj}}+vXX*`RqY9T6NKO zB|#QG8_$n2TgvWQHu>Rk@TMq_14D`7?o0e%tNvu$*@Bghr66yo_}savWKd_gj8G&= z+p*qmpk+Osw6%Dh-#!X%iC%4(GJ1I+&1iTVU2G%wmSlh(SSehl7%QaL&JQ-m@E(rED=dj7mF37$ zp_Tk#S$=Wg8Te>H>2Z9E45ITyZC#IJ^Y<#_?WlB%X&L(6E0#Iu0A8!}$SPgi{VfMP_`YeeCPiW@M|&)KMwYDvihUqk>I5k^U!% zALv;C%;luzL{8#4cC!gGzTZ;B0(PWlwPe7wy1jsaStxe zk~PxBPOVFw#Q&`9b6ovlX>TR})b_dT`CjQyc4`MWMvJ@{rH&T~3olhk_%b9_8($A* z5x$aG>UcZ&tjI;pY(u88bSOkTG-egk4NqY5Et`+DLag{KKlb)+rL8bP$WZhti&-PD zO}>VhG<=?z*Gh4dI8W??G*T1u~lU1cc>xs*?fftVn zS!x$6JN4Zq;n3@NOW($V>y=L9jOE$bbQnvAV{Z(t^$E^cSx+T+TgJP5-w=xL?Xj|; zwqQPGu|0e}fuefrh1b|C(UHOJ;p{NhGZspTr^B|^pKCwbap*$bdQ0CGPCvwZZ6dF) zOgPXD?<>WaR52@ORe>CKQJ(uetU1&3z67$N9={Z^nZLs#ans4nvUB~b8uC<>qQrY> zu+h$T>V|KL)L9k$OwVcfpqtgtg^UArbv6#!fiGcN!8GcAJWC2syybuLZhEg2*Z-R1 zc4*QJK3lT9yGXG(VLL>O+nfYtCd7GV%A`9m zWho$Pn{QnTb{;RbgiAt0dGS&Sh^O#ZZ_P*IVW-9HC-C!TOM0;%`bxRh`!@NEN8twT zO?529k5epj5pE!9)DzwPhQW_>S3f-T(HGtEztl|~lmV5{ewUIpBy`-b!Elxp>Y-cd zo@DU8+WuRfDIIUcOk(|5rOm>$lV1gV{pH5sQpHC&dwK~oP6dft)pzO~FiU#uYg;g} zNIk&GnT?-}spZEvt#oyDiTn6$^@P=XVLY_b(c%<+;jH0Z!%~~0PmP;;7s1WAS~*t1 z6)aE=Qn!#c<{K_CP|ae;D;@ckWY7WbtIVja zFj8Fi}o-0+HdFw4=*Kj+`m$H#wd|h6uI&^s*O`CoaHkjy3gtRNmVtqKn)wlu@@4) z5r&lI`E4m?&$B@3|c};REKne71C!hlnBl>Szp&hSsQxVDbIc*Pd z)!abB4812pHX2)`k^0{>d#;TjMUtPvwZ;9*GA}g-#SMMuJO0|CP-?A2};HR=zRA=fnCOd;Ykv zt4>9Ok_2gVi_vUx7b5%(++)zhaXq5MZd?9zJ{Nf7r zADOGqgqtfis07x>r5zEK8w63VHhzonr`ehdzSlrDf=txP?JMMXU&dd(8bezz#>qOe z@*|ujH^6rCeI2+~aHgAN@$0YaCwNv_Yh#)>9}G zD6FmRUpnVhL#o$4)KmD7-OkV@MLV=9#2K>cUa4b4ZhFrZ;6fn|)AGa&ivbKj@E+4^}q!y3#?+cemDwzf`#-z~jk>3iiX&%PffL~KOj?+T`NBm3851&e66@fQ;v zVkTgK3)Zvm?@50|%&&CIkZ`E<6!%)w1N)Wz9srBuJWBj4!L+pp(^-28ZYS=2Q3vo% z{}G}oWpWR07n-U0ZtMP#?zfSm223Q#Naq;M$vj+hkWgGCm7y zX8B=C4n^462OD?AUR!X$1nEsGqOTtaBepB#;^6tA{^P{!=I}p=;R(Kc%2)LLJ^JU@ z^-O2kXhX;!EZ;M;$<_qq#zgSs7@SeHed!V9dxe8pq#8$&2yQjrz3^;K@O-TT@r*GC z-4cYj4Rx`%Ps|3~?(Q~UNRMSYQlswd3WE(Ctb`A%m!^sZg=85A*xjxzC~3`HpML-) zT3-zbcvFn}ShoFc>6Kw2uYQD|Uy4BQT&$86s=C{4ae$Qcblek}i6f7FMGY#(XWK39 zgIAQr1k95>Y$r|*H18gO#6KJyW5he;&)pLye?FMF-Nl{X)-5)3q0{Cjm2gB=GHG-b zNi#|EY0GBnJ zqyy2z+kt9*BENV^3SvY*jvik9;D>P^i?&M#)sE{wH|jQhkzupt#Jso60fe|DNRMwf zXHQ%B-{Pxg^itWpKad4R{m$SwGew;)K^CW4gR3|`N0Xil;YWWxUC|{Hm(TTrc+|gG zYBD*vh}OeK-?<47v>=ex`%liiL%$}wI-$oT$kv_!_|7K9cx*N>GHoM(o;D;m5i08* z>_eow$q(D!2N`S%V)|gO=YD!2nn;OcQ22|E=rWk7L^CvL<>-og$z(3sgbvB5Cn)y`!PGN?LfZ88;x6auvoW5IMn@PMWkz zdtWnG6qA(5j@&(tABjH=(1OL_KBZkB+H=WRr7+l1Tu1r^Yt;LDq92WLIp15!A8_Sc zPbxkD$BLOxxM!=&+%a^5_1ebpQj(T;<4NMeP(qwSe*}&bJ;Mg7VM32#)zMVoo zITN*&auPke>RdVoza_(C8Tw%1HuJG@7bx?nGjm@VjV!9<2D2dKSF*Dj4i*^Jw+JzG@8u6kzsNBHsC?ILZRA^ z-|Ffso1`_%YK5G46ICXe@Y-}^u@DEicjI3P&ZYNYsZ+Eo>!y)WH9>h_us)=*{zRbF zkc5LR-kiKq<{Sp?urhE-1Qr6=q%PNfYpU;IoT}M91~pl!JwpC0~Fw+t4z;p{0@%;DF26n=w5rFlU^$?w;+11ZVv%K$fp0mE?$xG ztMNOck7tz*TglOV9s0(QT~|#sc`QP2s*I-QogRO406;KKxb0#gScSOIg~>$W%Tl-w z%=-P-F`{uu{1busSsQA6OY}kJw!cEPqgQj)45t-J7T+EiI}i-fFf16Nc)*pbZm#&? zfWLki#TGVsAHu4%4;>$o)z1)zQn|p9~ zNP?7X;yiddlEw8Z)$Sd=rt*)-Vj9b6Gh~K)&pvnM%R^!2q$QOd7)UUW~D&GixstgShC zb*|;8{k7fOka>_$lRnw}ifQ4o5Z(*m+naW-6dX?AUYAdl>YrqK;P93ZRa@P+n&?#% znjjQk^!?O`!Yf2$B*znEt4w^7NVYQde}+2@u{b+m<;xpEG$(Liw&HWn?18Hq)p9(1 zNHPulwn%P>ao0d$%~XPuX|vQypZBT3Sz-eD zt_Hp{j&NLODEZFh0K402Y*Tf=Fo(-8Q-DN`AtL`U*)Wy*Fm7YB&LlM8ya$<|)$-CH@)@ecj(pD9f<;)+n0dh)_)EJ@=(+t z?2herS?j zFAh3dbJSLTO*z?^oNE@Z^u(s&rAea)JEFb)NCJno zZEat>o6VCi96ZSf6{OBL05H#?O`7s^Pj|#x&wB<2zfV6B9(IybC?{VSL5^p(kH@z6 z{?vWHM9ay-IF;yq6M(Vtn$hVPqIu4>s{qu2C;!AmBnb~z4qx$H>e-lu4>KVv@NY^E zUFpu2c*8?kz;Gna;sWGs>IJf^5dBy6AP}8w3q*^*>Vgo&5!Tmf7-?0{a&}4?B3ZXk zs5EldtZMfj{A3OS+oJJkNikvfV#alTev+;}E4s50<+w5o4~{Z~j@bppmiJ!Ov(s68 zXGfBD#YORnj0e@bVO(5%apbe!aC(HUv_voqgPQcU^UB*lN%N;#eN{)2&E$kRMJJOKU8Q-kwMj9QITYT_XpEnu1 zukYKpy+I8ODJ>{jn+CX^bUl9F&~f02-5r9ZDOE)_^2P~q$D^ZyZ3qQQb-LI zADm^!4Z-(ol6B_-2&ERTT(q`E(iE_#6t}Ei_8>7Agmqmd&s0smh<2dUw4sFwyBjOz zP(Z}Q?B+8l^H<#TG}y|7i62ZAI{|``4?B2>PI>(L&n1g((FOd3;8K^-mK45g=52gk zWMZQx{qxw^RrpL2>BhT@B!VQUc6NWsBS$v~a&f8nX1>|5@7tfc^=iFGp39zH4dgQpviuT&LftsA23{EJr6OfR|zbVK^xviiIb^S^T42hXn$*x^>;Chm;Gn3pL3 zU8Eq8={g`2gZdRUVn9ow{BvhS|M*V9bgEEub z_q6or2dkV-3C>mUuvOd!YPdM8%6(XB+jEX4f9=P^D)c=fa3bR;69J5-hR$^!aHaJw zIiL3R*NVi8oh*6L|A$^F;_;bp(Z*u+@;&X;vJD4MXCZ`&w?#MIr_kg-v2c!w&are_ zZTHy`S9ssXWxWXjt(6}fugTkA9L)?n{}g5NBe~qwmsS1Ljew&;UuNJa89A`Ed647a{_FcOeYBeOj-Dnu-vfG68Q0N_$-c$JV zUqH|fLZqYqavfysfJL||E3WTDX`&_nNhDxrSpIUQMAP_(11B!x*>Lh-t(5rO!7a6Mu-_=aiK5CbY4{CI^i4qXh6`HOgUa z@2F59l&AxVX?wqrUy$`{5!r`OuT_B(AUNB5m!b>A!FJC>?SMvSzz2b^Rzwa5VSr@t zo_dSs-_z{F1OEZh_7$4{Etq&;p-CpFO7@4Mm-c90V!8Xh@qHpLfALFT=ji|{3D0BN zYg+b^Rl+Es1^@r+I*wg>2}Dl5kD54fweL0w^8u5v&q{$w5HxSTqbDLHWIA_!KP|s` z5|}M_JPh0%K*U1Hv|vzZP1ipAleq~9MX`?n3xKGP*|VVX zeXV7$HaJ|kUuK6e^xxC`2SnS4!2f$NF)?HQDPI5A1CH0K59)%?ST((P@gf*vxsPc) zhXew=nFH_H9VaD_bM-nvYqXDJ_W@Lahe<^S_t9W7w}H9~ezYGxumsSzfMFm0G9|dH zhttpOJwbm>5ZM85&F}g0_cZ?j(e{#&@GpO(=6%eK-+vcs-q#ZUcL5H8BhAtLY?PnUt^#F)Qu9bib5s2i@UkbP!Yosb_d%lp89S1i$GVQbQ|G*0b$U+*|-_{w4 z_Cn$W)|2;>ac>X?oT%8=RM|(J?8CCZBlCNr@%#Mr54G|C;+7B-<&WQAiUCNYVVZhR zd}$x=0dS80-^<^JRsKn8^-F>2-*Ud%sP;tr2XKq(>OI8+ftG``Pto>oCRFM3VRAl>Q3Ru6*o>D*&FxdjwI5ErI)F za)I0ASv&D?k2dqa3qlWB&$wd1BecZg?hh~wh@TOENS-Cf3f8Po#yQkMF~g^*PVoja zaBF_ld`dwb1S=U4UmtRAyS7+!z$6P+JZl;qKj*fC$J8I7l|0rI#pkeNm!9Aj^dU@2 z+-=wuLERh|8~f|aA=Y3yR;W-Uk6z@qp^*{mw-?l}@=fZ~#)8iXrp;QLT_;`|2uc2x z$8|4)4A1buYq^1Q%xGuLNYqOG(urU1U-8~OVAsG&nFO7;F|whpaP4`V1YLirrUEI# z-|f11(S36wN{-HWu)BUOO{2_!2R!1jn;x%i0C3ZfLjKwl;Oe>D&PrMw85vo+@#~dI zPo_rPO-6{POT6#)rqyY?wb>qi=gDU63OBowyZ{zYOT}2B%TQj*2KI+#N>FPXo33iP z;L{y;$o-;W_bvl&h`4`w<>DS2Je@D&OWrr)iGr1`b}9_2+)_~1mDt`GGYMdJ3$#dr zim-T2r);bw?VROIaJhfhd-2Q6&_XqCp>(BHVsI*^Fh7rHuhgQ(`aunQlhllcG zOCm(##nCf#&Qq-+>EJCXXQJb#=I_=too|6p7CO2zlR)N#xhyE+3ASv*HXAA+?0 zH7@}M#In^y#;3V8ut&L+_pao9qj@zJDv^VqZPPOJU=OG73kPMW^n>Otu?z$@NkUyt zkEg?-;l4OHLfVR_?6wOMaND_>;(8XZ??7^m$A7%B8blcRLSE1?$G#Z$CNkiJ<*B~~ zrb<|&OLy6z@DADJD$AGdl8n=jaYY1e?MnER|?TQOhU(bpGE(az$t zF{7hasELfbfL}7NWZl{m30&sM z^YOFm&QJx#{tfb0%+j^Sj!zf0>A2#qK9t4w8MqHKNN+ENi3XJ+483X#t-3Ch(G+IU z50+=i4r;$T$rEIgTjlm4jPrU~oA;#Po9u7I*$RTd3llOKYQnJR#C$AsjzL@cn8@SB zpQS*_8Mh~2Pk2^NmB&siz1n_*-*5B~*h$)NDe7v^t z^J!OoqSr#%ot)DKWlm;ME#a^Aoo>x6j()2^^=5Y>uKlAH0KHdW$+>O=Z^`(s+55fU zAPm|&LWSc4Z7Xfk(#8GeamogTg@xanV+7gCwhQLFmdJ~X(Xp|yohWH^==F7Nu@?)0>*>tDuAhEsQR^`u?Ev&@aHNl6SQw^q3S!mHL)i=2*;;XIgJ8agN-UgKOH-83 z<-lVR@WfD$>6C!FijlRP)BMIau~51{wC*5I!6!+`>VA@~5OJjj5W{=Q1pB5=?YyV- z?%1@bf9@=!B5Nn2j1ph8mE#H% z4o6AGH9I$ivkwc)t9U9ragy50T;!Rz0ORR$SuyYZ#&lQ`Y@Xj?q>`l}+ZSS?g?R1O z({VoFEFCL;T5izS^VdtxUlmBaeaZP^$FaKF)tRoYsmbOTdW9(7;c>~gU1P1%J6jo_ zKFxG^N`jZ>h?Qb89WcCHDR(Eq18qd#UhDj95k&?cq&oTi>vJm6X|}Rn=aON!SF}Eh zPiJ}(n5C7Jl&p@;e@aiB6$ln_8vk((tQ2(mAa<@8RZC8`B_SuG8w%u4&p)GA4uoKa z1noQla`{fMvo^ii{mHr4pHkF53}8OVx>0lJ8A1R}M!m;$kvD?4KTMz7#BeWZ@LcUf zLuYfV)^&wML~a8n>P8^yHTCQmhX zzd2s<<>{exyzN|p+x)-=atDvJw6c1M#bQs3Ihh7C3TNV%Oc!d6^YKW4R|idfhg*8~ zWAMA|AnnTKF}4>ZtjhnKNlywz<-)7gSkvR3?#`7UefTX};~&o#Aqbe2fTum5I%?(` zGDqn<$sc7F7a{;8@GGyaISXl&o0R-?c3K)oY+PIg)QtdWyObq(5}GfQnq(J0UER-w5RT9LD)TD=%TZ!vr2|BBh zkdjtB@{n<_G*;0+o{X{RJf}s+HRy^kV53Zhu6|$*VRj$l|HCKwY%0%qf6%FkM|8TB z2cW<9NXeBSY+>Jld5%epj*Nd?3|zAH*Sd5<3U!teOIJ1_V*aps@*)DNo0s0iOm}=> z6NB(jmMX|&2Zs;b&5~fS3%a2TPr8HSVX>JJci&(%$sa)Z7%(MN}J}ZlE^A( zt|%TwfKW3VN%%Pnlkb7Y9lMl7Q=tSyHls~uTQw+=xYssaVg-t2`P1{5z1+Mio6J+= zRzrH|djn_E(1-w~=b8xEPiF7Z3UGF6d$f!aUet!u5A^O-&G#4Qhx-ix*k-)7v6O(F zxfVq*!cILF%knu43BdonqF;Kos!nK=hqBJVd#QHD0hrX6KyBxB#Tq^yecor4o*>@B zPB}ab@Ge+upD4@TYJf|8gtcY{^zXCQdtj}R`aK@lne>v9*)Y&FjRf8zFj`hWva?hK zdVzuM>KZ8*KL{Js?}fJ(rlzt}XS1h&VTV(*NM7TuTJ2oDSrlRbE#E$C4`A@>tQu$D z+M}+=d(%)Eff*yAVO%Ns$c;@MT&IAfV9wi2~t(0Nbd+p4G=;JB?R7kbH7s zeb4vK{4p0-%-&i1zSq6VZ~fNVAD(lHO$roKEP?bPsDSrUmz-&anQuliR~CZN#9&LZ ztzf>dK@byTWES;V=$E_<{d}Haxy@BK0EqSR-`{0Wc#OGq1BxNf$^^*kT_}#eu}EXgW2mwQb*>|9Lni2YmE*gm@OiPQ^Mb_zsT{t*ZPt(`Xk1n3}5HAfv@kI*bH_p;~d=D zT&b^ld=#{bIqQdl1vvealBE1A01Ae=`>B9;O_1lQ@(DK3$`b~WxTRaUrmxv|Rn={6 zhzGLVrGCtwctGP{Mk}x7=1!a77enO?wiO)w&rmW+!&=~X-OS9)5a{)J&(--x1{07s z4fIarEO#2zKltV7$zM=*)49N=6%pr3M}f?$98w$e)uRBAv2i$A8Q#|D(DTD@@carK z^qZUdR68I2xH^T*FIn)rA5UkGg&Wvc=6Ebm3l|j>Q`kmRwJpEBy)xwE!>5~d{ChOt z7Y#7pOU*(%e*y5<`GC!C>Nqd9V{t)v@RUkR-**-=&fmV}^TmHt`jVDeq3_F29VhzZ zea!j6eLBa^OZ?}%8uGrbxan%-7gdW#} zVG3mo`4~8vd5cdvPq(K)E<=r!N2*9E&J%INW)zN48?RnGWe4_?fm1O!XncI!Z7dAd zstEXw9erAcvMxW!h>XNOPOx&YT$$FVv8FjCgbBZyTDtts~r zH_(X6xNw)k;MV|vf*5S&0)?&vCP1v{9%x%vmm1XnA@c3u1{)KL$miL)G4dVMd6eCR z0oNh%*}S&Y+vOiqY(84qUQ6I#I|bSmIyw-=vLLbNF7^Mx^xowKEU>;-1M!`D3EJ<# zgMH+A@eFo1wUmg@3PvTi7F^6cod{>epvQJ2AS9sx#2?KmsXl)*)Y z;RA1eauMj|$dIF3zVPEk5UL~%8|wYZMW;Y#CztHgAOGdRgI&>}#dfCv^8X=v&yPP0 zee!=jweyej^w&TByNc95syz7Qzl#;eObL+O=LNQwmLIzB|BNfT00NY7+`7S!V23;h z2=3Q?leu>s2B4jOj{sAJ<|l&x8>EZBnJWLGMfFQMsW{ z{wJ{FjwfzA@NgO$8fIk6kIA{Od#E?Z8RS)pQ@8G) zy3Tls=YaK*h@|zA0PPEufeF6d`t*JJsLYW-ZLEI2J^!P_hwr3lrN22sx0{hqTT4Ft zf&A42zdZ9Y9xtEck`5KJXnG}KG5+ndo@_Isyvs(Y*-0>dNM<<1Gjr&#vjzOPfIU6- zv%VSvp?~@lTdOjH|+Tn#_`u{fYqz`Gghx0kYN3niJx^K zsDor)V(0uOD34eN5_GDI$NvbG9(IFf*xcBW`XT6ai|&}JS{MLEk!pQzg`eYs4L_zq zpp=hRaxwv}%CDdIZiw|5D1m@FWLe*ew-2%LmDh(5V-bpuO_CvYQM*yU{MCAK>vt}I zGTGVQ+Y5~l8sM?V{AHC6A3ygFT)oG`I51;bF3Z5!c%*zQ#u&Rg0BZM+0BWjhij~@5 zkivzTo0<7cVaqCNJIT_F!&O?Y;nS`p^kU*|yed-l(pn23n*Pq$HV^KiaoPcNkn~`Cng6d5OhEa=+0zM za^Pob`xhym?%mhAEd~AibN{9kS13r^Qaq(HVfr_vprKrE+m?c%^7_9iMF2&Ii)ouz zM!;&o$?J-{w_?$p1u5h!R~ZVhj^LkkaQad0O`U=kk}|>Q2YmDJb|4k}vp|Y!FVFUq zQ%3&mvUxD6E;{iq%JzL@5-$2Fm1Ip-=$uIEBA0Oc8NidKq!e0?^4Eet5#`iww(gic zKe-#}@Yxw?)8uw}?Hm;hmJirN=ATH|BIj2|ZLq%HuBrYsF<3IyXV2P>0|PC+JS+8N zWoKP&A^B5Sdi$k$S4g8t%b#?MT?`f->SzxLzplK&>dh3ZaMD|B^R#M_hw_=04GN(% zAx~f&Qk+Ix{d$cUW~m*L*n#%r&LsarJF>^Jr18^~Bk~UCcb-MTg;OfO#q#EcWwOa8 zUUN0+*&Ahi+`k*`KH=DiIr2~6@2Ef-ci)E(zqIL_DY*Ot*-Aa!U16$9AT%<@>DI!G zb|wxi*ML!fo@&ZZX4K^qAQBwU`?7871wW{RcWR8~F_Vk03BDrAUuH*cVQm2YKK}~a z>^*4f*TEpZPB}4Sau?=z1=N9T-dgKYlbgSHz^i2_oy&x7D6b5804)i-05TQo)dF8p zrFQ5e6Ssv0ob-$vilxxH`)z_Nmzx`5WM7d_cn(kzmK2uG9mm>&|eM?FcAVcHIImUl1`B zJ^xQ_KG9Xw(ME5>2 z=bu0WHmWz_LjgGk{FD=@l6ZdBuaFX2dIMp{GhefjuPcbYEdF9?poM9Nu;JkDBnS%k z3ugoyx%O+{ak5>?^8;eA()BQqi{S|AOEQDbw!Soi3QFgONZt;^1U&u@+YNl}fvk=r zeOlWqvjFLn@l95;mGKh=X4vNb$-a|hVh9HVF-F^~l(zM04QziDCctHfKiS(ILv|4W z93iUV_c%b`2t4hWvh97cRd3k`#IU=5%Ya;rI?(uU6Qi&D3v@?e4EXiMt`^!q_IUdb z;L?P4h%gOCCg>>IYC>mPz5!IT^F1HiJtIG*@<0rlC$@p)Vu%AVo@!F_LZ{1b9NLRb zmfjtX_D2T=0-(L!(~s=73x1aF;Az7JvuVhDn>;IIbS^x%BcF}I0!=h@v^L$IXb7w{ zg>9p;!cJTIqmz0EHJ3sG7oSOI&o3@ukp4m@-f44lD-2lt9 z3^q6oQ_M_us(j@e!LhQwVs#vP5X*}|{xf`0r^&DNI|-#x+YaQf9@2tWqzArWtNAUBA}i4=k>tyCM2Ce914=I(iiN@ zJ{8MdWE*h&+$0d>z7YlWAFA<>KSDNDsl_ocJzCoI^Vh1yZ>BuXdx>TAVEBS z;oG5DOWUa3~Fb%wNsDGN-=^8JP8yYcw_m-hiGj$Mng$!_xfP1piDKq_23{jXW&DDYSrj{_~H zpu3$j0aEWi%Ccj_4we8x5+W)o$oE%%3+TYQ!-Vqok^HAU>wo@0zDukGgzH=|$;rqt zRFAUTy2|qKw#*uk0kS3yO`S2%w8>{Zz7-a@**C*BD`FHbp4ir|t4}0ynT{QEyZjwsrE)t@Hpfqm%rO zh5p-DbNKI2xzE<)Qq=t`Hjko&g4>Mbv*d8jjgo2g$z)~I;Ktall2bDbpjgL z62zD&`EZK7Qu&N6Q$tY8QGW?h`Vz`P{MTE$=|WD7&j6FaJ6RBUCp{ZYIxL=xY z=NXz%!h87@-;M#WO91a2_uUcGe+Nd)DeHomSa6>BB_;{mtA(y}9=B{fM}642VEg^# zqUB_bHeNoyq3hT5X!T1hc9cB#{TNf|y(0Q+9{R?Eq$3dtz*s#5rHZ zd8US_9>uIsGSPLjBOumsy>?|b{@T-C)vp`=ZKH2FxmKZ%Z<6n;?a@&nUOsitX(Z96h;O+3`XHF!8D0p z$A?b?1C2za0vIT}b<5(sCKh-LVc7&iK~L)Qn}aryt}vzeTlv zjWU?o1VB@t9Qw)*?0Ff2J(3HG$%5pBStuc$IN&+_^kLnR{ULJ&UteA;otdy1d18#_ zE`HwLGvme8ICJEn@~iYi8(j-8Ui`$!~i2nCiL+ znPV9J5^jMa!s~GfY5MfM)?IzC)Z?DMNjJ3}tl%2-SbL$H>u5olE7HWvo&HMBHKSic zwEfkf`=_NZ_*a?}by;uTny-7UuO5QcjFjs0ws(tbTcg9k&cMpk!^!j0mjKpw+J=Mw zOuT+auVgAynq=?^ZUebVOH8Q3a~*5c7B$xt)p#Tp5w7V^n+NBwevJ1T$#)u=>@fo^ zVyS)Xfy(xJGL^AQ8)eTWvruS6;9$=2Gr@D-mv0ZJirM$ECBr?9Gfk7DIAY-@@*4ZCzZvLY0N`?UXVSmI{BzlaA-nmsB~$dnE}56X9_-dKuj^KAmFNy!sJs{yRu)w@la-HHQNfw&OF zkuUg8_hVT8lywi|G1=Ex&$ql@K5i51t4kwc_q8P#MI%9ycAviHE}3@u%FBThxb8Q8 zlsrppF$Qa}`Ke>8l&+|FBYo|>)#ohx(BwIF2h3e7>D5(&y-(brGAof+lkEcrC2pZV5JqE)MxY%V+5apsBaeMA9I>v*w_xja+2a`Y; z!po~8K2VTBY6)Rb2AUQXFM+T>=KdklK2U9-Qr5Ma^ErAizo*gq(lvaXzfcL{SKbYG z3yYe1A211FD8Hi56?IQ$%D<1J{}_%$xNrX6$g%yKTjxynd8?VM?)*|A$pD`0;ezj6 z0NTfmeXbU4edvu2v+|1()LH!ST>d7#JYnkLa(s`b*`{CO$XOyk)~Dw6 z8D}cPs|@_vrk<>H^o=a$t1=Bdj}2Y2?XBR_LH8XI8!CL=OzXof9--2<6qo5Q-%D?+=L*5jDC~^B_{z)NTAl zQ@dHTOYKN$m-emJh*$A2@VX?OSD}SFRFV?{#O@-}pORSGwrDw1AK ztiN1VcKts6z@|7Hp?l1Qdvk7Gs;idEr<*igO>KN$Xw&w6MT_?O8A>|=;#ySENQ8~Y zXRlA^HBXW-BTaYw1?`9Oxx78Q)>l7=aB%-(Bv45xAIfOFry=OI{>|Pm#q*i7*C+yG zz`Er!ni@ryqva+zOXBh||rA}kvZWA31Ba05=!g0TqBYk;ombKRW^1xFBu&%1o9X!76=iY$4LvXL2Dim@^;og;bIhR?8cYpA2Z zbcForG^8Gj&#)#}7~G&DDc``w{PicFExnb)LsDCw9?}kjtKufME3^+#gdES=;G%Ei zilgB~JSw&3VaekRz4 zkz|9bOpb``a$=ad!gG|h^Rit?td0 z-3P@4lN)xA$@Zfc+K*W!`|3Y29qewP@D)?TE~oYkuh)SS3tfezFj?EXv;6zq#H>NGkZ15PIZkx^5}w=A6MH!htU@ zsV}3B!-{#YQ*)J%RnRyNBM!mJg6|53Q-zHUzZHyg(GAIe+oU%E&(51ELs2NQ7S0AQ z8@Seg{xzTb*x8OLn?m&5msh;*hHIUadMnvgTpi<&?t{12uhUl)-W&+FZTwg;&ur<- z-$4jc#^Fq#-+LPGMd}y7rO}+LODlj7mLiQ1OT0Jy#n_`}GorZGyoiMQQbaECVMNo1 zi_QZ+NDq$u`6q*S31euyDyPrtH$BG<(@|a{52_rL{B5e+400&zm-p^}bi4S){j7*- z>Kd{ln-9A>-UHP~iOroK$W~82!3cc3{6Z8O{r;Cz0cJ23j7#q0mS4;lnpl~=0FfN>LrFbsNN<@Mx$01n{J{mYL!z)v=)wCG!2{MMd*}%?R_{>u%7^btS4|u zdn$6Oyo}m#Sd0kf-lzylm>H??70uvCf71I$Lf4c9vI}NHS!)) zeOOy&m6*zsJn#CpaF@X#D&x+)di-xW#N}4uRo+^7Tj;gL5#Y5?jE;Vx~OpcKC(kQbS4r0=Hi7X|(Z^REe$aVW`TW@SU zEOl|M3y($Kyj1^;Nu&(P#OE{dd*m@7V#H5jo08TOEl`}PXpybN>p8wi8w8G3@8~ke zhAG@7!>l-dg&~F&XcK>uxUEj$E(1wHj@8UDd?1R5?(PSpl>*h`=$ljC5OsSUim!j& zjtD;Uc@mtj*m~tC8{=O!4iSeQiq^|{+TeO-)wv2scrPgRSjm=sa~hCcs1x93}FWDp=4+CfN(1CjEOpRC4W&Okl37tV#&<(v3pwF4+uS6 zr8@A{C$I1p)MuRg`W@JNV<R-2FUdSiTWq&*_ChTSQ}UHBjq7! z!2pD_`pOtvuaau7!Pm2J`nA#0F2&Ra)3z(MADg9*UwJth2|G49&085R+3z(Wre4z| z6N_K4Pzk2oA3i6pqq~wv03f&rmLk#`u~^PJGNCMb8QWcgIhwmtdMW30l;E)tZ3T9)-lw9{^hdoX@{H@EoLbC2@ zzpISRw5+Z;WVQ#<^W|PWNvmJrShI!5V7KLXwa53loDYqUS)_pMgx_;X_8c2T;{{~Rj6U;I z&Cwfc>WRA-SKgqiBQ-4ZmfX8B6-~E<+`9JglHz&WOLQ*oSkSF+?L0`YTsphu1bIV! zI(-$v^u8S4&FM7a#RniQCp8ArHeGbDRqrKE*!OoWXUoxc>hRz*!#qyJ_B7GwdA z@D*J*FUw7Mv6@}&3CsrRuFjaGiS~jwSg7JGV-&7S5=M_h@A@k;ew5gtbNjJzaLioe4r z5PM4O7_anS)T>pY-p5O|Ji61-!SM_h(ysa;wZ@){({6R2&-aML`>PKxzd@njm*GRO zW@9C6j_de=dOmt_kCk}!kO+FXf_}zLXouLcKT$r@U@R$wXA5g2rzyodpT>f~N%OaZ z%OBR+o?mqHZ$1%vWr<1Be?WHLxB&uyT!?!4yWiZ!4Us-{y47nRx@89IrQ|cd+7oPd z*#_;L5f*?^f-4#rt|-|!Z&pp=N0L=Op(`^8*Vww7;CZLmjY<=-ZAvTGrC}(7&(;fO z{EOt}nZvs1MZIFIY@ify1Q>8~FSQ9x&XJD|J;kCTXYjIjUdg*7AT2w8GlaXqU@W z1Ugyn4bAS|Jfw83b1A(vI$#)D@h*eu2ixGzr5gz;?TF`I85xG%?=o>ue2jQ5Y1#Rn zg*SRMEgt$*(n&At4@=ZxdBd|I3zv|}omA0c3Fe`NYyY|#C45`g&}DpsN$RfUp*p&p zvh}VI?azG%^%$jn%t5;hiie&X3-;0Bh|OLD2dTS^bIjm!3IS_H9dQ^X>GHNhh*jp* zJV`?qo&nF=2`o)Mg9tl$+_nu5kg_r*krz~0<;waVOi?vHPJCEf^MwuWW7N+!Y6N>XVKcGbVexo3{OXj>Z0hyRdpf=V{!ioZ*o`oW8O4(**@xD^;S? zpqC~fQ8i9|ulD{Pll`1+U}*-zv~~KeL>~#M;Ml%$vtD2T&ZnDQr4yZ38qO1g-!*0+ zTvJcEetlg49;WAcc{F3iA?T2|t9zm9Y{+$$e7`&xyNJXf{>_w(WLOw=Sy-A9C^qsVid^jm>Cwe$OX+ z9Kt&DTfvau;G0Jrdz!my=rM+xNARzjrOv5I*;|LWYVKOtk1=M9H!92NYpz%>sLox4 zuO7StCW#%+HXN{lCwgv$b>_Xf>6QX!#|}1BK$GG7NMx+PhuR{lDVMJoE}V!b(TZQQlveH&`Fq@@`ASH}-Mh2(x-Xu0h?C zT3T}@%}N9V0lKh@rV~D9dK(~3w6FHu0BD%HMab-V=V9&Wu+Kz45OGa#$i997rZ98h(dY<6;JXna>7IOH#S!1mhssf=^r+W41M`YGt&T03v|#M zm|f(XSs9sLj@UZPvl$~SKn>n+$ zhr7cZVmUY;A`VS!A2q@9?$VoD&PYBcbh=3m_MTz(?r6b)V{a|7>g`%cL&rO}*|5&D z9EH6rygW}L)&anY4gEm9W3h%oRLW9uiDAduLZ-L_=lnLI4FJj!2t#G{=>*ZGk2NZ$ zhiCf=<=Bl&roPxy^D-Xf*%0uz(5RexUtj1-Bv=X6)mW&-_Kl|2Ew#GhbXns|ho9#h zfKU}Ej^nco+n?OpP>#4*>a$$nW^m!_)M@o-I^0?y^XUme+Goo;PwB2G#eRGu<$`js zFLqGaa9InVWGd;(pTD*>5r0fta&v?GDDt53vFkjH$CWr30$N+Qynxwx(#0l(Qk>*& zT)|^k;G7^X;cwhR+k_)J_{xz0lJ+nZi5};rwb5pANfoYi0e0E3^?4|g2}ODb5ZpY!#M$j-{ZGMP|O6AWmF34ae!|CvM!g9?BdVGRlwv<9BSjy5`6u9vR@q zdlQ>n$f+9qpje?ZRAqz@r*xdJ88+wQ?|PrE<(qu2EPUTgMk2^6Pb^;UCr);*cFwBT zJOzk|Vlz9dQ&Lb6ouTskLWMISA1L%8(4>e^sP3Fmnwci8S)UU>PfA1L$ufls6br)HlEtMXF_8x`*Ru)-DQrQsNxRXp zC;GQ|izar1oan*vS>#AYgy3TlR@d8YOAIE;azoi^Lf+ZVX6XZEyb$R0Vt4t{;GxYd z0f2UR!S3j6uRP)9RAO$zXqL3L-rM3^jRmXKPx#M!Ybkz?k^W7JYl^DxVzraH^jV{o zbZ(jHXV#=;2zhpZ$M%qx18IYko0&9oj8GJ|J-uqL`sg?LHqLc?0~otm`}~RX4!!S~ zL_94Ss8lw?_I0Y7fH9SqwJmttXkoCO5&0^`aS4sbXr`=sX9@jIb;EoI}$%DJr_o87$s;p&^+v{to%Tl9D z%5!PoI%Z-+HpgbfUFM3|aWKgyltojBUMSLNWv%pK;jON?q~y$92B!ndhk-%)!j*YQ z6$jHOt*9F)a8mE=Kxa`Kp-hvN_Sjm{#QRjgeqMF6L)w2LG1KWrHzKS57+ANDcfJ@f z)Bp$QC6JMI)KmmSKqu;DxjYyAH72rJS;m~Sh~6lhMI#(|HnnXk_g_GOpK`H4vjQ*_ zg{vu29THsZS~*TI{cEO?scz=w{((=V9*6aIK-IFcg?DkVON7%|aul|xx*0pRa{tOF zd(6h_BzypJHud3}jJF{ZEtI^&fAMH~gMtrqwcg4Hqp3=!^INktft++k#BbQ~+p!%y zUQcX#U=i;t>iFqRl!snYpJNDtt$Km42seGV_cJ();{0R*58T@AW&z*dZ6GmcY4KmF zcwrnVD}Rk47xl}yBz zrz+q2SX2&Mw}xUFUBL8gPkFMCEWSDn4^nPdWQO4*o^rWcAD+pUj-b+R3^{h}kx4=e z&uFRo@~ghMXuE~e&b_%1X-XfKyz2QDYd_@Edg9BCah}#Hr}^1Fj*!scnEu9se!VzR zZ#Tocb!D>H>sI><3XO|}OMfj`y@t+|o(9=a0%u!n{}^(< zRsW{*CDnB`#c&8I8=4o}s9Tf~qvGH(3u~IYuy0_EJj@66vt1Np9H&V1{34X8gTz*a z5l|ZRn9U2jca1y7Y0-;yR9Oz*VK<{;^YOmUM6BEOvW<6pl!9Z?SIbw!I~++Io!WVw z8z^ccQ6VF@ng{Gd(xCKFw$>U4Lx;0&q~gSJ;m^FR(GU@M3qtxA`?z_Q!b> zqqJ4EAbOY<&{2r03Zj6ntn__SqfWp>{j9!1hfB?UWi2Q6n_w(;zY$;LM^ zs)sO#dKmRYol%*X&M{8^*z}&uNQx^sPNu+y4OP-TCa0n{MN&u(<|~~u4{->=lu!H3 zungYvS<_oYt`bBQS=`?HC61Ia9y_KUD=Qg*csX~a5MYl*Q-jO+sag~^v@Rv@%G&Ku zZ6F6xeIh5}=FIQ} zd5aVr`J27&TNAZumE{H$YwqFeKJo3dc7)xWqtBP6%zBGV9k-_K90zcn1lOVx&o0>h zbh<|#qq)g-lSdTHKpg$${H?3~CAZA_LFuGtdD-Qu`5+}<$MA<&W+Pv~4L05d>u%N% z0kWP+rjc> zfq`#3+L)RDL5DwB00cIHRc?@9Z}Jta!5%T;9lnrB7~J+kqL||z2>La-!S}e*8_=2i znkxgyxT?D;5P+_8X(?&~aX}d2^mKpibL@gWXJ#c9uW_wSULC zH>aV%n#9tj2pVvp9}x|{Le^3R8kxWzh(@lD?_gr=0UhdWMw3)gOja)S zt=t8Ij(rCvouJ*fvmvpH|4y4e%;B7V;By))29!u|l;(|q~yB>6MSXM#PX-4Raw-PZy#)&CRCW!fssfHve@ z4#J3$B&68OjdiFdqqjdO=!C+_g4zC~7~|~I-ucd&^g=XqY02zTr-S8eJq?M`T%YC~ z@4&j92#FRUPk%|29O-Hhtqo^Gve>!b&xuQJWI76A-!r`f%nqeDt)$Cb6f?0*LN_hT z<|4VFb1>80!5rey%PhBAT|ydPOP4Sk%^OXsh4U8H)E1_*(wlj3sT?_(1R zhvRO4=K}oBZm~pyvr`~Pv5V~3CBy;MTW&|&q8Ay}wu7PBF*-oUK^}4?^*l6^;0=~? zHav)BYBg)2b6fj75{3^&Dm@(JF}Q5*z$Rj2+n?>}WG)!thUImT zaRyxSoC{DKDUs?Vtcp@n2+G-btFQ?1ZLYj-#2*^+$Qr@$0_K`BfL(E(YooS@X7&PO z^SQZ;>L0o4YV6O$!X z7PUGqeK1Q1;?Dr*db9sS@Z?O;YF`@1qf(Bk@`g5f*SM18Sg)!Jj@1L$(L_1jFPX!# z#P#CmC9c1NMl1gI>ms_bvg=i>hn{lMvd1EVcn{KdTCE4fj2Bhc)n3kw8nE!qI2Tjw zbLk^D@F<;syQ0nn?I6ZMr>SDO( zVxnVXCP(Hf?um^L@lx5O`kC7Ui?mEqeAEn*eS3%^M@?8G<1=>{G){=ncac7woD$Ou zITyTA>_fQ53M%(W$qV@J@ImfnIl$Uk{puRwiaoRZB=JmryJK~3R^O(#tka1n0j8m227xf}4+`Uzp#rUv%I-Uj1o`6{<*C1-%^|hc^lgUIG z$;)_2d0#S{SiTLByV9i&wW7F*j7R?>fVhl>V$If$0;neM#K7Np&x5p9dh*aqsw|qB zz2JZUx^lR?oi;X|E`eACIcnP6W#N+7v=?ITetzk;{AK^Gfw0z4N#g*OH(0Zf-#5xRWc<7_`rg&8g(wL;*Zz>E5)l-> z(_w&(O@XuGZd;;W0zaJl6BK?2>&D+j(L1>X;cmU^J=DZ}ee()@$U%wfFIdwpOZTBrRjTH}b^SUWG!@r|pdM z)9H{oeGcRGsZ@3+udkT!dhU9)#}-X2tO^g`+OW9R5H}ZJudax(g)@_^r;>_5N&Zu7 zICU0j7Jx+APW89D#9ctmB=!q)NO~W$@#<9ScGwE1S%dJ6Ez)Z&0lq)_jMC8C`Rn>y zj6OL$LNhjHgVx4ReNKxA#_;-|QkS#WS7FI*Ubk4Wf7DKC+ZA^fdD)_O)Es2|*_;pS zGTFQ77(J8A9ET-V$7e(8u_3W6p`qB8M|M(DqvJ#|XDP*x%YaH52EUAK+>&NbY3qA% zgY7&>6j!@AGCx;%RfBjOLiC5^+{Wit$h`-rLY1J(CMT^U%WTq+X{m~Fh#+~Y8sZy7;HSA~dR_YVM30b$ z+|OcLy5ljt4}^HvSo3kN06Gy}*!M?Zu>FT6)B$-V^Vu?azyUSWix^2XP$f2x#_t+2 z@LE7PnSINmQwh?=n8Q?#38gojpeb^0EJ!Enr6glrJqCjLPQAJqJ5)i!uePd&xRiT; z?svpDb2We3#Gyj$n!TH;6w+a_<;Rnra39rO3eVjGHUzS_*qNq!5!?^w_KE#!E(-qo?9!}lZcjH zRu8FDx7p6+7@SQOKNosWnfu#I$>*qq7XB5x4<%tJxvdYzO%S@+M!&EQXngoQX0wsD zRf+$6`!;+| zgX=v&9G*aBwoEw}rOqQ+=ML*Mx;HDlka9uP`2$$EeTpwOb4g0V{+6QQ)eEPu3Mm5(hi{l7kKJN*f@o$_)3whUG;rlWTd+DVC3STo0 zcl-pKYYe|W3&OY(&>p!sv}boguQRECmqC8QcmCh3_Teh~_33H!xXJkF-}tP~EqxVgwof+9z>e|H6_YDP-9Un&&P z=ROeT>Qf3To3*uKnF3Nic(;z2!NCpQ6!(&(<;LdNa7tC*U?MzgfL|;ctxeF5ZlWAHlPeAmXp{tD~!%h4Gpa~>ptuLvw#!f^5 zxOt6;qL6X7Z?(Qyd1gXYIG{;jLZ<1S=kRN$lx}^3SgTr2m}ju;I|I)`;PSsb0=y*fh25(bTl*eC@9AC zb(|11Rvwhd1f@@^x3R8ct>`TMX%p9B*NA#3aBY&nC(O8x0*>g2*Ve={@sY1zWI#c{ z11OM)`=u^Ps!XazDVn5wZKCVtiueleFX`iNy287k{AG8MN zqd~!X?Kr3u_;<;Nr+sUl8#ug$1IZG?gm*@(BYrv}=PFD^!1L2v^f?`Tk$a_;Lw&mY z`McdMtC>TxN8&M#+AikSR3)UjVg4RO@>Gg*nFf~Qkb#zsz`+>ZEcb+&o+_6XkaawS&4I#RC$6VIObbo=c3%hQE6j#N{*0C3G z8G{X@d>`01EQ1NQdFX%)=Ey&d%_Pi1*;3{=;JV!Rr)$8(!+S3e`cHKd#p?EB`eZ7@ zjb$QB-q5?6q@8LB9p`0q`8Y8El5Ts00&e>f3k5zeTV&bS`Cp(`5ErblPTH{f3 zG`>=F?)){KsnnMClr){;m=Q+^!*(0GCX{RMkR~?w8GTn}ce1cUiHC?DV^k9MVuSs{36B>L4cP-Z%GKVzsw$_T2{ z^Q&f5Nz{&hgZU<*bsfrWaTRw18=9krW~sg`O>9j}g^q0a3*DTUYg!$6(g>O9=jbu% zFV)F)j`-`ELW5*5%~2#ZMr~gStuzU~c}Fj<4fH%~am% zp9ys?w`DhBl}qJ>;KH&kDIz*g)i2{)6d`K=9wJo>jWBTU8@;aMv=v6zViOS&eLC9UeE{kT|afm7PVDwO=?=LQr9sb&fSnO zegA+-RCokxDANpn^{a*4Rsz$n&;9KMid-BHOae-Gy`qtf6le$fP$lND8d44u)d7^4 z!RI^(R${4xY~<7D_hh#Na1-8x?6GdE53a&4CkbfgAIutipF($L9Tb7_Xd8mj`&V(4 z`-7hx4T(H#ywzcrZC)Q|v=Y%tO3@szOaI6@QYVKFQ5W=nYwPbpomCDz|0VXB_1{Tw zw@2w*!0k=OLg|cb^qC;eb(KGDJLXRMh?iQt_(=g7C!>R`lBYI{tZgv_fYUpmwz}>k{0i!wWML_4OAq<@lCJugVYIWnSuK%L}Kop$Kq~!FI1uGGk^C z$56Q}>u-l{C(U^<4sBS9c5c#Up@OYAWZhkyU0muq0O2%Tra-Jr%;S;ni5#x;?ysmHh=yZ9ZDRwSW*6NbWfH~DJFVNvK+NW;RtZU~xI|>|`&-yKS-lws3 zM9}t_gFUZ#RX`?1s24pD>sVQ2s`pGl287ThwkUzS;t%r=QE!cI70kv}wPdy2jQx=+&4r^Xbddl0{h+VjPPN@kw!{>e~E*iD9d6Vv8sb zzvwr$rOGmJq*{`oU?(&Bcww}TiYJCqX?92V#FMI|EGVjdkJDU*eiXU1mMtID5IU9gHEW(@}6{oSt z6;h^PjAx$A0NqipAIH9vvFr0XHsR0r9X8=5+8B2n0&~KoHWi5TO)W$>0x zQvX$$1cS?-2A{}>HB_aSP{&oN%8m(}8b=CHK9ltsO-)bid{609fmFBHm_#o-R^7v& zKNWBbUZAm3I3lq)ck5{V!NBH$`KyMm^%cvgczzHo^35-OKqpW>a{(07dP? z#asT(oLJYIwIR2xZ-)Lj)SPKxQ&hbBAjZRhp^9e(swPJQbfyq(4NxA4)?c{Rru{~_ z%Qg`3qE2|vKSba``ZB|kbM720^}Y^pRcMh86lT+Z^ku61`kEYIM8|n`7ol79)^nx8 z9kwCrY)&bqeIHt-QoN0&XS1M)`cQAX`)nPuv);m|tZ)%TvG@wb!n%z;imth;Xhy@* zGVFR&`-xdlR{lis07c>KUYJ{z=E^IBn>x0MO>r8{vP#U!M-9c&iT9N@1%oDE+-u%t zWT=C1{1yNLEc`xgd@Nm}kN5jWhlG=gi+7V=`y$`?&=?!mqqHtB3d0LtPPW+#TL$S% zYEjva7l^PxQvy#k+`O>^Y5fgs`llEVWE!9XZhBFvf|!$AEmYD6g%xa&Su0IZ&~w0P zm)^&?O}|Ya?f!(2%CS*c{`ocPD3Rh1zDHaY$#x4FFZ2Gf$bm2jeHO=oN@l*fy2W;vcW zYaQghky$dEUGy!yLwhFYyisGO7{6iT_W*^}%ult5>R%r=)<9)ApYzJGGBTJ6`27u! zo}{7;MjOjnMZb;I&M%ioyto1cn?^XH;3Qh*nT~&bLJGFFK>?Rv-^Jf+F_b%79;2kiM zFA#1I-TPmm5XGbggy8$04%U$CRKjhbQzD7NhsiDNJ7^#msj+dJ%J}!M9{+oV4+#JJ z+B9ikGF;`~qYPdZI!7F%8U#iCT_1TNMQq9WKIDR*+QDpxY6d{6StCqGu9}m|zmNEe zD2j)Q`|`Yiz>$9!F-zWN+j|*!6F84uJlRcK4ugBr+}IZg9UeahsN(+oVlO$?o|g}u z>4>3m@-q!h5?f{vW z)^iDYs3cAOZ|xV9kq0w|AHG@2tC+MxdPWzVeHgcZdiN$dfJa*^<@Mf%4fBYGPT|8yuPWaTTKp5;as>T+hjq+E#s&wq5v^Cv$u&m-Vxu_|>B(ojgaIm1T3*o_q@M|Kz#B~$ms*o! z2tWBrrrHDVzHfWEkUhx@5i*YyCwvJOHOqxjTW834qm|BcQ&2p-O@xns(-;mTYed$ z4@M7OTr9y0wSZCJ|7gYGW6pnx>NJP;i+jtK7y7?{PDU)USmhKn?Q=;DMX8}Z)KDUE zv}{W6?Pi=gL@(tK%C1(;WXkrqBn_HK-o`#G)RyBH6HHPJcP1Q)U**h_YeePB(IPsGdXA1uL|$S#hO?Wj6LXXyCiLcB?ivgb9estoKfR zR1NoT^8q@E-V9__HYB?)hEgsgvP>9@Mst&e9{(UtoqxjS{dTL`Yz zHqY>VyWtYgR+4E$_rNzPs$6q*SFe+YD@Xr&TQ6_NU;qa{vyL!cnal7>&iLAhk5$Bz z1LNI)c6DUD=^-HMHlsI%&KBdBh7e%%LF&@i?lr6DVg*{81@2tEsUr=`*CafrH{^M@ zijsbDUScv>yg_k2y!A9S=(XALl}mzcgq}pryH_dsCN{j2a9MO?;-O*mGtj@J2bnU2 zVJTHg8|2)LS_U8kS=YN+ap7@Nlh&Ly11VoZG-f@^G28eI!NO0I^gzGv1829Hmx%9N zXVdsb9FWXGl^({%YMBU0p~VnohMBI;+Y2$xV?cBk_$4-Abd)~;sp}?5-d}5mlxRJu zHfX<-RPf*}xU#15`MpP7{X_A3&CnACcNzHK$;~$U9|A99xjw%e#p$G~ETh(?o5E4J z2RR~CuZf1<*sC54&%kcC22NAF4RiJE&wu|Abk0C=@t8>F{v@-am%v4ht5tg6#t|3Q zUl=Dy6&iPq^GyUDyH*Lq7oVyHIU}4n=i>MP2N09fixbTp!XGozN!3qG3na=d^Vktu z5QP4;&^`VvH}waY`tLa}<#=Ozsti++7$b2J9t-qfsreLU!=S@dxQwx=|4a7lm*7(c ze%B*ne}-5uOfFZh6A879YvS7(vIO8s)q^6*5{}$ga8ERfoV(&3CMfthVr(#c_1v zqB|J6*cPWs=FWF?8gkvDmvt_#cGmy;P&IjZLA)qTEO#V5yLR+iroM;^VlrevGgCyA z$3P2T;g-bwA%8ypQL800!%Kgs+4uY{FE%f+y1Djw^2EpmCI@EGv)2dQ;NX4cRLgjZ z0exVzz7rch5RuC>>4v4Pa%IY$9M4`A+PD|U)ZbE8QQNv+QQPwBm|shKU{U*nDilX2 z^K-y4HyQYgAz>V+4Rcc9$KqEtccv( z8KaqbdSg9`zh-8?LtTSeLJI%U*Z8H0sAAGt^f|34%#`r)`TfV6ST=?R_!V&A)1hMd zO88PWw@c_etv5M&=9rF^xbG4&6bPhe%2{93O<0I@Qr-wsR2}joaMsiZimF{1O|?6P zg0z^=$Og&&fb4_27}g)rKb%Bf>z>}}tm?@&<1{z2iq+RAKSFBmvA!08m~IX?d>a!I zaC&2OrP(BqDywdvFvc^{;bzQZBz&?sj5%>@X+O8n&JSaMMo? z*g0D4xVB7n$NjwhU53|%5Nmf;OVj%7H>u`ZZY7PbHq%!bUdbFyRmNyVlpxz*B}e(~ zAGl~C^3f`5S?v2z(ZVB)oV2&IvKRva^jem25?w z8I!#Q*IeiynXPiyET(i<;IGL*)xd}~cDN%(@cvf6NA$Gr$jcEa0e^S{|Fu$RKruSM4U_Jh~rP^C{#@ z++QbrWZ}PMc3>u0)S=@u*1&<_TiUVNuU~AQkU}BFI9p5am$dnfd~bZ~(y{Ojj?`7Z zkOY=A$BevIr75MQ+H!tbQar+HE*CeGVTm$R9dy>8d715qo#QdJ#`#oyF8s>*; z3OB5>vZ9Wpj)J@wCN$L!Blqm46fPS{m+i+k;o{aXWS*O=n{-uwHHtof)Zc|ggl_Tr zF56<=Vn4Ip1pmjhBBvs~uKy)16TdD|~E^=A09iE!si zjliU^z4(bq^wE#icoByidu?#l!lXnM%w(p}EQ%Y)jfk4=odn+f#H8r?$8M*y z?Y?no-+>I5s{A=*htN_d&fD?b#D*C6yQ9|O3xW9;NOXXKS*MAmNf#QYmkB&wPo7~| zuN}68UST%4GXM5%%k#EeapAKSG;h7xJRX~@b27X8^|zHEM_#cPEhMFxN9^%G?TT0> zy_^}#KJ1&%DTs*GJIcX)z19b<7d6Qo{f0Aa;fBr3W{+F{)JkpM?MDcX;tLsvHQGD| zqcOsUyy$?I{&J8|4lG`f6`I9|`YD_!8BRoeJ=aKk!^h*o-Suc4@hVVdzV% zfLfOIh{F}Ol(#v%)Qk9~HYY0dnr0?q`7Q{R2n_cl%6K=~XZF%prMV;e3E0YWMzmuA z!y3zddd3A4gSlZR4zIOGcF3+-h@PhUSS;jXBec*$@7vh-ojKe9XFT;}DYzCxH9>1> z%n2&%=&Rx_l%|*H-hQ&$T#6eiU|CpcB^8(dgM_dcyazZ8seSEfJf%zch%?yFnBrw~ z6O6kJ?l?9~N*Pm}UP7pC?rH}vFLF~6?(!~zFiVNq?LJXY=S0ZM;uT}o@jKblESwgJ z!dS~IeXEx%j%?T%Hmi6@r5j&18zXGRCorAW{{5_QdPO~201=!0>8V&ZPP$xVm9eLW zkvgnXbhDQEOMojob>havQ~%cSQ^l=wv=^w%CBI-_e89cO4WwUc3Kn-?h))=h&7e{i z`>bebl-DnQiDzSMg}d8vwe&tA-&QI(q@%a5ImyD!tR$mW$Uvs|(7NyjmA=B1EWI+} zb%QJQs*aMo%&O+>9B=Y1PY&i2&lpGc=f5{`%t+a5qb1?0rXz8BIS&yh^^gq5iaJ}?$DuqLovvhAj zkmljT%SwTOFpCS@Qz?G=E)p)l)9hgz6C~1gdw{M}YV1XE#1xtU!9xUsET6(?hwVRiUQ!2zG3{NN^ysl6v1{6<=4@g#(C5Gq|!5wPR!VQ zkCL23$2tSDZ$k@THm%lPbaTI4jVzwy<;%_L;I9glbus5=SI37v2_5QcqQF+zymxc9 zZ$zH=JE&%2%*Cu@W*I>7+|40yQR&Hd+qY@1)bxED&OwY#lmn}p({saVw>+mG_uohm z6)$Kx#T;WcIavU_k}R!Cns+vCt3hT4xI0E?ETH+tqw-5%oym^!ph zdq|UpI=)qjd|43!%@&uxP{M!%PYE+XyOe#a!f+fT5xR-ZYAGumqQs!%GBRDE`i1?W z`D*O>`BNzv5xf$D7LW&3(Q*Q^`x;UpZB1exWoA zM30b~ONXSKXA)JjBy`nJPoks3^vDZP;7F^oA*(t))2`;pQ6|3?1_&l9+e`>h+7sRx zn9R1&npmTmJ}3uTPWO1F3q`7nGJ2Mjx=EOwU=eG8x{`1m(@nsd_MyB#J{M znf%}jN0q!FyOY!nYA0w?3)a7PRVTTGp^cq{?1qxCK7 z%s5Js6JnDI)BGo1`4{WV&LLWZvRb%o0ecKHW>2ee?Cm0~EDK_v^=fHaY^+_ZU$HIqd6m_ z*W(J--1NZB)dsde)UzhFXY~5(GhA6Mjf8b);cr8MmN)j_FLH8 zHz|o)8N-MRvPF6<*BGf6Qu0>1<%(Vw-are-r!CzSd)3h=>g_{m#vyt-`};C(O6=^$ zq>s|x?{igdqYgL2*GCJnX3Dav7L4BC)z?pX$f=ut!3Bh->B^9pV^->fOris+8#~(X z-DD?39e6cDYU=%N$jvX-Tz@&Mfg;__8i7VM5RN|>tm@LmJ(oUfw7aZj@`sPv{!KHN zRNZVdgl7P=2UO&YvBGgE{9%9_p`$)Ng-I|2=&qYm64tNbX|Wvmhs>s+`yDzuHjRv+ zp3qGP+u5-W5_57Z{!5Ig@-I^aiqOTG{5j(~W(>i!V7^<>;$a;wUCB`ga_PIYGZCZg(Rx5I=jWY;YR|`)|0QRC7oiPVNHPu)h$47 z{@fXf3kd;;k8BAx*pNew>YGfLUGc)3`n^VrKqu%f0PmS|=9|OH2$!K$@o3uUK&>`o zhK+9|W{5TSng)Tf+NV#5K}XoKv)M#tlgVTVGn&0(nT@|x@YWOv<;Qqhx~+?1zgVsZ zO7U0<9Uy8Cp9)Ql!80L7DZ{AJC5_JojGhpSF|sKLk9HQ0T&wEz70(6rV&oo_2ZE`* za+Dek07}!rjJo7_L>U{P8CP|;QgkGsyJ$2iJf4PBxg*OMJBO&%HcBXrij3U5H2ng? zqVS))c6V=9H5fT;xEYjkG825GS=j9XsAIUhs7rF{cwutTlSOSoLlfgv4NY$emA3)R z{RC6|ni*B=sb2p(+UqOdc@nJxr!qnSE|V-CAXPl{=@=Agz8`it9@)WI&|=)&0!3xT zDu0aU+%FeXrJwfd)bnE=tE3kmn%hVk&xW8&B7t{_WVoE|&||u>+axK2 zw1xIS(HlB;)hOA17jq=LMH{}i$|&CNT=5IOW9|G@e^wUPFBTrW?l(3QEyBOyIJA&# z)rc;+*sGY5zihjiBK1BNsg@tEE#iXtmXEFbt2N80WU)I~7*pXqRD+&m+?PHwJuEQ5 zlBObR-PyB-!?lAEbxLlc8E~_6GCdw>1T)rAcbMuuDK7~*IBm~lRc_nuMjb7%jxboa zd;Bh7v)-jJsscB6g2qYWXq ztJ7F!*r;2;6Q^PtX61FIAjE;U2>ZCn)2+LipLZuviUu~Z?66O zC$63x$P~5{)jfSUeV=UU{r%^uvALI?Dm#z34VT!k;Ir{`p?m4j8{OR4EM}&H9y{+{ zy3)9jABI3D$Vgu%-+yoT2{Z#g^3T$x`dR%JCiP?uvvuKD{N_2<{P$CULTyJ?mcgu% zr$jj9Al@0dK}0_{K)?3Rpn&AWgG)}^_@_)@dmS^!YA%+l#Rk1<6!gLO%3InC8;*Lb z`d7q7;yr|1Hf>ZLIRXeGgJB+q!T~AOv{JVS7}1W}hidiyHf5Hvtx?R3A=JXSegkY0 zIxMrN`Q>ab1$L&J5A={MSaRJ1U={GY&=o4Fq=UNUMX|UwZ2`x>n;cK0BA>maS;z&bNjTi z0O0KAwfK8%4hF)IX_jkjl%YWi+hJ_5q-MO-8Il6uqpIgy`n;uE2h^z>ui8ni&lL~r z=m%si9q)V)idyM6-HkFFsI>}Ujub2Iem((EzMCPZ7}X}CqJ`zvkyDvYej#yJ+??O%<2a=~+}&l75f99iPw*a- zdU4}9N0}l`rDnwUAK*ATmH{GA59#&|rn&qC75}SkeFsQlhOO~2M0{d3^{8?6v}h3@jaRU8imR6d zMq^d8zemLL_`ZY2r$H}#ZvZO)(^Td_<3ZL$@56`R@qj^5>ZJT0)Y2H-VU`wU2Q^{) zNz|N5<0E(Od>P@0ltCV9n?i8qBU|+aFLrYsL!s_*G=OJ2 z+#r-*iWw8gieH|IlHDxp%5Hc*p!(F})6fW(P;J-h9W>x59&eV&ojahTBV@D8{lO^E z5_>?dQC1wCFdgrM6JVV#ZcopC(Cv#D{fvbobe>m{dixOIioUv$9|KV>=Y8nr|JATTAJ?$ZWBXH^=&w%CxBK;iqhI1SlLzrmOZ^JzX!fWaBp82 zE^B10(7?(oKL%FzL%^+@AMIAf%HNMtL*o^9$tb}ma^C+Z_7SrLmTijmVI>k}67vDr zhCv4Q3fL3nkICE<5YNo0CriNO)OZ*8lfA~7aOD%@40($Q^xGpxf3G7zMfd!E*vaqm zqJ-4)!R?Ww|02Cf3SFO-2nvR8yB%CZRuV1Vo~fGX2G-PrdrJvFE}%~wh|cuBJlNC2 zA2U@8pmSfNV}rLjVUPfSvO6sKI&9mCOyCm}*dOyMr=g>8KcGl-Tx4BE#Pcd20!E|# zF*LTV7@T{5t_r;UM$;}xLo_`&1M{dQ7Qwa^<+!#7a+*T(3YT0GNnmRNCqK$abf&bR z3q{h&?RnxcGvM4ON-8Mfyto%Ct@k|32A??kQ3`y**?D{7sQemOQx4`DE&RBE2;$%} zaxn4#vk0O|qJ^C9DQQH1Vwudnxt(+LG9(icF5OGi5anPZG(BMCN3GW1oKGeMx}K!8 zo~OPJ9WpN@DvxBc9>7T?581F_A|mmE8HY;cT;$@{f7SxjZ_hg%hs=%7j(LX#8sI~< z>O1BgZU8Dx=g$8LNzuPA)TYq9!zba}^A4MI0blLkG4BwO4;B)dyJOzr8swTx6W%~(un`xfY_5e~wP>(XXV*sf{D6|kKDt010lnn{=1~LkgOs{|6{!l}D z$9jHy0I3i(sZ)2y08&mU5}%_Z9zbdgLAPtuB3qSP|Gt7|fXZwJN4E!%A|TE!-!Xtx z6Do=r;n*>N6zT!D)a$f0Vd>x3VB5m^zf&Mt&tG(3Uol4sbcsU^-oa+c}BQ@A6d>0@jR7B~h{~#ou692a# zBoJuGXgyPIg73vC$$4#KL1e7#7Y$V~hDv5OoDMNUqSg5meo+RuFTo1K{OX+JogSi0|A#B*w!2Ej1|;(X)blk$5= z#1fKX_JzPf9%_(u>#*ifz6fJ$M4TsAW#K(=pHT>k?B-?sZ^p3gag}w5BkJK9a~FyT za^@%CS!qt1?-h=r>EW+R*TAH`rIKcb*Ak%=D-kn?kU?-<=n>cm?+R6nD=hE;)#_Z8 zY?BszBsg%pyLXMl+?6Dl_Pd~_s#O-OntPWQ1j8}?k}3!q0WjQ(UJ(R~$?payL3uL# zA&4nlWtD#*bH6B{13>O@Wq2n$iu>}V^urLyJf zG4?lmx_29-*hqIgvYTWY;I3xpBC38vxMa}Ac%Jna8~?q1;{?EB!h<64D<)fC{jHpB z9j-9YA3giVxPP;T-$~cjUVEUM%(4Q3a80nqLBEwLXbDZSKocO#oVEiJ*y{hKBzYkL zI+1g@;dk^_0>%GMw7(%wwgNbaNNXmd+#Ixq^xJ?nlYTMk-;5X&3FK$`@%+)9QsAWs zex>!J_3O_EHl$pH4Gyf>ua%HmZH{h;_)yB^|Au|ml!C%emVT~A5{)UsE2 z8Ui}(L}#EB6)~r&;Ap-O1o)T7v%*JyepCi3xxM!|+cs6j5{iiv6VOD8V19mhe;ts< z<*GcMw|X1GtGqta0|TJ!wfV=Fu(OPD-s8%xRa~604Tym)?#N~RKeWYZ z8iB#bhcl~yvkvB)n5Ph6v^*8rmFl3d#n)tM`SUnJHxLa}Z@h6o`P*LR?8W zbz80#rr}r<=#qf=;Ms`f9N`y)u{IBsb$!IA1uxid;su69ASlQERgNqfs={JEuq(r=fJ zW$*9^{zbYOY@3u{8w-ht{37IDl}HlJ60(QVI6g9p+p_XgRo+hne3_KXue7ZyU1eQ} zGqb;iMvQfU$nsmy2g^c=K#>dhBopdH-S6dnfFRg+-6tAX3ItrQ$k|1ITa>{TcVrU( zhqnmRus>DNmi{um0u&@&!2^GoR{42IKS}iK`QHoZPgS_{Ie98Tft;o@W-um&vEQ$k zgU?AY0)}eWRS4e7yZzUT@=}B1Id4u?xUPY{6Sm*G4bO- z-7I!lPIb<97;rilXj&A986TG-lLy?E`xiEF z9%yil!<~uGTqObNTH#CEEo;S9yl}7EW^Dl*Br80f-Iit4C>zyepmOCI z)GzG>BZKhM;*F)=B*BI8W)>>TispCij*6_Ms0YmGelja;DKR!$lnl24SRmk72k)3%U5pr#8TBhT%+&k8jmQ zqlZ>MJt8;KXL9}i$z-6&37d=Fu%PszKrsTQ?|zZesqNtwcFExO(zuj3zBwD?zDDcw zmk~4gB?9BN9H*LPXn+nJlk#T>M)sH_WRHZXr;g5kJ}md%hspVTp;fq)HEZ%$%ME4nnP+Q+Ok%QS%OwJ@3cR$Z}d1dbIuAHC_?(P^uE z^huA;*Z!Zi0Jyq0m;GdCQ#my9F815ja#Tp2IHiW(+_*<9xJ_G=V!9m^;aEGEXTmBE zdeI(844&bk`-7YjAy4;&Rf9&WU@EiLS2x}EzD>kvgD_5GT_K-EpjMQ=%GP~M7;{V1 zWyySQ>Wxv9>L1!UZDF9suuK=U@IumQm|;yXd_TfO^R(7y`gRrT<8{Nj*Dp9rR)(e; zXA8O9u+msS7PgrlI&@aVBM#W`rQ*t$r^`Q-B zZD85AFfeC9{xSaaXQi$SDGOifl&H){o`6fCt#jQ&R5D>y}!g&I>oi*9Q+MY!WuI7_%Bgm;0tx8p~*4@+PPE(GxU=MSsYJH{VO^vf82!-(uURsZJl zInnvL%Q6!UxY7{J2X;$_%Z7L8p84SkbD`)5x&lBuNhB|GU zSH6rbLs0@Xmo(I<0Ekz*+oW(BRU6P)1Y|7i8NC+s=1NtT{ymKIWXf5OUSK~IPOKhN z2AZ<1xYx4KV0ZbrK34If-!Nb7()!|yK-WJRZ9dajID1gOEiF^i_sLCP1xhoNaYQw5 zPM?on$B$cVwOY=wDl25>WGH5lD}dQw-)LT0WbN$%xAVg8yy8}UETtXsN(Jz+2J#y( z18;@5m6eKE)ly`0rpFiRx=koV-_~9qqR;YTNj##PR@NLyXYFN*rS=QbHyehEo9+#ppt)%WI9)wqxU34B2eI*$3^0vQLeu789(F^ zC;Y~uWOWof9vj&3gc1|I{_PxgBvhL-fYB?(%44m!J1fq<0yku3BPX(!6}^#s>nR7p z{o4yEL$9>F)cnyfV7d>`RyC0CRd;!$x1Mh|mfBH%l&g|MWA&hFz5^$6)X!PpV(?JN ztmTo^t2$|Yvi8kuj^sn#anhmjmm36Pt7kL8y*2x0%nshn0a%)n`bMT2F^&M@0<%Lp zewx`ObHQm3uDs2Q0)`1OTS3o%OuXzWut7EM^o6fS9^Lhh<&}DN^sejKfaqtMt$GRR z6@K(ayWdm1EM6J%tI*LP`*`{*?O<9xdIce6_#FL}Lc73)gAw$24@0b{gdqRZmZ6y~C8||t4^unKM zQ}0cRx#1rSeu@%xL7Cc4rsT#K&J-jGD4nP{cEg+Hrt}uQ?4F|0vz5oOX&xJ9&cM7X zkk!2l=*}`qP}X+KS#gvG!wfl$M!oDLa9se)XnFY8{ zI2)zC*3%jGOxe;WBjjwIPqp5FG*>Z?Oq57|J1^>pj$f?Q0g>;eLq{`Mx>SuK6-6HM zh>rp5o)uYq`n>YYM1P@!&{TT)mZ4SZY>V2ZUJ|fQHc}aOQTJ*M`Nr)>>Z|=zx39Uy zd8M?VL&JiXGV<+1-hJjyYc79t*$6cO-R(nm`RQm5nx@91@~U2Zpx3azV#B^0rPMpT zcgG3KKz$hQ7qW*yj@93>ovEhI`pT^8J{$9QexQyTSTIzlmWx3ZKkyZbD=x5Hv7n(`fSW5u1!&3j6 zJ2vlSbF7q+lsa8N6g#z81ujr%B9Ts+NjSe^zj4N}lGwK+QeN&@Y3&*k+c-1R3#Cd7 zvc9`|feC-31zb>b##P3yIV!(l4oJ~Px#O|z9laV&>>m2yK5G4*4>gQ_ss*II*(R+m z-A2)x8ds&Iz}@HZw--iPHE>d%DTvUl*Jf3W4s`)~i~`m=rY7{+_X=&LLXZ#0Q7tTY z_jIu?FbD08tQrXJ-gd5JkFpt#6ao=h$6DEOTAWd zYty;fsLLu-0d$}8%I?19HWbOKpi_Szs_iZ)tcCdWa8CvI{O*JnYxTY(!sQh+78zN+ zK+{K!Hb!F}6le7I0#~91c(7qiZVS!qp39#OxL*{m;TbIf0Zj`vf1kW-JR%|AmfEzQ zBkDr`+tj#|?Yx~DK@dBfRj9Bu1V+|^PGfDDd9&-ARj(D=t;b_?BMTG!q?naPhmF_o zr0!HhE~HYcm_0-^?b{V)0v=8$gH8vR!3QjZSaf22z)s|R`!^u3Jvn+6cr((QhZUBC z)*@1O)IfiP!WPPKy<0G0kw-&7CT2iM+8||5#?b3VoEy%?r1_m>#hEJZ=!{mi6UXpf z`hfrCHu9TC%=dlYJdHI>tR4~w$>JW;oLv=UmH49SNPqU-Pd0%>tVvXB0&3D0m zCf-IO0S^Fb&AzU&6}Dyx9e*-c?vfiCmXpS`l!nDO7V?p>FQqw6E;$pP z)*iPi;+6^H`Ow~dK;WxqHi(=ImNnfL+p#aCm`oPm!^2W(z6)*jOJEmjkk`VY85jYg3Su*}yN>y3bN(14S6@4^k{p3|2*HU&uN=lMm zv+Y}A^VK}I4&`W#%Q>eq1iJd=`;|L3NQjrOiIB)K34&VJp6K*ZNHqt0v30i9RmTNaKGz( zeMR(qRbVBTw1Yw|F1@owZ#c1Ia|5$!VH;u8Cyp3zT3Af;5Cw)-5Q_P`53cq|UkkGF zI58iF!&&6^hs*#yG_(*`I8@tbTyt*+{pg?h1#D>;a|l$-N|IC%@V`Q>XXA zyIYY7-2xI2bdvhoFN#Cfvgb5N7^BYTCxnrmZsT!Tn&QzZ^_=QzE^%9n2QhI1#ZxdZ zJ}MXEF1)7DcU(7~H-7*`q-_achM4d>fD*;uqWG9^qt}qs5KpqMxq;-|$-_a386B3iM-~jKkkAK6F0Ua*5k4tI z^H0F!oDcaqKrZco*oscUQjeJ*)1$rAx6ehp+D;4<3$uWw{u$k>MRXrU-U)25COjN| zvxGqf;=4ZAIA!lr5bOq7<_$g>QafFWY`JwVj!D${hzGw#Pd1N1&8w-g6jTo@@HXDF zc4%4B)FRo|qLy^1=^e!@U|Obgh%ohF=$=qf{xKNwxAa1RyiRwffyvTT_XnfK zsHEvsKC_Nw1OT<-D10jHyL5fxICA?ksg!%y+n~*mWuJ%d0^*gA5IXM5r$SVm33#5c zM{hX`7=Dh$nSNmo)yzMEA~iG#)kI#PBh+1Pd=LC`{>6!QwzdA2IO*0;6b7xj2YZ}7Y(7H1Z(?#V2jIAX$P%Mn`0D~Q&(_=$9Owi7-fPkM=z?8TtkahF* zS(h)oX>>h%Tp!qTY>PCP42WjMea(D_ukm;3JTh;v1Rc&E?H4-lg0ilU78mb#Uz^4P zzv&Kyle;rcVt}r0WpbH%0iuEuHT|)=H$enoff5p;oLMX8Z7T3Lo*jK5x?C;io;hW= z>eZ9gaqP0;`-@J5)iJ-_#gNaJxRmmbSa;1 z*<*HNx=zUaaTbCAB^vneZY>a|cOtbdhGYKKKxw|a|1{J$dHCY7$BsriFac`AH&+#<&x#%Z0o_Yv!^qT zcZZ_aN2VMc`fQ|7kJJZQA2;5v;*e6KNQdHZEP%l@ES^y`j6AX!sr*9TrN^HSfqXLk z!T@K>>>mwF%0N5^L7gEd2tv7p31O*=Gle)=evGSKc!x89BI1F0F1IM?l-ap*GNEh0 zNM1hTzXQiV+4Qm0mzJ$%5530o)7O>qmO!AN4P5(pY-@s%a7?gP`4j5ehZZzUYk(nM zYPqkD3~_k(clR99S^FX-_A)?V=;@SW6KH87pk>$c{ub7YoXI#dwx=sYzx8am`AbG8 z2B}AcUi5}K9lP*ypIs}6w(o1ydhRC8jFr|`&IbV`XHtT-Ka;4C8vxO6Q_-;-XDVp4 z9TCn>0a(GCY5*oik2XYx@BiWK@lNKSHS@P0yY`6NxUR5OR@mT5+3U+56?i$(#kMI& zrJZS#U0%0=N>;quePSnF-u$iR-cw zQ@sQK_0fTN2sd3(NnU(>SoS7J3Ary73fx;X5?@md!j~-Nq{Yeb_qQE~_ru@XXv}FK zgtWSR`(V~@D~Uz%c?OIOvAMy$PAlv#d35TXALnhZqVHFqWY_ccz6S!r2PuvK860(2xA|D4gBEphK4aHVCB^$l`wmqQ7(noO4?rEmpDD~x zGzv8EJhZiu%$s@Z!^A44AmGe{G-2zf=CJfdSKa*p&en`h%dED&BS*_)V-Z`jHp8|* zfAGoic#twdt=K&~d?gQH8YwG%?f zWIVbXX$FJ{E5lj4dds}2vM`c5)2W3stb2R(=ySS6r%}C@Y^L7RKheg-8&h zF~;U}R63u|qpNxgDF#S?){7mIimXhvqIn1aelySM_;AbH9)K>s*JuOo)d#u(ZDd%L zK$l8!kO_FtDZwMSoxDaSBnj55@ME4g?DtJdUS_HJ@zwhcKoU;D8RR1~^~HwA>oy6v z&4z#@{^`mZ8D?FX2DL!j7r)v#;gt?Sk|rQR>23ro7f@ONiJvPdGj(jc;%?NB)Z&BM z{7u4|!qhV*frr@$fBmWe8Guk|_*Fec@t4x+SJAv%odh0iJ;@_)HvFtNm+RY2L&$X%soyEZLbz zr0|p~9c3xAj@t^XoB|+V<``c;qV=p-HpQgq%GXETskYvcW0a)Vi_b+l>M`(|-p|rU z@s=t%KQ=yL55j66x+>6tW3_@_o1I{pR=iXbaM}Jh1yYHRfiHdLRsBvKCx7G%d4pbP zHiaE+>ebQ-aEbmF1XZHu)pQdwDUcQ7p-<9lrc3FnUZKu`rhvzc-UE|&q|@&EC-MY2 z*P+8Q1(4r^Sn4*S_vj2MRh6FhSQ+$bweQpnZD&w^Mqib9V>~6F?L<#z2Sqg$-c;xg z8Mi>W0LUL$)BGfec?4Nxi{izO0Tp7S2wZ6x44vqIY5Er+}4)r-I=T6Z@f;n zmh|MPG{u_Ms_9QD$cg404e*-35P%J;d@7qG zy!EB$gT_eBXV?CQE()j$Yl4MSv--*{s9)sRr`(l3;e-diV0K;y0GKYiZA@x&xh$9_ zNfr>*?`~=De_2=PwM6y#MTE`uaVRbN$oxk-gOi}a`3==5w)UXPaTH81>}`5`t#uh> zNi(k`(`z}N(zO1xr=lts0BH!Ic=(Rta2}g31po(b1-@$dy8G8sFRUE`M6ud*isptJ7es$Vx}x@2dLF*6#WC53uoJ_E&o%Fzit}gv||2bt92O^cYA}y=9gb2jV-m7 zs$lW;ym4UYjJ{qS_h|BUvOiDI@{Ah46I6}Dvp_CU)=67BoPOmuLJLg^{0uR?hTPnK+wkJ5m!SYZKHc z)8Ex%!@|PSoo000gRgV7&VH;RGQ(q&aP;``+R-x;0lo7>j8=>mwyWUC7~cNl9J zFz~m2pad6rmA)S@o!c4d{QWoo{Gb0b?>}-+2c(O;A?Bej_N(CZjbHZOz1-H|XDz^%LD~l3^4VZT zDH2|LTrX;U9a0y9h>Ixp97A-5ej$TV?j%%OCdmGgsJ3jGaz&@e#Z2FD%l4Jp^@whL z=9eYhzK?FF3@ga;BB270l~&|#RfCQ@gZ-v*z1Rm|)PAsETb29L@cquvg8eRcRMu_n z*K;h}1&(S_Y+pwoO@#+td+5MpUVc_v^?FTklv_xMAvn4f zx1UMmD3F5I(L4Z^jZaR7+sr^BbMM3bdBV#x(t6g`*0Pf_cCJ$yRhec7{`lkbT`5M| zB;~Y*hDf0$bGtXKdl;{KmeTAIN$5ZMi{21fVz!z&@H%%Kw$;DOd>XnNp6rr0UpMeT%=t5bjuQxMq+@(7zy*qoMFhxCAHe8=FO%!`e&KC;K zGC=GcwybcF)Bi=8wSqQ?m;;PNhL6ooq6w4SJUl$p8};G*Rp-Q4s~v$`G7+`*GPLMV z!fNzp{%A3jM}$fQP(lSqz25C}l%-D@0+k5=BxgU=0ft)fA{-$({|s8Wf+^K>dMPNm zYF$Y} za?F&c28s&{UV;wW+Wz##ec>c)>o7 z9B+CH(sXP?pbRoB=-sZ~h}S2s$D9Ex;n3wFy7cur6kJp)$~$of#>`}4hk??ub8k3% zQQ1q0yz8vP{X&3a3`1GwyT0bt} z?c29E)q~?XE$y84rPoVVbCapDd=>0xkcx$oVwcw{phdk@Z3dAdxV4-48UMa<1c}`d zV|z~y&80`o>mW5=CEkGgQ${_ANx=FfSVwbnb3xW^UDT2L$C;QK2IR)#x$h-fm{y@! zIHWx-0wf)ddeKoh%I5BBJ_vumP88sbc``czxKTE~QA$2sKQ0(r+P6oIQAe7w3q+I! zAUu=S0FVHrZ0?hqo2WMfd3Aabpqg!bS7h!B%WOF_;o_R0y}Ap)IT)lE?O*Nc?W-NT zNy2#ed&JDqVk$V^>jlgMCD3N@b`(pHVc8f>Rk`EkO_3fqE#Ar(qd;m7;L|4>EoJzf^8b0hY-WGUvy!cfO$dIMw! zY!`glQlDlu2${)_q7FtL4thk9eZADK;LNkVu;jsRl6i3HCY(O>5o|G@5_TkFN39wf zyU9jbn?R58iTEW*(gAL{rOwtrF|S|gut+&{6R46lFAE)=7(?gWK3i#5fTQ@`_T;d* z;0z*{Dp1x78I@EQ+2TdOVmT85EKNmV_w!Hw;YMuP^m#D-JUs37Fnk%1LMzaUq1}xF zF>VC_q3NPQ#pY@6u6xTl{OHws8&GLsIGIM)^~A?#ILs{dL-*AMkb@2k1hq2Ue0+3V z?z4WwXSkehA!Xx&)bZ}^9~Ebx@E4te%4|Lw3Ar!N^sAPp%pC$9S$pf@F3Fa#K^JVz zNE$!D858)D% zZ7A6bT|fYvQp~X?=-sH4tkT^4A)eF9L>3z?l$N5Af2sE)Vh&{8W6#*y?A+uY&}T7giwse6Yr4#Z}|-B(pfBeo!S+ZWhOs%5Z-j4^?DkhVr&m~ zkSFL>;wk0?MK=N<#4j}P9o>mtNC2e_LXsUy89E96ed|+3;(iBQzmnP~yb_8ip7pnS z233NF$7@^;g%Yz==`!w`KeQ5JIzr|*cf=&18{Phif^g%#M&?7toVal1Yj} zKbY&WMR&Nd_{p%~`YW;X4E>?RBN8j8TJ_A!7@)>@1s=BkeE&Zdc?7y~*tMAZn;VB` z0rQ8I+Ohup%C7}@Jw2#nclC==Ru`Y{_n2Z@qo)U9Va7xx42h; z%zZ>RbGInQ09|Z6*kJOHjr?3q4LD*;qMLSGM5Tt(iWFl!JJ@_Ti2=BToBNwfxL$j~ z0vI0KG5m%EU=(4v`tLA{M_>WeWrFarh+CAGPz79&vb7la!egNRD0>GZeskwC2K3V! zRoZ3?*h5V*Km$ComW8=;+v}7EVr$3tlOWrN?!`xG-j~N+*b^@k-s7kk`Xd$_J^fWs zl>ST=(I0mJTIF;QUpD;u6c{Rptu9o%yg>LCF*DHf8eV_?O!Xb z@5Mp)?w#$z*;i1cLv*bFMWnNJ^)Hnkd=h`?%}LmCd;>hk0lZiE#hh_1=#y3!b=zvd zQMQoya@Ka&x*1;>RpoWSWAu3|+wIddxqyqdc;?a-7|(op3n;(e&-51TN#I+H9mV}k zkna)514KmKZd%tz3p2YhrQk6x>sgLSNK6W}fZs4F{oo<&)z6{JjVI{Zi7q#~fLMQ^ zIEVZ4*6ILUz(m9*|NlV*3^b0l>H{w$mw8<1V)t5beN}?)Ztw- z5Pl#GEQ~uedL@PzNK1+h52cqS4LID8+ppmzaz~(wmz(VQEt@qU_aOl7=K?*Q&A=WoJb8-55L5Bq?j%MRrO^ zjNRC`Y-Qi~A^S3h#x@u;e%H8{_dV|3kH_cp{pvdh{I@ejB=bSDV2n{3n zJ^E)|h5{%Zj_cg{5!Jz80@RyQ-F2$AlsFM*wn2abClJsBPv?*A8#oX6^tR1km-?^w z|4OV`Bm&Y6zQn2T8=!dT`=7k$Yr74iHsZ!5y9U1gN?%cmF@su|q#R1*(_x7JXkoN%4J-AQk7PTh)uTq?fK7Sqc%u8@1 z75fbS7i|%Sc9kAezy65!1fS%hWYj)1R>*zWtJ>sb6@w8F0 zp~P1Ux0V|9E{E@G3bOOv3BdLcweCDNDz@L@=6(r|U(zTHu((*sVIJ}Aa> z1Sy+>Co+FXXR7ra`4$MsV|c-m``^Ta9Czz289ts0(-kyKeLMI_%b+{pL(ne_6+SI+ zQHw65)c$+pdH49>kBP*JZDL;q0-S_e2QgsyBS=P5S%4!6y4gS3v z6~?@ejL{3$D(|cI{QI|(K&>@8Izs-fSra6YW+hp(&rYx%eSJ^wq{;F~YD zsDE#P>EM+PKNUj$i_fvtu}R8T?_UCp_U!nO)4#VcC46Z_$fh{6d8F^;zn@wQq>!K) z2DQ6GEihE$XqIQeLI-3Vxp&Hqss8xBHhAIJ@Zqn&c;T5#y*n>d?A{fLppf(yz#a*1 zavPDT9=CHFVQdjf?BbZ-sis-#3jIH%*s02?bGzgjv)loJFnxtRsZjEKN4}I4cQ#~mvH&>?+4Cj0sQH_ zRkXOR)%YWWfIUb}#x5*{3DUxX6@mEz1o-YiEu|OoZl(OU_^za3P^K`^0F5zc6cDU3 zDLZ2MpQ1jF2CaWo=*Us3J0ZX*wz9IQ86Ig@j;A@Zt9Lt%|(oh*xH%n)_c$2s)S zB_wW&`2Q{=7cQ$e+}0cg5HJ4JXG|%e}G~Ser%F0`o_9BfOu}ye{;s> zw41o2zT|W9df4IM5W=TMo40aS6IJuC>)B-=qi=XFk!pPf+FT|Q^wxaXo<$!h>)tn( z^Hl#VtLu7UgQPEwldL;*!o33oLNhY*skmLC0GKOqYtyDcDvQBM0H{s<@ci+>fqcXj z7aZ>^1LEzEo8Ft4@;7}&;3tc`kzM%oAv)B#r{Zj9Oj1MpXeu^0Pm_c<)wMFqXOy71 z`=Klq(QcVrF!+TqFa_eYbh5{{t_7pFp(q|5^QJ3pqcDE%{)ttkH~A)kE2h7jui>7I zRM)G>yI66b3zf8tu`~%EiLPJGl??AV$kKcMQH9##!(qKXPvdrlxqTA(j6d0-3FD4f*G~?5Y)|#1w3)u5E^L?D zSY&+{l$H&pgr(EJi0wKJ;IhI!{hFx97C4=yoJJU|uJ#i|91FjtglU_D-)$^T!SM#? z1)JZ=K) zRq{&6X&J$^Lg>aVpsjQZ3I_HJ{TxGXK$}j=QewoSNtYi`(dpY8pkP7*ECr}owT(n^ zK*${2mM`cP>&Dv+=uNCQpp(Rg-ugAJhT>1bLBSH}Hp>sWsi*Qz`q<4|wVZyV_-R06IN&7&&)&aeAI#YmC;lQm=xMx0+N_L0AE6;CBfJG3cWtyw9zH?)5n`sD8AX_O+0auUup+K_ewRW;Y90Uw$xz}8nrj|MW%UHU7Wj&M=U=U7vqg!W^XOYuTMO&{Ky0F-~tL zBgy!&+xcE}PQE!Bl|n%^dfZQVhrSqVnbxsF%s$D!`aChbiwTX=HrDHiY?Ddx{ApkF zj!NApOamV9D1W}IhkDs>fb7-X9yYBXs;}icndg`AVmN0BqGg+#^ojOHr_+1l%hf*Q zBasIS9k82&G>jCOYC4yy!o({d-GDQ<5KY~_h83jAbBZ9=wGH! z+d*JSseUe@9@oyQnedV~NWOGj0gi3;Y`B*1iCzdaAS%v{&|8+Ye&3sa@ss#b&1DJ$ zA&VHFfV7QO^f8CPljySsk}_A@vr#!rmhMU&t7-3>NFQZ=o*a$ulr_BE$$#RZKn4{S z;7>k7asjVuGU4T}=742qUK5z>GzfW%_WdUdMoL5%N)cM05(daOL~ZPQdnHl>!GJGa zcfq#d)X$N?97-VUJNL|X5@Et(FsM#IM?SNy4x~4(7Eu~cH58lT^O(}c=2^5%p}i=s2D-8O#UA?I6j?Jtx*YOTG^4X2#h%%xuF0CPMMntmiaMvSSqX%o5_6`rw zxReB42@G-yH1b4lFg~e5cV*i;|5jIjj;w;~S*uC(ACGyfrA2F7R*bXAluwz48kkeP zdk?aOqAbeyo9!|ii#30caYV4?#i6n@JpJwoE*Tm$)&hl7JC}8X{mv=$8M=WGgZ$s( z+?VF8-lNBR4^8vd?wVz~2l)c{`W(hE>JCIlzNJ)#qljZ`o~~ve@)w75&!88iv~$~D z;S|_!Z5tIe4pbV+ok|a&SQArcFU6sGR51NHLbm?Na0|T>r}J@+9dlPbtG~Dnyh@`R=z7YwFkKcOp?h0^q)U0%Y<#cH0o7!*dI$bK8p?-+HX3;kBiEsoeBxN6s~tpoMa5aS zu^0sr$_uWgsBr~XhM-WM0Q{R*EjCA!YWo z!AnL`S*8b^<0}n9=YylmeV|_WHN`J)IZxihn#$_G$v>0jh(j_Yz%~b28!1cw>Kw-Q0>pC&JNriaM=Si z9kJ9EWJ3q_;ho(0{CF9*_FneD7h1Qse~kV8*9y0cyjJc9&5s;^aG@5~s?V}myq@Yn z{op-F9uPypj*dm{;KTkS!3r0%Nsiy4UV^^brSqR>I8QwPqoc8t@O=)>;2WO2tvwrf ziCA9#^ph>5Cpv^(zoQSS==po>Sl7sp2R@;0 zc7}h<^dY}+YhG5n(2#@LH5qL}6+F=OY(5|L1Anii>m7=MxVCpZLLY{xkN)AoogTdB zJkp{(me#9(XQxZ_KlVKH09+0pNQ@=K&VOY z7K%Di$Z+=l!L0u4dG}2Vc96|4UpBCPJa~g%ykJ+E)Qkam?Jz1fa@T9E=+QT`-TEK2 zOI*6+xAVRU!-Om19R6!^h6X8n^cIxtsK+q9HUSUfV(;zv$Da5U__k?MY4yP2-x>UT z3kto+VHio8|HJrOy71bTyBMK$GxeeOeuk{c{DB^7tMC@cvI|}hL+ygv2)?GVxgpqV z#yb%j%GJ~(dA0O(NBBp>Ke*PO!nHZ58=Yy0_WNu@a)s7ukekbz7FephH1axtMjih$ z#6}GSi)HI8bTkq71Eo&+PcDS*(c`dLsT@8bohQucYJ_)K%gjV1ob46)1BWy7Nx{%GPPOKL2(c0XWP0Yq1{o@K!N*R6A{%)*J%#K| zVqQ~Zo=?>^y@vjf1V9||jPijyXKsA(-Mw)U7YC?VVYsGz;S-%h7Y%j&09|@yA5&|H z*ObxFTism&|9LG%xCYX(NmYi28*=alXZsS**yd`wCmk6y+RUoR%nXDp{}G^kM|Usj zglwKLW9FlH)4=eW%KLLdQrgwORZeA4(QPkH8Ko)t;Hs!66}!)fAYH&@h2N60+vlsd z@o#)5p{uo(A3w~fZSQ9%M8wQeB3BAW_thyfo3^5|=ldTugvZ3`2|UM0+~?F!a|4Gl z+5)iL{}klkaskca(yVR&`9r=oI1TPl)+07~Iq~1&S$97?ZAi*_k~lt{;}*c zH^JD$xsG-1{4|vTv%IByd(r8lgFo)CtTH?Ed_P1XH6MYdU)0k1W2Y`d)}#2lq)Bsi z2JKdSE$IMk~&81B!IcuE>U++U7+tOn|2<@kwrWT$09f;>zQ1DGl=O+WX{DZ?%r0AqUP<6nVwK`|;c$kV<3P#r{ zx%^#b-oW`b2!O&VWw=yE9y|Yy97>Bsiw|>C(i+IE16%@G;u{;6FI_#VZ#{LkvL`4G zM#Z#G6)66T6qHt!N|68xI-5MTy}Vr$Zg=Lv7w7LMdDBp;Q0NNt#Sn5;S1bf>$$9bq z>cyQvmf5k-`+FK%Uw_BQc1l`Gx4mkK3p^Kh3O2;!EHp-A1AE|f=pO zn}-%3TtjKQOgK2?$%mHpb@sB8!tU!aVyP#xKX8c>S}v3%;$$}`5uQu@BR_TGJGFJQdX&Eq@VNcUD( zO&_DHN72=o!Q#TaWB@EcX0UuC6QxrVbs18*1+-(}O-;p5RA;05C##GBIJ0C`nR@v6 zfhHw|r?!UrhpEClduf^vvWirSzwN8hgik`|E(~@jCDbZcp_jtpEL8ymPxWp>VSbL{ zVBH>?2@lEkv)T`7tira@EqR~vFy_tZmQ+r&O?zv0P`9Ec3F0=1le>z9+PneRV6<37 zsf7s_aIcy_6ee}#9l4zpIH+u{Rlut<8pzr-qjai(wJl;@Zgbzz6yM?zkVJow@GwAK zz|Xs$pBz?qIwx~>ni57Z_ja?Ma#BEXk(&n*EOdRv_&PCuD!$>nzSY*$y{H9RJU>vkFemU;zymLE;x zO2?^2qDdF;xM_hPk6z7QYB`dS2JU=uvCKK@?*A0iLoXsgv-n6iPW2tlDcFIhpAzbC zenSoALx>k9(3g&jUO;X!HgTy6x@tuCBBC1%&SK$_^Sn*+Qx^q66RR!A9x7XAp9f`7 zU_GP7Ce{1f^Z=c?zi6T9pzbAQR{n_sdP~h`C)m6vpAH_<3p4vrkY|tzQIdXW`TlI% ze&ou?TlFH?fU=%vh+@rrZ0EUWxf*Dhj2Gs%NSR(^hOFjC4USaLS4f}~?-b_>nNb@c zk|3py9sAnDRHVXGmUO>?%TVWhlJIG`Lp0tRfi8p2EoqXZ{6BDm_5j#@IYO3`6{Kot zJwJ_=z=|#b%k|7spw8ntE+7{o--sZh^i&)bkNqM=zXUSp;h|ExbW|6J_2G|a)^@=k z!mJsKHxFMd;c!x_3eR?;Pfi6qWEZJdX*$4=ynkS76Gnet|95~n>tg<>;%pehb6vkT#-mg-uC<#|V~*2G9M?WZ!f3=?}@d5|yYI!5j`M1=Xq z&)S=hv9nxjn;)!3Nc(SRSZ&8~lzHJszI9bZrpjwcU0_HSn=89NeJ~61F5PjLcJZVD z1;*$9a6bn8JtCB!yz7W=m5S#bgSq`CEivx?GLud8Wkq zkr6z-IN?C@E=~Dsm}f#K;KtYt1y_AUa>m2_LH^_-)q{Kxh1WALPbkgd?YkX=@r0T?*bljJwsZONUg)DyJG?aufu>aaoCocfHr+P-zOxh$<*I@=rE zTMWV0cnCoLS|9!Nn97cPs^AZ3B(-lbXV=Z4^b|ndzjAwU-&7vyNmTPhi?OJ)V02G+ zx|iWbSDkc%7>rbn8Q_?k3m1wN3$re##pq|mb8BEW^M^1(BpEod4~^jyx|tvX_E-J*RAlt$QG!Y*#OULRD+;) zJU$K}Gp(DNb#{?akHUczl+oWTzw?2E0yN&!ypuh7EeQ6kAJ%D1bubA}!2!@+ET*1U zrr-mfx1^yYNLe4U4*Z7$24kb5#E}^`@I1TJ=k9-V&&~(re#*BMU^`)r2{1qaqBWU! zf=c2(!l`}Yc0&;;SZA^dL??5Pp3?Mj3bE{r4R=kQqe&R~6nUQ@iyCxUK#)PGF)Kh~0*nVmrUT{&Y2OECfxAjBDS6Mr!fZlmNUjM5QGZE~G0&%1$pI<{rC&1KSNPTTSHJp1!4!mL^?bXShmD+px9(?tJxt=8{4gNmP1Gfj}t-gsL>{_)Z=aEPVmi ze2t2Wfre}TS1s-T>lIuQcBm37sB3G;-++ETPPzJfkSF))o;32M2CnY=FV(aAbU}-; zp3C=WcL)>;F3PIKsPeAJ+OJ^u36yc!9))hnJ1{Z`(7x6O6L&!u73u+NNZnriO1=LC zN&~JbreLtv{}$`|71Qll%+5OPzGQ9C1(Hgcqz(mL_FAc`>5dH#q2Hi{qMML zhBA4SM<_0p!k-FCU@y8~Er35sxtxCq$71CZpJ!tKS1!eAr`;9Y{Zz}xH*^ZgQ zpbmKD9qhL*1ru}Zk0HiVd>nHXWBI zGfaktgqG`^xLvQ5P59wvGbnn@RW46n&U?@WQ`=zH3w3gekxZfm8KJW6DgQB8fxO{7 zXVculi?2cHC`}aybpzuAc@zY@N}JHG3?UncBHEsM7$`lJtE$7(OCEUDF0c2@mf-<| zh3&8hA9=G!ajb-}fIIQyh1iX3m)ehWs} z50ma(G)5qjbYEJ2292`!ugpjgZaYpr+h}=9T$5v zpS)*qwLpd|eq-3h6_l+o0;z-E^Z5mpBHK%2OQ|jTQ`!CF9%@q7pqtk>-pp#`X;lBA zaO;MA=9rDPC~L(=lNkOPKPu=$6}i|VaXiVbvwkroUe@Es%g?PtSr_BrO99#y9uF?^ za=a{e5?}anM?l1SY zOTK(F*C2bb%?*R^C^mGMYPbXK)b^!id3%ymu$ET(_L#LRF2LSy7*UsWuo-%lhO*O#}i%j>PxRd{XV zB}2y*XqJI0hz<=m*>%&X6yx9L=5&z)qP2ljK(ya4&`|{J>zvA&pses}B6?XBw!fl5J~#o-BE1Y8R}p;nqai7dW` zzM{&|3E}+0`zcy#xNMNQ&RWh%t2q;~$lhBIQ!_ZKD9jnNYFN04zkx4+djCcqpDw}j z=m}Sbe>Jk-TF2m1DtwH&3Y;EbmU-N^?wi54$NkM>ZU|0C3N5{03=L+F&-duz`1BfM zpvPP{Z8s9<_j9g{Tpt*JMA)n2%L%FLuH~#xN=Bmx$3#cEU)l9=Ht$_NW?}ji)!jPl z4ymc4(jW>6U?!vu9i-Hh$_!F4-=|BeD%_)IJ{QrO)*?}<>A3zaO)YRdR97Z=vhthT z)*4f^^>rPA6_?I1^Cn3L&66u_76KWhEfPMs+27ZMw{T_R>UiI%d)OJe+H?N+Il2}z z&P#rCa;vfRW105ePX<1~rL%6`jM{$l)xa{L>*NArt6COb(v$BIhj7Gy*Gb9SDCHRz zJC?Z_9p4?3<1uTbPiS~M_9`u<7m=J@QD&;9znVH*RkzVpo4xNGxD7?Tf1m5lhySAZ zJSh64o*{$A&m4$j;DqpYW7W;j1E0e#@ogWfkU^j4Y zWo+B^v_iMV5j(4=xE%A$;Kuk|mtol5L6h>y#l=^=vUwF#L${xqy`}FfcfCR$^he0q zMt5qqEFYiFcqJI`I-lp!{NiV({Lq{3+@*VBcSdWAoHL@OT;_~io6Q%aala4qU*ZSa zMN&j^SJ>Mb1$0qwcwG(E=J-Ovp}D79`Sf&IJcS$_a-N(8LU{!zvW)|R(g5m~u-}sO zr4~P`70=;I%70VvCR@EHb^gvEa(D%0i!a#5Ta52TbNi1iF?ZYf>)J1jC%q*3*X3eY zR8*$Iv|xi}p5p)u5FV8;#%ZMFo3vnuc$8A2mp0}8NirFCo%m6hO|73{<%flU&vadL z_GGqi_p0GCP*mGqn+?T?J(e-fs!P$j)6UmZS@9+Dg`T>5F3BTQG&^<%CR&y33R75= zflXq;JlMbNnm?4G84!Iim~*az=w5Ur`cvs@N=Xh%KBat^#K}2!l3yrL(IvP>VsV&d z+jDtgy$9o&Qyo|ISZAvhDk-PQ#H=iBzvRXqQ+~Mzbs}h4)sSb}E@HSH=g+)AktbS* zgy%9ta@_}!>r+#&un!Hx7R&$?@D@Xo_jm^8%`-~74()C1asJv_Y`AiCBQ5-^gCc`} z5`KHOWYA&KHqfog&RR-72%q-5jtv8|J{tA&Qx!}w ztQ}y9g^bYtUHYdVC_XcmB}$n8A6#KUsRIPC%Wfy68I6r7U4a<@iF}Xu>{wb~lg)pe z)9}%7sjD0G!iv9yBKEtY=YtD;zM!%FoKQFY(yduVwdKMcEvr#n(fmC8s{@qOpHXbz z%GwiEnq)ay6Un9ShJ*7Ca?QpxVCUZR@CsjrdC=#wo>O|9%`1~-Ki~zVcx3UU%HcSb z@}Gh6Ia1}GBg0We738lb;}Y>jW&>Lb7@tdq%Wq1oeB!U$00MZVYpZkK@p7BKQ#xnQ zkAA*-x-AIoLDZv#3Td*JCKlKUSTCj~YW83NunElsW5N&hXrQlI>!Q?N+Ehn<(3WXq z7xbqed!Fb&<{F34SME2xG6$FS>|_iLgr>0=Nvpfr2~FNL=mnan>9F^pQv8WVe=Az; zRdja@m`>1oif){|AeLfk&~UEe-cd&qgt^|lyue$iv#Gec**q@S_S9y6{9=gwc7ea~ z$Ort%Oaw+yEg&~8%Xxqp1~E)2$Cn4H25hg?42)wpnk}mipS}blw#|@TVUJ&1$!#X! zV#{h~JyR)&+%xDUos|SRD=g)zB7<8`%64+Q)S>#0R-@AjzJ;~T><42MIKc*EN*?Q^ z>lK}6Y;QIdqQ6}?USQwHMUeOTEY<08Sm1TIZL2%6W-aC|^e>qlHIg(wvRq3)9w~3&R4zcHTU$t~x$$7Qllv}poWq|-%0V2lTKS#G{#%VrBT-}k8?x0eQssJ%h@~ z&t0{G@SmGMdwEb%O{~t*g1UmVHllF5Xso{OQ6ta#Mzxpb@j1o=XFJ8??1kyK*^J{X zi5*xWDS(9MY68MCK2A#L@dnmPh_{KzWpSQXN!!Uh_X5z#w=+C3?8nIU z=3E+L)O1E5RLUCkL6Y;7Krh<#bJ~--!sok~4PEZV6uaEjK%Dx%?rN8gd6u8LH&*|b z6R9^_UqZrbe|A@m{npqb zcfoLNC2{U@{jfjlodFPW@Cv&@v0gu1c4^!wZKnnbLBT)9v0!umL(JhM24S>MAc#&P~< z-XsajEj=a+{lXNKq|hC0SSYu-X~i6`_7pdQ$PpaI3ln-Ly&8oxLf^~bK3gtQ{4_&C zcC%Q8T2(%<-ytvdTNgCNSU-g@xzzg-$C~2{Q_O}|(?D-TsJGQ2Rm;2Y!E{@`zU$4f zWY76$omzFr+0z60GvdngXC_w52|)8v%v;+CJw&l!2Fc5NSryZp5sd(Q*y`=0Mg;SX zfFs?=$n2)lRGo$pP7Om2`weF z^RQQdvr572mG^RhZVkUn=>N;f>I{hmElVB-xNSpN47x950@B_Cq+PMSs-ajsLaf2y za}}9ut~ZI=az^v# z9Gt?nB^B#NTZ5r4E$h9jV=3t&xp9}c7KsJ^TXe_%H?bk)X$syid#)^ZDw4*=l1`xX zN*k|;I8Wy@l~v94)I}LK1RQ%t%-#T)zNc-Oq*n8Zm7sZfg)D2TXRok49L^S!*1Pv& ztd?-)+Lu4fQt{;5v+Df0izlp6A6oI^rEquU2pJG|Ge0rlJRm}Q+32ejGM5m$oxbvGr3p!TDIBL zMpdKgD~(>ESH6kk{rMDu>qQtY!=p+FPkOy@2t)SFtF9`ClxGh~xR(h`xQri9sSh|y z+c_86vk_e4Qa`dCUw@j8rd8I`JqGP^S7!2HHU881i}4v#UlA$8fL=tp_>Vqmgn-!q zB|(AFJi1JkeFHs1+*d0k1!fF)Vnp9EbuBK0l^h}9essjunoY}dW z?qoDKEGBkE4F1|{8iTXh?!}TLEU;K5v&`yoz3C}D?SVe zQnMEC_pi{|{3FXTsVu`b%Szm%!}kyOB$=2q^frsDdTawEVA+XzCLwVbg$3KPZyLW4=T?5%wF`vLBfcB z37u8cAW>LOGA_r*O-Ns?9-vmDn+_^c(iEOl;&;z~NocG@0Hq#!Jl&|%_FSb)Zx1d0 zJ&0MzyBUof?e-zu!j7Ao^NOX}FkB^dx)#*mn&#H%JZPH?V3o@%!uq`4xz3N%oQ;%N z3W0Nvmq$##%60Kp8b#%uoy*)xNjz;EtK*)uXK-;yyM)QC%t3n~S=5gZ&crQOz*AW* ztvCkfEfHMf=E!s}>0VB^IVEL%^c?pk4XrZgZ+eK0SNK3RL$Nf{1h)UWv)pD#tO|%> ziJw}ea%4eg5K5wEVKZW9F2~<{o&atj#GJ0V-)V}&i1Vai?Xa&&wr#7$&_)}%a8_^7 z7^r7S>(a3yF-QDHeGP?&Tkby2Zq{S2$&as)A2eJ_7E}w(7d-5GH@Y}@a$>LH*1K+e z@iZ#1TZtaqa@O|2CdzZIhSz(le2z#7yZ9^uVjWCl@0I08VG~FCIJQ0NL)-}VHZRR% z3e!D?7HCcI#i6ztIz)J;j6jRW*N!=nB=s6zFDIo>EmpHzZ$nRhDVee#0S)JVc6JsR zptyz_+{rz{>A~-1;Bc;7ulA!i%)blLvB<9l+X5fFBvgB(vlsGegjl=kIF((k$OUsw zf7iXL$HQP3J*%$y>W)r*Wx)bUCJBW9l!U$Cic14P;ZX2DU zAx!m$YSEZ8j!gl#9Jkke@qW@S%2)+)XdT5okZ~tD%n!+j!IyuJ+@dDp0({9n|W*jepVO)9!Aa69Tie6__Y6w%|iAayDE2 z1On;GmE^YvxInzKne;!hz3)3988eE+UT9`O3ZG5xMBU%efn0`QPGbHpo>HYKwtK_y zT~dgGG0}Z$abPFVZwangXy!%k@^z|7OfJ~C9AV*bSy zNS?9&l=PRaUarI->v?o7B>C_pNXmsn%4A05Cjy){lb_XoidXB}N^z^ZuEmG1ir@G**tIAMfU7;A`$3 z-&9f!0S0KCi!K94GT);w8Vnzr39Elu?c>OuP&;|UORY5|Q!BKAJ)N|><^%hei zX5GB|Artsc=9;q+imt&mGj0MP!dSEP0eL>`v=4#BwLX+*VB}b9Sc{6lK0=S0t))efOZ>xw*r1@Wu^%&TE4QE*b-LJTCwo)`6xO9B?BZPS zr}tm#QC|J*>@o;E4(xmnggfaReIEy|5==Ci+v7G#S1;Zz3t=8bnQgyVGJ0LiTfVeq zV(gsG3UUff#h^TayIhovSXP;UPBzhYVl8=kP?pfA(vKTSKg2n0$ME3dmUk6?wjBTxCHTIn zHB}}K`Q_+&f^Z%7Y^C_m^WVGRI2gN7V9!+Y#nSL%%j2+EVpl)jUo9#Oe*B)+9M>9A zkx;u`ME>A|DI6w_`Mn9z*qRR&uWNXlVy@^JVs9DHlJPQsaIIb*0rOrP>Qu7{l!)|6`=c2xPK3Qf)JUgi~*oyrdm#6z0zC4FKp#Jf|q z)vIGnK<5WmzP#WR_7Y(f_P*{NzS(h7O`_gCrTLXfaxhqCocp#FCa!RT6MGnYdaXcM zN+_<(OIO(fC#eo!&Il@fiM=AWIg#%_h4|TTid0a3=58 zlNT!2riHJH$Dy6?HAM_@KvY!PxiR=}z+lAdiPs484tUpi$Si(BUtOGqjgf;+lF^papVKbORl^9d9y}%+y}q9rt*uUSGy5nnTN)GSpFZtNrHUnQQ6xi`6%rTQlU?eja(p;hNzx zj{frR6|Sr;A0k?BZVrd7Ex00jd6oz3>IE42%~#b9&w94kLiN~%eZDrWZ}Z3}+N&)L z^`3;5S?lSO->Q(py*{-*7;RoX_Dyj5=tpor4Vqeo2)pdg+Y}7N%I>~N_a7jgwv9^- zX>B$hyqf%hvO4u2(c;1jVz`UWW?FgG9S8+0P zOGe3yE;_$^c3G?9@h#=Zw&>4u-uI*~S3vCh!1-vF)O)||G2(tJ;gpfFtpsslI`$N8ztEgNqmvVEi`o5LDuqhg?sxm zx6$vS`?-bbeAL;?oSE2+lt$BCWAn4@ac5HN8{=gJ(|R{?^kdIjhvQ|f7xEl+jD(rH zj`y4|BbRx)J~=mg51Hl~pP!ZaV%C~=al6MJP4FxJ-Bx5(_-rMp9Mt}uj+=h4e%9J= zFWNXRKID~GZzN;xPu19W&bRi|f8( zC=mb3HC<*cYf;Q^gi-yqt-g}lQKEPJtCwF*ECV;VCSgh@2Z2h!HIQH)-|Vvt|Muqo z{ICgZb(>woukT;`>--lH{bOze9jQ7B^QCIc+*u7ugq87>y0g<4Tb?Nj{8;`F&C?=f zL7Ih4*GmG)=S>=B@*D@J-!iOz;`&|g0rEyPHfN90oY(5d`qppPA3`^3d)8|poH=$z z@x-^LB5-3R{Y_uR+zc!Jrq~l{;<(1T&B{i$`t_cv_03GQDdw$uO3KO*tFqvskwb?9 zCO#yyrr*03#yebahCFJG&8h5{uds2H@u`})xbJ=OXVR1Imcp6-`kFMg=(iZ6@5tgy z#MWl9Tf^@HZ=}?d?5tALdaUJP&>qMa2XBEO*KS;NPL{6I`s9FYzefjbQNKibc%$}4 zY{PyZ))!LOuh0<<5u2qpr7}O>6-Rqd44u`nxD=V3HLyU6)gfed^Otgnk_%KYX&a8P61+ng|;NXw0J#I#D(o5~NrldM?wW6XJ~@d1{-Dl85F z0*VL(0AvdJLADnJnN8!7U9v)Oj1tkwh#qZBfbtw`ELk|~OB7$5A7wq^U5D2gkFFTz zftH#m$;W@4KE9ClFg}mIbF1S`POEGxgcJLsq@dp4er|R^v#BsOF22IST{*6eOJd8z z(`#ZXj!-^l2|MkbA-@)@csWpgZM)Jt#=jc9u<#AEvzCppiuxKdyZn1=fkUR(qALz> zKjQNnQEnyXxUC~`&cJcp9?ia5Hs0uJ^Ga=a)1n8T+0}Y8%5yWF+*r_KzFayHa|<6d zocONOkQ6UnV8+eWY3NSeHhk?JFR(SNIX`_~0*o%$LhN}*oOY($=6|6`6PS#IkgIkuZB%E_&GPhSk67#eHb^8t*)o_vhm6TMLKqRDtV ze^XGG<*Y5>+UwHp7H9n47?Qtx5^a-O^|LNY-^&JJX?8<|F`JR#yz`YIa$|`_((Bg2 zj0N5(&Z~9|zmuj$W%2C2GJO(H7*_Rt+Lt1(low?x^_a2?h5Cusn=A5^m;D%KEiRsz zuhkhuI}e#W{&o(vA)M)biP4v-RQ>66h**g~>BOY!_vLoOPWR3_(`y;dXDcM2{hhCb zxZgxQlN%&B*9elA3Etz$3e95n`fuo#JMgKqC+a?g9O1UP4E0zJxOg)nKQ}QCWtESVsvEpT@XTE}l;yp_u1DndjCvM~$alKNH8Ve3{@!(& z)24+M3$=L)VYWMr4qetgbSUfi@|Lq&>b0b0wP+T0Iby^e4C9E~Ew$ifk&a&SPZpBA zM6Hh*eMt265O@Bs(TKq~x`O>B_xiVKN8%C8cZgzs=jqc720CX?T4u8DSECPk)IbP% z<8@B`4z4v*2SbR2#ArQcufCGGMKl!gH2yJt9;%f}{HTjqkh-3yD`@gZq4(d}-h`_l zlrk1Gw5x8Ufr3zscl^w!;Ui<34YCsCDye{^1?cRzrmmA0gxhA7w=}UhIVt1;=K%AQ`h&?C z#=u^29BRV%SI}qJ8(qriQjr;Y^Cp!RQ<4cCWM^x#em#Y+tn=16a-LL@E108TGR949e zCwVQN_-=0eBfGpB7dzx-^b{4*`k)+r1{bJaVII^?qOmbKyI4f+Una-^GkOyA(xtNg zkDLePCMfyN``3~y$i8&_4`OP0PDS}&^4Y3vng7=OHBETWHnb-%;1HIaz zVQyCco2rE^PacIT`(0c>Q;)l`jZ1cqZ`kHl0US(y?nm)yK8Z;-f%LVsRZruF^>DW; zlN9sEd3p<&qwl>;3`R;`FqTY8cvlc+I;-Z$62iJi1bL;;NDDbyKKjeYUh z&kV~{7sSfDex3U`h*m}ejsEO`_NwbvzGa#=q(G*u$rrxewwH2^ zu)S+QZUZx6ZQF=Y3XErfeeB3x5`;4^8*=6 z{JW1}LOtlV&*q96ur&Mg=92T7fYX;cw0lF%o!ooA=d2x&ZIQ0 zr|MJ)sdW7;<2WL=j}2SE`;t-46LEJh+JIORkby|ui{>4!5EUX@ZSzf49=1uuAlz-2 zT2;-pT!+X%G749RxwEa7$!-e)Xtz~MSdNK1J>Z^Ls69Q96ogW@#}}ieMFLJr#5e9} z;wzqWz|x_wa87DWP@ZTNJX@*RF#S1VHE4sm>DajhTD^nXcDryak3e=v)9;qZS*lpoXDASF z+i?-S>K<4E;sS9Oe@`#be;jYKnVDqL%U8zZAn)>zg zH|XwchH%+PIXjz^e<%rShtnB1^doUgf#K#KvW!FMAg)hQmuBA6(`ZzFcJQ8!=+Ov? z=E8YOk59&zy7qZ6r4yw!c{t7EE{igr>ob~|jR<50%pBBTd zq%-$mo$69qNe-F}GD#th?w6cCZ?Xeq7`aWT+?J_VXUSVnJraCxpF){0ZLe;TPq0&{^x0_&&DTi58GKJtNKW zlc_cGJ-58u;1>h@0+zRd$GKzcF1{5}cr{tgYL=bh!rABQC;0AGg}1J}NB{#gXIp*a+@{gMWdzdb|;kXfTz+f;Z>)~A1JE=&_Pb?1qCYpDlTHk$XC0(iiQGsns zx%kj#hliZkLPt7nH^s^0E!uc@G5)Kz6&&29wH&8O-#~vLEWw z!3Mn@$k|9b>{w0LH|)|qH-D$qc@i_+Q9+Nb)YJ}jZZ#s#HuT1xSWXFTj3b6Zq3rSq z2Q6`z57SBVZdWo${o%h$WgXw&7xHl1nARqh*cUHjZ6{t~&(?JnmtW!C?)awoz?A2g zE)wC@a%kxB<#VR7IA9!$^^10tX82SGqzC@MrMcFYkV~dv8tbK9{q{g7m^0g92c?B) zQQi!Qq}CC6ZnShqG(r(9JXfE}t6H=XH%;bC+o+QZ&c)gQhC^2R# zDDN5-)X5i@Fs-WW^-8X{&$9mP*s^5vC)e+S^s#cM80oxYl+I-I>De;7c1p3u@ zl=0m2@TPgD_wujdU7XqknV>MMu;KSs5=E{C8>I{-l}wtU%$OC27?-WCtHf8in{@Q? zK3iRGBmJ_NEp<|d%-}iniX@xMdLsanNltt~ZEg5vCaostdoQSkAAhu6H76P2S}ZJo zS*$EhiowPC7MkF3xJ!mLK$-ZVZ#soA$=%UW(K&raXJ{oFr_)=?x^PcRS3}Xhk)|oFW5z0wXaeItM%c_H;4NI2bjV5D-Q#NJN1lQFx zHA4b$B@_l-)Af%4(Q^hwy6Vxg+;2iERALG0eK`)^e#qR3T)38^@(iZJoxe!E!Zm0` z^iAgD!ZOi;*7i5$?1wjkKQKUn`TzA}#7+~C{{%M9gAzm3zKaD^9Iyx3PGJFdE}_cs zkw_`i*m5u3JoLoTxEm&i=2upc?}bBBF2+UCX)Fo)!R1K4!T4sVJ(f67ZKIo!{(drY z8q%TB?$bQ|qZBqVAM+7soo#~%#(KIhusC+OAy-P7c<;Lib$MPJwHt|IGDD|wOiAz$ zmfR8y6O&|h+wu2A$vr4ii{3DnG9?x^N_R`X)>~)SeO1q8#oy(Vec@LL}usFw&|znY=Fu-aX1K76T0_CwB&?6-Wc{iPS1K^l~lt9Uh|1HI%t zyQM;RMeMXVB~+^RR!$XGrtCgidD^^OJyz0G6s9nb5lmiIKF)PNCclR>& zSlJ;QFOQtSC?)glfLNyII=$0A0q5?7X-7cMh|SkHdG88F+o7v{`;RThw1a0Q5W+X{ zZW8Nl?J=&qH<<|&Zkc+fqX@Uqh_{}BU7P&*WS`&SBF8{hb>R$_ zlD_s^SY&6-&o~M)WQr{0=E^`V}=iZE<_?o%J(J%!hkG zHokUgB)<%`C^hdY-fr8xaArAKLghUSm{hs8DYci`$ z#bZI7eadn1A3ZYQBcFzV^YUU!W0253wbbNjbup`Nh4OS&kB@CRLbG5ZRVcRdSSrG0 z&R6|1FF9}c+ogTZF?vERLorL0Y@v^PvUkaKM+WwG2cE%O3N<9RBk59!kD=%{?5XRV z%55OiGudYei= z%SXz2P!xK;q8`9F8vCeZTj4CFvl8Gq2~#{FMQD(9t@LmRAqlkHI9*aaEAxk|dGO%{ zu%nB34pfV%D`16|d2{T(&TEQLa$nZZo#J;LbC5#gg!;zBeIB)@r{-Oz`V0{+9^bntgI>^8N9H9$3$E>iCjIJp{NKGZb zAT^zY!qq>{8XT0XT&pu7z*@}_Z&L90tYw@i?7DbPsBhY+Z)^^#b@;o7O@N1S>4|n@ zWeU9;LDi7u!nXE7dpCHq!vzgU=*_C1PGNUPpM{`#zD$<6Kx}&YD0ehAW1a4)1dGYh z__BjlID$<^9IK5#6S4>Vl@4{RMzJQIzwf8uCWXL)KJYv_D98Epko?ujwz53^ALmS-Co6}`3s2VGb`F)=o3{>^5L+?x*BW-&^5=P z@5~^)1<7CV7D}Lj)4$*?m_g{YFIZ|DAnH6h_kYGS&~-<#_a+_ciXS}O6)*Y>qWb2^ zd=F@$Ksp|#dH_T{m!k^tSgdOyXE50kI`ACKZ=WiZ*Ljk|^xF(dZ&G49(^I7&{hIqg z{cwt&2-GEHT`oCwDBSxBB&BqowC6%?o!C*TC0z0`aO z>jXZjy9GF+6nogy`Sb+v+Y<#s2YvsfphGt27hnTLs1M)q7vzRH=p6uZgAxo+aHC&l z`5y2aZx(Ym1=J2u?4tyNR7alD2H6JQi}Mm|iyI`Z9m?l{yel;xDZ(3y?ckQZI^_0P zU=T-fqyTpc6uuSsgZB?Uh(gUkEt2L5xM!+1Kn@nlp64#Tno51Ozc-02-`;4K^4NwW zv@YM%;pF3o->8JcCz3#*9^@hBU!fS$XHB!s{5Zoo(b`_MRHNv1$4Srn^d=CQVy)V% zkKit#LE4I(C-Kyr-M8)B%sdn8MzEv z@Lma>s+6ow^*mZ?4W;MKdO%3#)NTPU01@|4?|J z-Cc~*0M|W0?Mcosh|ztkf}Mhoe{7|Ar652M=#~T=+W+$>y*bs-&kv`Hx}g;v4a`o{ zstpeGSvQZV31a!cpcpu{R#f!3fvK~1I+|N1&#PR#Fk85ZicKmQw>7u#xa~ONuzf zb+@_q^aU&5XA2%8pGWW~PJix(fz*FdFMPa6WUwDqk>@eE;L_VYRbTv?`7M9v?vFk4 zfq8`s%U_Vkc9&RZM;o^0K3i^xx24aN zlwx{McG@2pM%K?CjbdYj9M6ObFlBKckV2{o2%F#JBz zU)OhqqJwERFwPp6rf)}!(KRj)1WhHhoe&`65lG7%*F|D5RFcOr>Nxe$%)*x`!f`L< z*~_nc^Ch(Pg%yMxjd?qUmLB6_XFmn+wfhm*x4bibc)QSB19wOPb=K_c!d0|)ICEec z05tpFKVL@vs?6ho8gUO5ri zxt?b1zQ#T6F)a&eum>-EjWeol&;}YhdlL%FjT}?P zw+q~PK$oFjN#)qLrBuZ4wE!H-=jVO*wZ?DA|0Vz;@_%-Y=7L4GG8zW?OzEy`4@K6LfWrqqCILenEE= zIq?_L%LW+Wn@*eoWfG{7nl!3;VK5!Xo+*Z}#Q_mM7X9ufm8TGte_mkx0 zV51oQQr$%pNX+k94e{0r1`3JA#RyMGJ&MI5txISU_3Deu(IyFu>O&L#~IMCTk zZVRHQwKfLy-$PFgE8kz}BtJ!f2O@kOC-FWIIq_q9D}|N==P9lRGo8p(&SyB0-L<~T z1s_M%>?n@y&T8hltSn?tXjh&*+22)=WE#s(Rr6?QYT;GKj@yL&lyTGB)Uup0&uc83 z3%js9*n*lI75TfUkB%gIND=9hU-XYxU6kJ6&i3Gy=Gy}1c0a;||9a?KwLAeuT3-0!(Q@0|Q-4cvy5BfU|~7|fJb zf8wGzk8q5m9od}QYzD`KH9~|NFFmEha(b^1MF20!7Z|8JGNd>`e6mH^xe!m-nXWD@ z&kppWpYcey!>f%OB04$ORStzI1 zN)x)3Pc=Htq60Dw=SI{}(kyzkx3qvN>%?^v!M4#ZYlE)~*3k}FmZTXpuk9qG3ptas z!4vBb{>ulUb1G()m-&#SC~f(eB(nNNV{g=9*o0q;{lsXOn#*(J2d9^2o9+dmW2@-| zg6-2qwXVxx+)uB+i$!RL(at?oQ5>32X1r_GRJk~*UW@K;! z3=;vC{pM6p-5J~_ceEy6kz3k1%kaG)oj_Hj*jk+8uN+?^vR>Xi@eXr>Xx?C6TkI@w zH+vNegD0~_4KcwUdfm$@H(Xom&p;50-+YkB_dG~$__^?k)B8$&2`OcpiK8v59;###` z%vbPmN@qg@=uN)j4{gm+xFi;%6K`&wr1r^XMBnAE5}pkbjB=^+pRe*iZ$6p`(h%sK(CyUIcZGPeC-0QVGCq8w>cjSIX2Iqom zI;tyW{YQqNlqY1fn5)Ao>vl&pkPoGN&f({S zoOWPX+)c-qy6x=>n&H(w)(DA&V4zDwdp4M``1hKkCwTdlXwpNVarmHt*u=gks?Gp3 zPj)vpfYJ)TGdb-81;P7n6WcB*Ibvho4$tK;OPkTGIn7~wI(6B%jyn&53KWpQn)o!$ z(%$&9gzQwHv>LCc+rB9-|I10+yG_AUQgKt{{kTB_3h0q)PtY({)NTh7J}|^iSG2~R zIaPJpC)UZ}Z1+h);6E~%t?-ZkEKR;FTk@8J-e*yl4QMtx=q6jL<`KW==?6mio=@oz8 zd2(=aA}tT9_;hJ%eKOLL9i=&6?d3m?zk^UVh@; z1l2T4^Jus;jdqc-S8bWD)3=rTy!IX&C4zP8E=*G}5Ion;)#*hEp@c0TfNImYTO)M_ z{a)1@&+l_P8xjx0zt7JXvaMPk(7Nk>-;R|%C`=0w@1Q5o*)Gk}Knsi(^*G`iV<_{4 zg(*0^l#!aKb&!zhUHd)9HX0o0wp(|0K1elcVO_*?YWTiqTRl^Z%LtF8hM5Y-hP2a;(-Yi;s1ykiMS^#a2GRQT2GEp#JB%8Bz zw)P8xO~0+#7gh!t-XEd089z>U8BV#(o&d##3vo%TKVPysp*{O3_Ccv_q21W(xR0iDWeAac-fk^Q@M3f2m%RSz z6jRt-N$dL*zHv*`7$sQ_zXGQ7lXmGjZ!*1_J5R=EPui{6RRkr&(mop#2hUR zrd&FU#cLo9m<}8;Nd+_PIL)I!w zCp8{C`Q7qdzg1c#u7zumGXHE$i;`!*wAyU+k}9StoTVkOTCgkhRDTyNzQRD0co38< z042j;R6XD3v&)Z|oc998#zDD6^8^rqL>=@MmvR?6O5gy(N~hadE@NK0DM@dua*_>L zrel{u9jwn1ZeHGK2{T^HWZx;hQeM0h=bWwk?QZGWo8LQC^L-6%%CdmBdRA?pYRf%V zlnuN`16{>oUdn@b3E9|&%}aF`z-9zWe}AEb`~7+`GjkFlOvFLq1p2Jt)8hV&cup_nDoopi;RpY?B(c?b zQ7LB~N6eHr&dKMjI~B+s{djM<@Y}6Dym**=)ml#^o3&x8 zqAyhXxlrOc@~x6;6eD6eK@0j&8J+II^EiQ)>8ct&gyNP7WiSRA427|IDd9Ny^~L;Q ztxz1)q7Vg#!jd)!;ZRmCf4hKKT_YSS6xHG{W%udJt%0pxXyONXYu>GXnE@D7WC2Pq zo$AraWXdxODQO)oaiOEmt<{SIcJW(WU3RPJw53P)^aN? zUSS+8q%v5wvC85`wThWZRVw#Y^L1+(Vd6R>ICJTdHUe+R#rc1@HD_>8`QV|n;PB*J zHz~K8zBBj$FQi~yT9no9Djb#a>yMs-yWVO>}e$@CK>4MN~l91A<-zl{_D8<|Ex)2F9DJ;KJ6PUNC zCI&MTjKR$rCKj*

oam$W zy@Ucjvs;|3sIud*5rboqUkUA#&pwUzWuPNk^TF_G52^6cuuzE8?1$kWwGQ)N)#!!M zDecPCH1znw>Yh|R5Ox*=B7BIE3B1oIZOY{44VqAQRB$@vCh3U(rBjr0u=Ho`WLsQeJ40Ty25v_e^an z?nQ|60GI5kh=i0W?sT_!z7H028&~TvZM8V{>e0@?9z4q?JLTj3D^m5<@W_CwWNhPH zv-y!Z?Z_~%bU34xk`e6YMQJ=PwkCJ#M@9KLk0?2y;{K`RJ&)HBuR=_MTz9rzzN0B$ z`R=Ac2kiKWYB8;`p3ylyA>g8pq6okdP!tDpP+yiA-Ibxwc)-uApd)o_#c0W)`BUB?hX3c) zmDCo}wHLw$*n@~jJ$!63s*oGLjvuS>SlMg1+G~*z z_v}tuNxf9ZSPlZPZH^j?SsY9+qvuUItT=exDYo@ew~KAApKY!e+xq(3KUVc|R;jX@ zvd@Haf4b{VC1S-(H!0qA2SJ-AT5Q{VF#m4{;3WU-G;Wz@H`&1R`+9bZuRCOaky3Ih zzj<^HO3stpy#9&F_qfe+)ZIQ;nfy8Bg7?UYetYT^;M2y%IG@cWx zGN`&35F5^aVf=D9p(gUbdP{I-EiyzLPmV8og2=CFk~4m8@!mh0V)~&-8jsAu_xcj* znhD2Hi$d8R|LK+I!VdC3%a1M)QqdHR0Jm)8`t?bJ5;m_nJ?Zi!RSQ@6gGut#hY>xt z74EF*N}S!!{7lO0w*C*PD$!$ISnu9|>a+W(tWwO0oSbC6K(=}2;p0F)nd9$5K9djG z(%^X4?x6|nBt*V0F5>a(hub?zN+0?7nWGOa2sFkTCl@4w$`MSZD2P)nLwoE#PVVn8 z;E}KJn}7He z<8%Wl`B_o%>m4nl7Uji4+RK;dGuGK$dyp|+Q8tn(|H^2$uRQ`?P9|abWU8%I`E6p; z{1_3;k)qmr1II zoF#WdRC#NtolWEeTts2>V01JxY3i%~8i{&Yq*h-OGg#L0F1TK|q57Nu?`dg`-KKK)+U&4Iy1g? zuNVy{8R8^hEUWLFQrK)MNoUTXTl97eM!IlC`?$xzh#vm^FMT*H<%iCmJo)Mw3?-vYUMS zJeRdp&#Z5VR7d!$65)rE6ff5A4PTTg(A%vmF(i)xVn9Q8xqo8 z)>7;zCOw;qI1IxQz;8I~kVZwq(-BMi{T{)VIpgwY!3dNg_DC)sRCpy=RHVr|OC-TC zev|@Px(C$PKN%1URPLpYImlN!NDd#Bf67=36j+HA|Q$^GSnuL6Q77q<-P9ccO?zd_lDdy7^uUs`&qEG6Z= zw3ZO+Miw=iq0_jRnrHpmIyv}5H1G3ZH?KJhnkIN#^g#T9(YcBL9P0rYp{!51@)6aK zpO7!B7fUz#a|BMwv#dnCOybtYRP4rgeWim&eEt~9P4NpmI+A(9+8eN(kkJ z?7Fg9=1^pSBJ|N&*hoT^`*=FBIF8WDM^qUNr8g*?81j{h%1%&zPPU_GH^<_dY=b2x z<~JmxjJwmu;oK!!fSJMD*XVUXZNz-XG6M!B{sD2P zvZBTjTzK~xOL9@ZVP*Y1H&V5kw9Kj_v^&Ub*LYU) zRIsmBOO!v3II4$Heyc_}ndO~UoY7p!E8X;9sJ7+#z^Di)`;GNAabs9oEZqi;-v&H7 zFjONrjN=eqZH8i3jWzc4oxQNr)l#SxT?^GZ6qaUCxd#@A-x2jDR8Z>SNEQ43_3uq1 z?20U5r?&qQ5Io|`l~eYug7 z&xJm-5;lFFkjP@~>c?Hn=^IhyN#c9tj!~=CM_wBZf6Pf*m+(I#x(AAjFc^4ohxB&7 z(oI&dXsRJ=#)8S~Qg@coHZzPqemBD)*k`js;HKGAfuA7C^}Sf>bPZ#P!+TS~>E+Vd zOV!h4ol2J|CgH1qvCDLQyG-#2#3aA!g5Ir#c!r{VdS5L58HN~UW%1S%i?EZDd1Col zExOtlU$lnS2v`;0x%L84YvSQad6ah%%SNWN_>%(t6S?NChPTva*ScC+YUA{jHxLB8 zBt}17gq=`aLJ>v(^*tSDsmbBD9#=z*1Sx#IM6GM>5=yh9<|exV@fhO~YYC2Y!cI2K+O@!`xzm8KirJ7=Vo)C6)F;X`J{=e2ArKU zA(UbD$Lp^hay@1g9RN+2XxXUwa@-}orz9jtx3JNNxCtM?zE*`+c%m~08umTs`}LsL zkYi@)L&GX#M;?x^U`Ia)CP+E#3=rG1h6$MYd3jQ&G^!@d(47&!=cDV>@$Jqv`>iC)y1)UT zq1LbJ5hu5o!KzDr6h!-gM(=V9Iv?kB&&=`_u-z!&zlV=g1w7^wjStM2;cYwlsJERp zCn2uCI(|fc0x{o@Lf;5Zn0_H@W!mx1qJixH1RF-2)^v!r=gWQzpPkJ2*z#vl^n!M| zELU=SIQg8}k+0I-%}W%I#9h)M^TFEpDj(vTv10Nlpm2nGvnn;+mZRk`{4~=T$tkla zdX0hk#A~2eFe^f(_yjeU7or=h)LqCnS#L zo%bSEDAeJqp20PZD{X>j#8d?jnx~Xwzp`vT7Yx#@Y`^itYWc#NeNy)0vc0Av-mUR zjE@Gt{ah|`*jLh}H+o*qdpRMynriP6ctd|c|7fB4xynAlSa9uWKI)SwlVjK`n$XQ% zjlHjjgx!Kh>7xugeRI6ggSiBR&dAez zWO--hQ2uhOVz`rRd}g(V4JT>gjI)HIb6X~3r{(UJc@AC%lYR0DZ1}n&Y!_-dr${TZ8M&t(4d6>n^T!}eV0mv?OylTgLw10?$QXHqZA*zy#G5u#=+T55rM zq>9g`EVpIV$j1B2`1h_4>?oYx2yR`y+28r|m`Zx}qGcq4Ei?(@UY8I`MZZq9T_;hW zm?ejNvm1BRr169rb4l7QYM0z&5NkY}6wj_MlIdy%^7fP@Y%N1|25H~9?TwG(9>3cg zcVvIKxv_8&-d0~7ZC8G5@8?y|+~A?LfU8xL$B{?Vwg=FOrQAt zlN!PR;z#zZ+j%hvi=y~}B!^UAEu)G-r)!jOK(PrOi%PdIe#s^z&pvDH8fLA^Th zF3c1apLa&#G}5vR^Dks@CpkPmUm<;4KQqMEIHjE#2k_v6`N3UpBAiKmrsoyD+{&NH4{zb9}`sNWvv-_c2-IZ zYWFIOg9@~CW|t?vZ}-dVw*J2OS@7q=?Sjl-^CHWX6m3#|*D&qEFzs%xaO&%lVXOr6 zr{a6c^EY7+?)fVzc4e4n;SP9oD@S-w_uU$@R4+T}jFBlO3q#CDkoX0$@+rc9*k%+P z5=b}6P3G#A^^TJa>0;d1sV8fhc~?L-fx5leii8usK0qco*|&B6i?FAS!K7y?i6zG{ z_BC^((z}Sj#d|%2cA8}fUMJVA_XpqbC*?USwtqDh1dh-5%=sd*bQD;OMC6J=@Ka|S z1GQ()G_srN=4z>W{-LyO#^-Yab#qDrTRz9@2ete(tgDU6w#M`*S)vKX1h$(WwF>k0 zsyWuSL_UFO_(2@=SCb|#%vJ4SmjTq>wjzQN2XFYG#_uHU-EM5sC04a(p0W`{6a1q7 zdu9Qfwh=DXB9v5IoX%~0=TTDI5ph(mRr|6o%i^|8jq|J6Cu>mJVf{p7jB=ORI7l0z zuGz<-d2SuWz<`_$ceFY>z7Rxu`zO@)BF&@+9BuLr)zJOcvwkQCHmSMc^4Vz>>KefjCrkbD}7h z$rK!1gIO8HhQhir#HKCI_TZxy!F|;vc&(U<&lX`o<*||VI`SDJ+X2orgU$f4ySZIB zV=p2s)JNDDBaS!nOWxDeRg=o;3%RH%geHb(zZO!AC zZ=`oh*pbTs>$g$2yQSSGwp=9>C5hVS89;Gh!Ktt_&YJ2LXowlJ1no{<8_VR|dG$$i zVvqXB^rL-0#0B}1o-gY<+2UiH9oA}osaQpz#^x)jttS|>hkdO71HHbV@@c2bvrYPT; zP8+N9G97U&hqzF$sicyYg?+#XU91)}GU{)|C(+m~gTl0v81shqSN_FMqGAQGZj*OR zhq-YUeRKzuUORS?hpJ^b?jPOuuHYJvQ~3GO6X=z!oN!(+BE9XL6+*4dkC>l9Em2HJ zR+GUpNqFIBqvzB`)TX}OgDxk?6WxuBgft*}$2Icl)=9$rfKc^4dMuf!Y-Kq5nk3gt z$iRa)oC20@r!?4WKHN%4N--$Tq(qG8Nec_j$!N9l&3@=^=N*_s8RwmhOMFS;Qgse9 z6NN{wn5N*ZS(0u>{$xvT0r%pAI*m%gNCHfdZtA_@Q4+vdYA%DPUaC)dBt<(bfZ_Mb zcGWj!*e_X29fe^YFP$YezCJUqW^DD~W3eudqW(vVxE`!bt(R>pj3*K3RL%w@Or`kn zZE+G8Z9jciyAcWL5(Td6X?Fr6#$50$GmXFec!F+MhuPOA{&aC86m4$QJVFp3Wi@@b zfjlbf+;z3*d1g9wV+Jn6+p8tdjKKZIjyGIq6`PEqHV|zk+D0ThoL-VZbdAgVEKfCa zS1~vs*vIGu{l_+VbM@xE6U1iw(90@C#nI9LqCSJUMY| zD>$>$A*du^LeV3cD8kGVuVF@I9E_^Bk5bWOGM;l1&PkQAbd6nPdMp+3hn^>@r!n`958-USTH6OM9$kCI!LUdWqZc{@ zP1p!kX0HDPjUKnSy9|06?Iekw*`L;Ad|t2 z$G)mLfrddlZs8Q`W_fARzRZ-v)r&wJ?+qzG1d40uxMH7iS_d*MqAx>AuSQ0aO(97q zV|6l^|9D3j9dhGD=Z*dOk%<4wBMWW&vO!ojqdEY&xSGL(% zua8JwLbGbUoepRGWbHS{W3CXXDbs^ipbNOPq$d=@?)AYs&wzK7MD1IVN8X? zQq+~jVpNzi+2!70>?sAxlZ=&6LHtWhaC5No?@GZ! z4sm(e3^aj>>POJFoDcOw5k32_+C^Yjj*k#Te9jvpWqg@I%^GqdreHQ(E-sPm~<=*W}0++PUlhsMxK6hQx zos>7CwtR6oM!~ymu$wa;VDrS1B3}%>c5l5^nQz>CpxPoUVQ^)NSEyr|cv1`Tnd{jw z80kq5&crqTfi8Jt&iaEfoG(GuE4-7>X8g>)?R&c+wddI3I;7;&_E!t4#DnB0gqz8t z3?GhgdJ-0w-jAXlLUfc{mWeCl?X3K4fhc9SRA#mx1j1KPxfebqw1@oOw_VjNTbqo7r4`EtEmoGjYPh`Hiw?PR}AB zXiPmf!ipZ8juzakZdyT-Z<*EWjU%v}eG@eEzwz0oyB#Vd&91unG;fQ9JL!bb66F=L zK>Gz}1=w30jMx%+w8$y&{UtJs$)MzI0Ap9Qh4mjX$bDIsy`D}KW+aHm+ z%L8G|bQJi0l8p5RGhSOL2xWYZ=?jeaOb-rx&!!ih7zpGJ<5Ni_9}#VAhBug7vk6GD zKd(}GhoLsF6J%Yp2fD4}9-u#;w$zY5?&?3%dHivIVWo3Mr4vP&;8?{mS!vF_b^(%lMh{8SF_(W973;fX93!Ky8JBf#=JarnlhDb%Xrq#F za5QDY&DZ>RY*wNj6)nUGHSj!N{TM3p0i-N|oD6isjPJaQ+0ylpu| z4~r_8KkXIE7VV0OJ$RvSF}*af?QS5Hydqq$u$twHusS6gc)AYPLF`P#P$yM55_a)o z5_v_N&+wHC_B_J%c7klLsrx`F8t>QR7AP}(AxMWn%_$V?4?WAp@=PvrOQT{uJ}dUs zm(GoK7eVzMSza_4d#20&4tG750;rje)yp>tN1_g~UVFhbjDAf3Nx)-p6d3MN>OC&_eHsR_h6v)6Qk~85t52>K3{RS;~d1W$xz&t z7m(=5CGwRDY-8(lOyYdk7K${X&-7ElCSxSe#OJ}G1alN9S;{%vtx=5bMN}mFG4(04 zY8$F86Fxe=?g%A5o4e-H>f^~2&9Yn*a(vMDIx)L?8((qKWcLY_o-*#co}{~O$YDf_1QQ|}H6`%6Db|t>pMJ&T|NgD5>0dM^51Y%bFbE%|_UaqvjWI^ini(T@_1$jV?{=^%fw zjMb+Ks=9515tAR1%tFDU5@vOW0;>odqV`V8^LF)g9ATCEJQ|HSYWL1gc!JkH$fWHs z4r+EcL9ilHgFBA!b_s6O2;*7F*z_1HLlRH#8%5h?^gFjJwI_>boSlbN^l zL)M|s%Oq|gJ{SM5;RaWO;)?dw*KIgHj@i8y5Q2RH8?DZmln-CuRD4aCKO~8bk5$x6 zIQSO;WBfj1y_)RAP3A+#J-VtUJ#J}IIT8c9<;}Nwo9l;VX5yk^?Q8R5_VX6a_ab_) zOF;E#6@Nj4CI2d}%DooMwMCZb_JDM&<1>XJj_K{bho5BKpxNW}C+AYp?<(L^i73p5 zvf1URE-APrc^&bywvUBSf<$n$%V8PcGw*}`Bc?H^?{f7rm_vm?#+me9BG<~@+8&@T z=|olq!lDvGe~YrQHcq`V#^3x(EwybTjv&FN3UPhe%`C!np-H%Y!@PwfkB)4X-oWF~ z0J|ow4Ln$ePFfxPwmR5Uzq%-|+0)yZS*oIh)hduq&()EXe#Qj`3 zQd61_XLjCg*gB~Te3?q_bF9yeXzs~BBy9ZoAnwr7EW?%ceZ55E%SERz#c6$|TyZ?| zOtZ==sb{f2;jUR{TT;K0kNyMdzC(B-qxail2?okUkNzO|1-j$*FGPN!f#Z?mB(0*#9gj{1T|{wS7JauA_W zw85b9RPilZtb~b=+lS!1q9K!Vn`Q~S3Do+kMWS9A%R*^1n|NQ1gDzuFs$y%N1t`!UZ=zW=p0A-ecv=QZkm% zn9Zx)a8B8CDn$95FEWt2GvcbkzkTW{ZMuJS zbTPMG-{nj7Z2nJ=B|jG8t9uS4FfBJ`#A0NU6>;F(a@u=(%H_4KGf(zQ0)G*H8oY9; zem4Fu*Pb7d@&Z6Y?hZV%51NAELVr7FkP{)L;i0B?QdVGkE}SZbRGzV5U?5LqG;X0A*6>wq6^R`@1A}cQLEWQNbj{nzAx}_a%A`#dBAm z?__LGT(lE64;PxQL$fHPHzev1a}!RhdK@di0kLe?hT`W8pu!x>y`HLo(T@jJh7<%= zyw$d8o<32rtbkc3X42(51eKu~Mv0w<%op68`QdmyWX4xvod+sJxi8bzUH9$$CTfBE z*8C%iuskR6X%~B-@J>R$rbbTq@0c+5ZvFqT}QGjp#)^!Ue1HCI-==eofDY#S8@O!J#LN|5!wn0Hx zPC=*7Aa(eD13YW^MZxk=3=(V2W(*K(@BwcIKNs5B8xWHG7G*-{&pFOXQfv42LDS|q zSp86OTU|*#cXq*C7J=Tcp?(IKfRG4I|7vjI`gJil0vBR1P%5=}5HQtE-r*@21)$H* zj?|D@Ej*p2yhP^yAMIUdSX0^71_Voh!4g44Q5hjb0)}Exl%j|VCPqM!BFzX&mnMWR zhzc06fJDGhq=-PI22dcOg`o*pkS4uKCz{X#cW1ocmGR!W-#qvCKKa@x&39HNNZZp_}x9Z3_? z@&0&i;jPF8Gl3N)evd;%FhtiY`KYV>3zoNtjeqImg?+2KF z5))`%r)cZBz;L)FNDRQBlv*>Z)nl(Z*f%B64fZLcT8g>#Te5yXxc6^G4r$naYLtNc z71oZ@BSi_A6R6Z8d$iF8!4hV zTdP>?gndJbJ#I=$%*5w|J*C|me!m!&B^ed|G*BV<&=A@cVBM5cH{Z935v!&n^u+XA zl~R!oj(|D|ZK5uNea_LdXMx>z)z=j;FAnW0+|)xyAY(b1Q zV7_h=%L})+S9qPH8iw0B+<$mjRuWk*;yGU$6FW%H)DR{*bR?r@5gPN=P0-ezo5V`R)MKAO8WZ6pLs z+T$AoNNLD9MJs&-E4A6d-oI;J01Y>g+U6gdt3+abZ3w=GgVfVA<#5K7G*?_`79IMI zAe&_O0v9J!o%u?SWY44S2U|rQ0Zxh%%)YKRQYJyR?2Kijqzw*oN1-^~FSZ|*a#pmL zWRD#>Zu3+^wK}B%ggDv>aF|%9J|#yhD2F_={A`=0IN)Bi=2Kav(<#{>WcBlvQZ2$J zx!nUO^vn`msiI69UHDP*Og=0Hq-AG!v&ssOsWO~Qc!8v!s;*d`fkVSZS08|P%HN)E zH`qsT^1(GI_h62jl%gi1MxtGeLhMnGSlj9H98Y+Ju-l1<;cLmeH z=LV^CFW%5GC`)ORSkZmkV4N)<-P{N`Yww9J8Bfre43blwZTw7TL;qpZPGK=63d|N) zmwkuw4!TUhi-&%@b2D6JaLn~p$_;8L0+*z2EINZrs+2?V zw5d>{S}*Lj;xJ1)-*9cxI#8z@dD?2{kQ21vko=|JoXhn^%O;f{w-rBdltn-?<_>RL zls0xf{?z!+fQPJF-NTqG^4M3?-eiG36*TWrb=Or@;oT|gA@7XE893>8mtL8DeyOzc zY|#*HGaphHtIqx!35)!4hz|}a_Gvjz>giyi_>i9zFEtQk9PFIyaRnm)P@x;(EQIck zc4qWQ$8|va2JLpX1OCn_@I3pl#WZD3Z|w5N<-uy zUx=xhTntazriFte9?g1cLTOcnrK$S6B22nBIEdrZ96gCqFSwW`3$V#+_=9np4^d&+ zNV_wpiWy^c=C^YVi{LJDmYLBqFWF{u*r|FZIeti(-cSC@!jde%WEYoVFT0`^JQ$8! zMY854vSN6MWjd3pU#88l5y2MstMO^hnHX-gT4PX^VRuQ7#v5Y@zsmVzLU8@dCXn7o z34TAoxwQUm5BRT-kLjGW(1Znet9R)*hs0jeM_2n^Xk4@# zTxkeZBz`kPX1ui!4*zvz_**43VRo@-Dyul!dRmp4R#l3Sb8=qjlthh*qeFz>D~+cH zgmvoKpHP}I50!Zv)0S*+@#^2CB`Zsv{#Ef)*PT}S02+D~_W4*T@3 zp9mL8;x`~|Sdx4kad{9mzh&0sRY^_VT9eQ<;jVCE*l#HJ1D%o@!$OGQI zdJtPD?C9;hptRMgc+43@M3ExJ=nJwc76b zbj~_Vq5^nuc8v2aDKhScnNtE2mmOb^rOJzT@#cwm#x9Lf>^_D+4wn^&IN|F}-q}p8 zvs4Ef6rBf&xkvoYjrXfbmfSYC5QKT$eVcM!ooyz)fg>h^9B0zJ9fcDo7;ghG3vyTE z=x+kpFYv1~;I9+*X*``wH-<>HSE?rfDCFkHr%29}JI0EsI~>+QvW0jye7~&RbMUS{S%EyX@wX z_5g_sTX};V_bg4uES0}7gu`y>(-umfDc9GoancE)ES)Z_)$m3e`KkAzJPTsZR3TZn z#2QI?yB8R9aY4MaPaeNO_q_mjI*6}5t9PUp#yTqpb{`K7I3auQ-b-YnSo@1i>(%qn zwB+IoS4dos^jZ)VNqQDBb~)D+CO5bTd!5)#x|3oatKjq`CrVKkO58ba38R zeDyla##>r_n2BmF$0xsuv&m-3I2^KyV#au3O&YHdjJ{}es?MOVd!##+2=&sBI&PK! zRNc?6MRkED_;nqXX(6~P1;xCnnpdjkR-_R=x?T2{dNd?Y7KecVrVmfX$~FZA3FJ? z)>;aHxEvxnEWBGA<5IVRm^(3CgUrQI`3)+zikC|i#aXhgmUf=baEjcl8O5h)X3+A9 zo7?B40Q)>_EoLQ2f(+ebd(5+OC@OwTNdHsj-Z+E!7qZU_^09fQ~ zmq+3Bu(;1mIbco0sw`q1U)!{SiNrx*d--M{{u*_ppMU$h#S;i4 zRRTw!2+;m94+i&vmyDZE>*%OL134C)`BoLG_-0RFV2f}v4|u{Nj?JAhEU>*;1f*0d zImflDTb2POM|GK5NEPgvrz`V#S97qzeZW*Vxv;ATyp$z4z?ul185RC9`~Ml_uauhU z5?o@sv(`qPeh*vE^PXo;0@OZEITz?O-38hiY7>BNfE#hMVDIPX&TbIqwcth<9|pDk z1U=#Nn^#BFS4H2uRi)NTW|VG_>_NWHKm$sv?fP@V zldoPnKF%AL0V9;3gU(t7g0_`1X0xF*9`4#jOfSIfLO8jlOKP(Kq=sE)^!eEzhXQQ} z@Z;4y82vtC{z3^Hf5ic24I`--L2J-Q-F0yN=!hiBQ-=_f%tTN)7N$Y-| z6o1wzo5)#~{_cuDj_7|MjDL5r=%AKH6I6PLfYp1pWgd=EHyh diff --git a/docs/images/styling-elements-1.png b/docs/images/styling-elements-1.png deleted file mode 100644 index 661a3e857462ecdefe128d406865c997b0243aed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171437 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ª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

E(wuwN+TzO`5 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%0AT>3ni3)7uPNReL*pMzZ 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 PsE>k-iZoc#B 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 Ii0S$N4?l6|?*IS* diff --git a/docs/images/structurizr-banner.png b/docs/images/structurizr-banner.png deleted file mode 100644 index a9a0b0abc1be158cdfb68820cca61d401ef6ebde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53460 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 diff --git a/docs/images/structurizr-logo.png b/docs/images/structurizr-logo.png deleted file mode 100644 index 9324ae8dc299b4e8a912e01157dfc8505027cf56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10206 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 z�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 z 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 diff --git a/docs/images/deployment-diagram-1.png b/docs/images/deployment-diagram-1.png deleted file mode 100644 index cb8600ad7c4966d5b8f661e67f4aee249ce4db7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 424108 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 X>aXt{ASR6=ndBjegitW7JZ^^U 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 z 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